Algorithm complexity
Count the work code does, and see why some programs stop scaling.
Create a free account Try part of it here
Why it matters
AI work means big inputs: a million customer rows, a hundred thousand documents to search, a context window of a million tokens. Code that feels instant on a test file can freeze on the real thing. Big O is the tool engineers use to see that coming before it happens, by asking one question: when the input doubles, what happens to the work?
What you'll be able to do
- Count how the work a function does grows as its input grows
- Name the common growth rates, from O(1) to O(n²), and spot each in code
- Pick a data structure that turns slow code into fast code
- Explain why attention's cost limits how long a model's context can be
What it covers
- Count steps, not seconds
- The growth rates you'll meet
- The right data structure changes everything
- From quadratic to linear
- Big O in AI systems
- What Big O doesn't tell you
Count steps, not seconds
How long does a recipe take? That depends on who's cooking. How much more work is it for 100 guests than for 10? That's a better question, because the answer holds in any kitchen.
Timing code with a stopwatch tells you about one machine on one day. A faster laptop changes the answer. So engineers count steps instead, and ask how the count grows with the size of the input, called n.
Here's a search through a list that counts its comparisons:
def find(items, target): steps = 0 for item in items: steps += 1 if item == target: return steps return steps for n in [10, 100, 1000, 10000]: items = list(range(n)) print(n, "items ->", find(items, -1), "comparisons (worst case: not found)")
Ten times the items means ten times the comparisons. The work grows in a straight line with n. We write that as O(n), said "order n" or "big O of n".
The growth rates you'll meet
Big O describes how the work grows, not the exact count. It keeps only the biggest term and drops constant factors. Code that does 3n + 7 steps is O(n): when n is a million, the 7 doesn't matter and the 3 doesn't change the shape.
Picture a party. Calling out one announcement is O(1), however many guests there are. Greeting each guest once is O(n). Everyone shaking hands with everyone else is O(n²), and it gets out of hand fast.
| Big O | name | example | steps for n = 1,000 | for n = 1,000,000 |
|---|---|---|---|---|
| O(1) | constant | look up a key in a dict | 1 | 1 |
| O(log n) | logarithmic | binary search in a sorted list | 10 | 20 |
| O(n) | linear | one loop over the items | 1,000 | 1,000,000 |
| O(n log n) | "n log n" | a good sort | 10,000 | 20,000,000 |
| O(n²) | quadratic | compare every item with every other | 1,000,000 | 1,000,000,000,000 |
Look at the last row. A quadratic step that takes a second on 1,000 items takes about twelve days on a million. That's the bug Big O helps you catch: code that works on your sample and dies on the full data.
In plain words: O(1) doesn't grow at all. O(n) doubles when the input doubles. O(n²) quadruples when the input doubles.
How do you tell from the code? Roughly:
- one loop over the input: O(n);
- a loop inside a loop, both over the input: O(n²);
- halving the problem each step: O(log n);
- a fixed number of steps, whatever the input: O(1).
Slide to a million: O(n²) takes about 11 days at that speed, and O(n log n) about 20 seconds.
def all_pairs(names): pairs = [] for a in names: for b in names: if a < b: pairs.append((a, b)) return pairs
Show the answer
O(n²)
A loop over the names inside another loop over the names: n × n checks, which is O(n²). The if doesn't change the count of checks, only how many pairs are kept.
The right data structure changes everything
Finding one letter in an unsorted pile means going through the pile. Finding it in a wall of labeled pigeonholes means walking straight to the right one. Checking x in some_list is the pile: it walks the list item by item, which is O(n). Checking x in some_set is the wall: it jumps straight to the answer, O(1) on average. A set and a dict use a hash table, which the next lesson builds from scratch. For now, feel the difference:
import time n = 20000 as_list = list(range(n)) as_set = set(as_list) lookups = range(n - 500, n) # 500 values near the end of the list start = time.perf_counter() hits = sum(1 for x in lookups if x in as_list) list_time = time.perf_counter() - start start = time.perf_counter() hits = sum(1 for x in lookups if x in as_set) set_time = time.perf_counter() - start print("list:", round(list_time * 1000, 1), "ms") print("set: ", round(set_time * 1000, 2), "ms") print("set is about", round(list_time / set_time), "times faster here")
The exact numbers depend on your machine. The gap is the point, and it widens as n grows.
Halving: where log n comes from
Guess a number between 1 and 1,000,000, and after each guess you're told "higher" or "lower". Always guess the middle and you halve what's left each time. It takes at most 20 guesses, because halving a million 20 times gets you to one. That count is the logarithm base 2 of n, and it grows very slowly: a billion items need only 30 halvings.
n = 1_000_000 halvings = 0 while n > 1: n = n // 2 halvings += 1 print(halvings, "halvings to go from a million to one")
n, steps = 1_000_000, 0 while n > 1: n //= 2 steps += 1 print(steps)
Show the answer
19
Halving a million with // takes 19 steps to reach 1, close to log₂(1,000,000) ≈ 20. That's why binary search finds one item in a million in about 20 comparisons.
From quadratic to linear
Two ways to find whether any email appears twice in a list. Both are correct. Count how many comparisons each makes:
def has_dupes_slow(emails): checks = 0 for i in range(len(emails)): for j in range(i + 1, len(emails)): checks += 1 if emails[i] == emails[j]: return True, checks return False, checks def has_dupes_fast(emails): seen = set() checks = 0 for e in emails: checks += 1 if e in seen: return True, checks seen.add(e) return False, checks emails = [f"user{i}@shop.test" for i in range(2000)] print("slow:", has_dupes_slow(emails)) print("fast:", has_dupes_fast(emails))
The slow version compares every pair: about 2,000 × 2,000 / 2, which is nearly two million checks. The fast version looks at each email once and remembers what it has seen, so it makes 2,000 checks. It does use more memory: the set holds up to n items. Trading memory for time like this is one of the most common moves in programming. Big O for memory is called space complexity, and here it's O(n).
Big O in AI systems
The same thinking explains limits you met earlier in the course:
- Attention compares every token with every other token, the handshakes at the party again, so its cost is O(n²) in the length of the context. Doubling the context roughly quadruples that part of the work. That's why long context costs more, and why researchers work on cheaper attention.
- Similarity search that compares a question with every stored embedding is O(n). That's fine for ten thousand documents and too slow for a billion, which is why vector databases build an index that skips most of them.
- Generating text takes one model step per output token, so it's O(n) in the reply's length. Long answers take longer.
Show the answer
16 times
The context is 4 times longer, and quadratic cost grows with the square: 4 × 4 = 16.
What Big O doesn't tell you
It isn't seconds. Big O describes how the work grows as the input grows, on any machine. A faster laptop changes the seconds and leaves the Big O alone. That's why it's useful: it predicts what happens at ten times the data, before you get there.
Constants get dropped. 3n + 7 and n are both O(n), because at a large size the shape of the curve matters more than the multiplier. So O(n) code can lose to O(n²) code on small inputs, and you shouldn't care. It's the large inputs that hurt.
A quick test on a small sample proves little. A dinner for four tells you nothing about catering a wedding. Quadratic code looks fine on 100 rows. On a million rows it takes about 100 million times as long. This is the classic way software works on a laptop and falls over in production, so try your code on realistic sizes before you trust it.
in isn't one thing. x in my_list checks items one at a time, which is O(n). x in my_set goes almost straight there, which is O(1). Inside a loop, that one word decides whether your program takes seconds or hours.
Practice what you just read
- 4 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
A function does 5n + 100 steps for an input of size n. What is its Big O?