Complexity & Big-O: Measuring How Work Grows
What you'll learn
- Why we judge an algorithm by how its work grows, not by seconds on your laptop
- How to count steps and write the result in Big-O notation
- The common growth classes:
O(1),O(log n),O(n),O(n log n),O(n²),O(2ⁿ) - How to read the complexity straight off a loop, and how to spot hidden costs
- You will feel it: a slow idea that times out on a big input, and the fast idea that passes instantly
The phone book problem 📖
Imagine a paper phone book with 1,000,000 names and you want to find "Omar".
- Method A: start at page one and check every name until you find him. In the worst case that is 1,000,000 checks.
- Method B: open the book in the middle. "Omar" comes after the name you see? Throw away the first half. Repeat on what is left. This needs only about 20 checks.
Now the city doubles to 2,000,000 names. Method A needs up to 2,000,000 checks. Method B needs one more check (21).
That is the whole idea of complexity: not "how fast is it today?" but "what happens to the work when the problem gets bigger?" Computers get faster every year, but a better algorithm beats a faster machine as soon as the input is large enough.
Big-O is the shorthand we use to talk about it. Method A is O(n) (work grows in step with the number of names). Method B is O(log n) (work grows only by one step each time the input doubles).
Big-O in one page
We count steps (a comparison, an addition, an array read) as a function of the input size n. Then we keep only the term that grows fastest and throw away constant multipliers:
3n² + 50n + 7 → O(n²)
Why throw things away? For big n the n² term dwarfs everything else, and the constant (3) depends on the machine and the language, not on the idea. Big-O compares ideas, not computers.
Big-O is an upper bound on growth. Unless we say otherwise we quote the worst case: the item is last, or missing.
The ladder, from best to worst:
- O(1) constant: read
a[5], push onto a stack, look up a key in a hash map (on average) - O(log n) logarithmic: halve the problem each step, like the phone book (binary search)
- O(n) linear: one pass over the data (sum, maximum, linear search)
- O(n log n): efficient sorting (merge sort, the built-in sort)
- O(n²) quadratic: every pair of items, usually two nested loops
- O(2ⁿ) exponential: try every subset of n items. Hopeless beyond n of about 40
Feel the scale with n = 1,000,000:
O(log n)is about 20 stepsO(n)is 1 million stepsO(n log n)is about 20 million stepsO(n²)is 1,000,000,000,000 steps: hours of computing
A rule of thumb: a compiled language does roughly 10⁸ simple operations per second, and Python tens of millions. So you can afford about n = 10⁶ for O(n log n) but only n of a few thousand for O(n²) if you want the answer in a second or two.
Both functions find the last item, but one counts every item and the other halves the search range each time. Multiply the list size by 1,000 and watch which counter grows. Try n = 10_000_000 as well.
def linear_search_steps(a, target):
steps = 0
for x in a:
steps += 1
if x == target:
break
return steps
def binary_search_steps(a, target):
lo, hi, steps = 0, len(a) - 1, 0
while lo <= hi:
steps += 1
mid = (lo + hi) // 2
if a[mid] == target:
break
if a[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return steps
for n in (10, 1_000, 1_000_000):
a = list(range(n)) # already sorted
target = n - 1 # the last item: a worst case for the linear search
print(n, "items ->", linear_search_steps(a, target), "linear steps,", binary_search_steps(a, target), "binary steps")
Reading complexity off the code
You rarely need math. Look at the loops.
One loop over the data is O(n):
def total(a):
s = 0
for x in a: # runs n times
s += x # 1 step each
return s
A loop inside a loop multiplies, O(n²):
def has_pair_sum(a, target):
for i in range(len(a)): # n times
for j in range(i + 1, len(a)): # up to n times
if a[i] + a[j] == target:
return True
return False
The inner loop runs (n-1) + (n-2) + ... + 1 + 0 times, which is n(n-1)/2. Drop the constant and the smaller term: O(n²).
A loop that halves (or doubles) its variable is O(log n):
def halvings(n):
steps = 0
while n > 1:
n //= 2 # 1,000,000 -> 500,000 -> 250,000 -> ... -> 1
steps += 1
return steps
Four rules that cover most code:
- Blocks one after another add, and the biggest wins:
O(n) + O(n²)isO(n²). - Nested blocks multiply: a loop of n running an O(m) step is
O(n·m). - Different inputs get different letters. Looping over list
aand then listbisO(a + b), notO(n). - Check what each line costs. A single line can hide a whole loop.
Hidden costs worth memorising (Python, similar elsewhere):
x in my_listscans the list: O(n). On asetordictit is O(1) on average.my_list.pop(0)andmy_list.insert(0, x)shift every element: O(n).a[1:]copies the list: O(n). Slicing inside a loop quietly makes it quadratic.- Building a string with
s += piecein a loop can cost O(n²) overall because strings are immutable. Collect pieces in a list and"".join(pieces)once.
Lab exercise 1
C++Graded in Code LabCount the steps without running them. Read an integer n (1 ≤ n ≤ 10⁹). Three snippets of pseudo-code each call work() a number of times:
A: for i in 0 .. n-1:
work()
B: for i in 0 .. n-1:
for j in i+1 .. n-1:
work()
C: x = n
while x > 1:
x = x / 2 // integer division
work()
Print three lines: how many times work() is called by A, by B and by C.
For n up to 10⁹ you cannot simulate B (it makes about 5·10¹⁷ calls), so derive a formula. That is exactly what Big-O thinking is for.
Example: n = 10 prints
10
45
3
Use 64-bit integers (long long).
- n = 1: only the first loop runs
- n = 2
- n = 10 (the example)
- n = 1000
- n = 10⁹ cannot be simulated
Example run
- Input
1
- Expected output
1 0 0
#include <iostream>
using namespace std;
int main() {
long long n;
cin >> n;
// TODO: compute how many times work() is called by each snippet.
// Do not simulate snippet B: n can be a billion. Find a formula.
long long a = 0, b = 0, c = 0;
cout << a << "\n" << b << "\n" << c << "\n";More lines in Code Lab: 2
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Trade memory for time
The cheapest way to make a slow algorithm fast is often to remember what you have already seen. Checking "have I seen this number before?" by scanning all previous numbers costs O(n) per question. A hash set answers the same question in O(1) on average, at the price of storing the numbers: O(n) extra space.
This is the most common trade in programming: spend memory to save time. Space complexity is written exactly like time complexity (it counts extra memory as the input grows).
Nested loops : O(n²) time, O(1) extra space
Hash set : O(n) time, O(n) extra space
Run the next demo and compare how much work each version does on the same data.
Multiply n by 10 and the nested loops do about 100 times more work, while the Set version does 10 times more. That gap is the difference between O(n²) and O(n).
function hasDuplicateSlow(a) {
let comparisons = 0;
for (let i = 0; i < a.length; i++) {
for (let j = i + 1; j < a.length; j++) {
comparisons++;
if (a[i] === a[j]) return { found: true, work: comparisons };
}
}
return { found: false, work: comparisons };
}
function hasDuplicateFast(a) {
const seen = new Set();
let lookups = 0;
for (const x of a) {
lookups++;
if (seen.has(x)) return { found: true, work: lookups };
seen.add(x);
}
return { found: false, work: lookups };
}
for (const n of [100, 1000, 10000]) {
const a = Array.from({ length: n }, (_, i) => i); // all different: the worst case
console.log("n =", n, "| nested loops:", hasDuplicateSlow(a).work, "comparisons | Set:", hasDuplicateFast(a).work, "lookups");
}
Lab exercise 2
PythonGraded in Code LabDoes any value appear twice? The input is one line with two integers n m. To keep the test files tiny, the program builds the list itself with a fixed formula:
a[i] = (i * 7919 + 13) mod m for i = 0 .. n-1
(The starter code already builds it.) Print yes if some value occurs at least twice in a, otherwise no.
Example: 5 3 builds the values 1, 0, 2, 1, 0 (five numbers but only three possible values), so the answer is yes.
The starter uses two nested loops. It is correct, but one test has n large enough that O(n²) cannot finish in the runner's time limit. Replace it with an O(n) idea. Note: when n ≤ m all the values are different (m is never a multiple of 7919).
- Empty list
- Five numbers, three possible values
- Exactly as many numbers as values: all different
- One more number than values: a repeat is forced
- 100,000 different numbers (O(n²) times out)
Example run
- Input
0 7
- Expected output
no
n, m = map(int, input().split())
a = [(i * 7919 + 13) % m for i in range(n)]
def has_duplicate(a):
# TODO: this brute force is correct but O(n^2): far too slow when n is 100000.
# Replace it with an O(n) approach that remembers what it has already seen.
for i in range(len(a)):
for j in range(i + 1, len(a)):
if a[i] == a[j]:
return True
return FalseMore lines in Code Lab: 3
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Best, average, worst, and when Big-O misleads
- Worst case is what Big-O usually quotes: linear search is O(n) because the item might be last. Its best case (item is first) is O(1).
- Average case assumes a typical input. A hash map lookup is O(1) on average, but a pathological set of keys that all collide could degrade it. Production languages defend against this.
- Amortised cost averages over a whole sequence of operations. Appending to a Python list is O(1) amortised: now and then it has to copy everything into a bigger block, but spread over many appends the cost per append stays constant.
Big-O is not the whole story:
- It hides constants. An algorithm taking
100nsteps beats one takingn²steps only once n is above 100. For tiny inputs the "worse" algorithm can win. - Two O(n) solutions can differ by a factor of 50 in real time because of memory access patterns.
- Measure before optimising. But when the input is large, growth rate beats everything else.
A solid habit for any problem: (1) write the simplest correct solution, (2) estimate its complexity, (3) compare with the input limits, (4) only then look for a better idea.
Lab exercise 3
JavaGraded in Code LabBest streak. A shop records its profit (positive) or loss (negative) each day. Find the largest total over any run of consecutive days (at least one day).
Input: the first line has n (1 ≤ n ≤ 10000), the next line has n integers, each between -1000 and 1000.
Output: one integer, the best total.
Example: for 9 days -2 1 -3 4 -1 2 1 -5 4 the answer is 6 (the run 4 -1 2 1).
Trying every start and end day is O(n²). Aim for one pass, O(n): while walking, keep the best run that ends today. Careful: if every day is a loss the answer is the least bad single day, not 0.
- The example
- Every day is a loss
- A single day
- All profits: take everything
- A big loss in the middle is worth skipping
- 5,000 days
Example run
- Input
9 -2 1 -3 4 -1 2 1 -5 4
- Expected output
6
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
long[] a = new long[n];
for (int i = 0; i < n; i++) a[i] = in.nextLong();
// TODO: print the largest sum of any non-empty run of consecutive days.
// Idea: walk once, tracking the best run that ENDS at the current day.
long best = 0;More lines in Code Lab: 3
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Question 1 of 5
A loop over n items contains another loop over the same n items, and the body is a single addition. What is the time complexity?
Question 2 of 5
Roughly how many halving steps does binary search need for 1,000,000 sorted items?
Question 3 of 5
Which one is NOT O(1) on average in Python?
Question 4 of 5
Algorithm A takes 100·n steps. Algorithm B takes n² steps. Which statement is true?
Question 5 of 5
Simplify 3n² + 50n + 7 into Big-O form.
Summary
- Complexity measures how the work grows with the input size, so it compares ideas independently of hardware.
- Keep the fastest-growing term and drop constants:
3n² + 50n + 7isO(n²). - The ladder:
O(1)<O(log n)<O(n)<O(n log n)<O(n²)<O(2ⁿ). - Read it off the code: sequential blocks add, nested blocks multiply, halving loops are logarithmic, and watch for hidden O(n) lines such as
x in list, slicing andpop(0). - A hash set trades O(n) memory for O(1) lookups and often turns O(n²) into O(n).
- Always compare your complexity with the input limits before you code.
Practice next: take any loop you wrote recently and write its complexity in a comment. Then continue with Arrays, Strings & Hash Maps, where these ideas become everyday tools.
Practice
Reading is not learning until you run something. Pick one.
Graded lab exercises
- Lab exercise 1C++Not passed yet Try in Code Lab
- Lab exercise 2PythonNot passed yet Try in Code Lab
- Lab exercise 3JavaNot passed yet Try in Code Lab