Big-O Notation
Learn to predict how code slows down as input grows. Count steps, drop the noise, and read Big-O straight from loops, with examples in JS and Python.
On this page
You have a guest list and you want to know if anyone signed up twice. You write two versions of the check. With 100 names, both finish before you can blink. With a million names, one still finishes in a few milliseconds. The other takes several minutes.
Nothing about your laptop changed. The difference is how the amount of work grows when the list grows. Big-O notation is the tool for seeing that difference in the code, before you run it.
Count steps, not seconds
Timing code with a stopwatch tells you about one machine, one input and one moment. Instead, count the basic steps the code does: comparisons, additions, lookups. Then ask how that count changes as the input size, called n, gets bigger.
Here is the slow duplicate check. It compares every name with every name after it. Step through it and watch the comparison counter.
for i from 0 to n - 1: for j from i + 1 to n - 1: if names[i] == names[j]: return truereturn false- names0Anai1Benj2Cat3Ben
- comparisons
- 1
1Fix
ion Ana.jstarts at the next name. Ana vs Ben: different. - names0Anai1Ben2Catj3Ben
- comparisons
- 2
2Ana vs Cat: different.
jkeeps walking. - names0Anai1Ben2Cat3Benj
- comparisons
- 3
3Ana vs Ben: different. Ana has now been checked against everyone after her.
- names0Ana1Beni2Catj3Ben
- comparisons
- 4
4Move
ito Ben. Ben vs Cat: different. - names0Ana1Beni2Cat3Benj
- comparisons
- 5
5Ben vs Ben: a match. We stop after 5 comparisons.
- names0Ana1Ben2Cat3Ben
- n
- 4
- worst case
- 6
- n = 1,000
- 499,500
6If there were no match, 4 names would cost 3 + 2 + 1 = 6 comparisons. For
nnames it is n(n − 1) / 2. For 1,000 names that is 499,500.
The exact count, n(n − 1) / 2, is more detail than you need. What matters is the shape: double the names and the work roughly quadruples. That shape is what Big-O captures.
Keep only what grows fastest
To turn a step count into Big-O, do two things. Drop constant factors, and drop every term except the fastest-growing one.
Why is that allowed? Plug in a big number. With n = 1,000,000, the formula n²/2 − n/2 gives 500,000,000,000 minus 500,000. The second term changes the answer by about one part in a million. And the "divide by 2" does not change how the cost grows: double n and the work still quadruples. So n(n − 1) / 2 becomes O(n²), read "order n squared".
3n + 5becomes O(n).n² + 100nbecomes O(n²).7becomes O(1): the work does not depend onnat all.
The growth rates you will meet
A handful of shapes cover almost every algorithm in this course. The picture shows how far apart they get.
| Big-O | Name | n = 1,000 | n = 1,000,000 | Typical example |
|---|---|---|---|---|
| O(1) | constant | 1 | 1 | read arr[i], hash map lookup |
| O(log n) | logarithmic | 10 | 20 | binary search |
| O(n) | linear | 1,000 | 1,000,000 | one pass over a list |
| O(n log n) | linearithmic | 10,000 | 20,000,000 | good sorting algorithms |
| O(n²) | quadratic | 1,000,000 | 1,000,000,000,000 | comparing every pair |
A computer does very roughly a few hundred million simple steps per second. Read the last row again with that in mind: a million names with an O(n²) algorithm means minutes of work, while O(n) is done in milliseconds.
Reading Big-O from code
You rarely need to write out a formula. Three rules get you most of the way:
- Steps in a row add up, and the biggest wins. A loop over
n, then another loop overn, is 2n, which is O(n). - Nested loops multiply. A loop over
ninside a loop overnis O(n²). - Halving each time is O(log n). If every step throws away half of what is left, you can only do that about log₂(n) times. For a million items, that is 20 steps.
Now compare the two duplicate checks side by side. The second one remembers every name it has seen in a set, so checking "have I seen this before?" is one step instead of a whole loop.
// O(n²) time: compare every pair
function hasDuplicateSlow(names) {
for (let i = 0; i < names.length; i++) {
for (let j = i + 1; j < names.length; j++) {
if (names[i] === names[j]) return true;
}
}
return false;
}
// O(n) time: remember what you have seen
function hasDuplicate(names) {
const seen = new Set();
for (const name of names) {
if (seen.has(name)) return true; // O(1) on average
seen.add(name);
}
return false;
}# O(n²) time: compare every pair
def has_duplicate_slow(names):
for i in range(len(names)):
for j in range(i + 1, len(names)):
if names[i] == names[j]:
return True
return False
# O(n) time: remember what you have seen
def has_duplicate(names):
seen = set()
for name in names:
if name in seen: # O(1) on average
return True
seen.add(name)
return FalseMemory counts too
Big-O works for memory the same way. The fast version stores up to n names in its set, so it uses O(n) extra space. The slow version only keeps two counters: O(1) space.
This is the most common trade in algorithms: spend memory to save time. Usually it is worth it. Sometimes, on a tiny device or a huge dataset, it is not. Knowing both numbers lets you choose on purpose.
| Approach | Time | Space | Why |
|---|---|---|---|
| Compare every pair | O(n²) | O(1) | Nested loops, nothing stored. |
| Set of seen names | O(n) | O(n) | One pass, but the set can hold every name. |
Worst case, unless someone says otherwise
If the duplicate happens to be the first two names, even the slow version stops after one comparison. Big-O usually describes the worst case, because that is the one that brings your app down. When people mean the average case, they say so, as with hash set lookups, which are O(1) on average.
Check yourself
A function loops over a list of n items. Inside the loop, it calls items.includes(x) on the same list. What is its time complexity?
Which of these step counts simplifies to O(n)?
Code A takes 2n steps. Code B takes n² / 100 steps. Which does fewer steps when n = 10?
What to practise next
The faster duplicate check worked because a hash set answers "have I seen this?" in one step. That idea is the whole of Hash Tables. To use it right away, solve Matching Receipts: the obvious solution is O(n²), and a map gets it to O(n).