Time complexity describes how the amount of work an algorithm does grows as its input grows; space complexity describes how its extra memory grows. Both are written in Big O notation: O(n) means work grows in proportion to the input size n, O(n²) means doubling n roughly quadruples the work.
products = ["rice", "beans", "oats", "lentils"]
print("oats" in products) # O(n): may check every item
stock = set(products)
print("oats" in stock) # O(1) on average: a hash lookupThis lesson defines input size, walks through the common growth classes with Python examples, explains best, average and worst cases, and covers extra memory. The examples search and compare items in a food bank's pantry inventory. Complexity is the vocabulary used in the search and sorting lessons and in most coding interviews.
Input size
A complexity claim only makes sense once you say what n is. For a pantry, n might be the number of products, the number of donations, or the length of a product name, and different steps of a program may depend on different sizes:
def has_product(products, name): # n = len(products)
for product in products:
if product == name:
return True
return FalseBig O keeps the fastest-growing term and drops constants: an algorithm doing 3n + 20 steps is O(n), because for large n the 3 and the 20 stop mattering to the shape of the growth. When there are two independent sizes, keep both: comparing every donation (d) against every product (p) is O(d × p), not O(n²).
Being explicit avoids confusing claims. "This is O(n)" about a function that loops over products and, inside, scans each product's name is really O(p × L), where L is name length; with short names that's effectively linear in p, but the assumption should be stated.
Growth classes
| Class | Name | Python example | n = 1,000 → 1,000,000 |
|---|---|---|---|
| O(1) | constant | d[key], list.append(), len(x) | same |
| O(log n) | logarithmic | binary search, bisect | ~10 → ~20 steps |
| O(n) | linear | x in list, sum(), one loop | ×1,000 |
| O(n log n) | linearithmic | sorted(), list.sort() | ×~2,000 |
| O(n²) | quadratic | nested loops comparing all pairs | ×1,000,000 |
def has_duplicate_slow(items): # O(n²): compares every pair
for i in range(len(items)):
for j in range(i + 1, len(items)):
if items[i] == items[j]:
return True
return False
def has_duplicate_fast(items): # O(n) average: one pass with a set
return len(set(items)) != len(items)Big O isn't a stopwatch. It describes growth, not actual seconds: an O(n²) algorithm on 50 items can beat an O(n log n) one with heavy constant overhead. It tells you which approach will fall over as data grows, and that's when it matters most.
Worst and average cases
The same algorithm can do very different amounts of work depending on the input:
- Best case: searching a list for the first item finds it immediately, 1 check.
- Worst case: searching for an item that isn't there checks all n items.
- Average case: for a random present item, about n/2 checks, which is still O(n).
Big O statements usually describe the worst case unless stated otherwise, because that's the guarantee you can rely on.
Python's sets and dictionaries are O(1) on average for lookups, inserts and deletes, thanks to hashing (see hash tables). That average assumes keys hash well; in pathological cases, many colliding keys, operations degrade towards O(n). In everyday code the average case holds, which is why "use a set for membership tests" is such reliable advice.
Some operations are amortised: list.append() occasionally has to copy the whole list to a bigger block, but averaged over many appends each one is O(1).
Auxiliary space
Space complexity usually counts the extra memory an algorithm needs beyond its input, called auxiliary space:
def total_quantity(items): # O(1) extra space: one running total
total = 0
for _, qty in items:
total += qty
return total
def sorted_names(items): # O(n) extra space: builds a new list
return sorted(name for name, _ in items)Common sources of extra memory are copies (items[:], sorted()), new collections built in a loop or comprehension, sets used for fast lookup, and the call stack in recursive functions: recursion that goes n levels deep uses O(n) stack frames even if each frame is small.
There's often a trade-off: has_duplicate_fast above uses O(n) extra memory for its set to save time, while the slow version uses O(1) space. Choosing between them depends on which resource is scarce. Generators can reduce space dramatically by producing one item at a time instead of building a full list.
Independent practice and a worked check
Task: analyse an inventory scan, nested pair comparison and dictionary lookup with explicit assumptions and auxiliary memory.
pantry = {"rice": 12, "beans": 0, "oats": 7, "lentils": 3}
def low_stock(pantry, limit): # O(n) time, O(k) extra space for k results
return [name for name, qty in pantry.items() if qty < limit]
def similar_pairs(names): # O(n²) time: every pair
return [(a, b) for i, a in enumerate(names) for b in names[i + 1:] if a[0] == b[0]]
def quantity(pantry, name): # O(1) average time, O(1) space
return pantry.get(name, 0)
print(low_stock(pantry, 5)) # ['beans', 'lentils']
print(similar_pairs(["rice", "rye", "oats"])) # [('rice', 'rye')]
print(quantity(pantry, "oats")) # 7State n for each: the number of pantry products. Note that names[i + 1:] also copies part of the list on every iteration, adding memory use. For the worked check, the worst-case growth of a full list scan is O(n).
Reference and next practice
The Python wiki's TimeComplexity page lists costs for list, deque, set and dict operations. Continue with linear and binary search and sorting algorithms. Practise with the Python exercises and Python quiz.
Time and Space Complexity Summary
- Input size: Complexity describes how work or memory grows with a defined input size.
- Growth classes: Constant, logarithmic, linear and quadratic growth summarize common patterns.
- Worst and average cases: Different inputs can require different amounts of work.
- Auxiliary space: Count additional working storage separately from input and required output when useful.
Time and Space Complexity Examples
Each example shows the code, the output it produces and what each part does.
Time and Space Complexity in a small program
products = ["rice", "beans", "oats", "lentils"]
checks = 0
for product in products:
checks += 1
if product == "oats":
break
print(checks)3Line-by-line explanation
- Lines 3 to 6A linear search counts how many items it inspects.
- Line 6
breakstops as soon as the target is found, so this search makes 3 checks, not 4. Searching for an absent product would check all 4, the worst case.
Counting operations as n grows
- Counting steps instead of timing makes the growth visible without measurement noise.
def count_linear(n):
steps = 0
for _ in range(n):
steps += 1
return steps
def count_quadratic(n):
steps = 0
for i in range(n):
for j in range(i + 1, n):
steps += 1
return steps
def count_binary(n):
steps, low, high = 0, 0, n
while low < high: # search for a value beyond the end
mid = (low + high) // 2
steps += 1
low = mid + 1
return steps
for n in [10, 100, 1000]:
print(f"n={n:<5} linear {count_linear(n):<5} pairs {count_quadratic(n):<7} binary {count_binary(n)}")n=10 linear 10 pairs 45 binary 3
n=100 linear 100 pairs 4950 binary 6
n=1000 linear 1000 pairs 499500 binary 9Line-by-line explanation
- Lines 7 to 12Comparing every pair takes n(n − 1)/2 steps: multiplying n by 10 multiplies the work by about 100.
- Lines 14 to 20Halving the search interval each step needs only about log₂ n steps.
Common Time and Space Complexity Mistakes
Membership tests on a list inside a loop
banned = load_banned_codes() # a list of 50,000 codes
valid = [c for c in donations if c not in banned] # O(d × b)banned = set(load_banned_codes()) # O(b) once
valid = [c for c in donations if c not in banned] # O(d) on averageEach in on a list scans it; a set answers in constant average time.
Treating Big O as measured speed
"Algorithm A is O(n log n), so it will run faster than B, which is O(n²), on our 30-item list.""For 30 items either is fine; measure if it matters. A scales far better as the list grows."Time and Space Complexity in Real Projects
Complexity in real systems
Performance problems in real applications are often accidental quadratic behaviour: a membership test on a list inside a loop, string concatenation in a loop, or a database query issued for every row of another query. They're invisible in testing with 50 records and painful in production with 50,000. Recognising the pattern and switching to a set, a dictionary, "".join() or a single query fixes it.
Python's documentation lists the time complexity of built-in operations. For real measurements, timeit and profilers such as cProfile show where time actually goes; Big O tells you what to expect as data grows.
Time and Space Complexity FAQ
What is Big O notation in simple terms?
It describes how an algorithm's work or memory grows as the input grows, ignoring constant factors. O(n) grows in step with the input; O(n²) grows with its square; O(1) stays the same regardless of size.
What is the time complexity of x in list versus x in set?
x in list is O(n) because Python may compare against every element. x in set and key in dict are O(1) on average because they use hashing to jump straight to the right place.
What is the time complexity of Python's sort?
sorted() and list.sort() use Timsort, which is O(n log n) in the worst case and O(n) when the data is already sorted or nearly so. It's also stable.
What is the difference between time and space complexity?
Time complexity measures how the number of operations grows; space complexity measures how extra memory grows. They often trade off: using a set or cache can save time at the cost of memory.
Is O(1) always faster than O(n)?
Not for every input size. O(1) means the cost doesn't grow, but that constant can be large; for tiny inputs an O(n) scan might be quicker. Big O predicts behaviour as inputs grow large, which is when the difference matters.
Time and Space Complexity Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What is the worst case growth of a full list scan?
Which review concern is valid for time and space complexity?
Time and Space Complexity Quiz
2 questions to check what you learned.
What is the worst case growth of a full list scan?
- O(1)
- O(n)
- Python ignores the distinction
Which statement correctly describes worst and average cases?
- Different inputs can require different amounts of work.
- Only the example output matters, not the rule
- Every operation has the same behavior