Big-O: how the work grows
In short: Big-O describes how the work of an algorithm grows as the data grows, ignoring constants: O(1), O(log n), O(n), O(n log n), O(n²) and O(2ⁿ).
Problem
Will it hold up with a million?
The shop signed a contract with a big supplier. In a month the catalogue grows from 1,000 to 1,000,000 products. There are six functions in the code that every product or every payment goes through:
- the first price in the list (for the banner on the home page);
- searching for a price in the sorted price list;
- the sum of the prices (for the report);
- sorting the prices (for the "cheapest first" page);
- the double payment check from the previous lesson;
- all combinations of products that fit in the customer's budget.
Which ones will hold up? There is no data with a million products yet, and the seconds on your laptop lie anyway. You have to tell from the code. Counting every step exactly, as with n(n − 1) / 2, is slow. And the details do not matter: what matters is how the work grows.
The idea
Big-O: what doubling does
Big-O is a short name for how the number of operations grows when n (the amount of data) grows. It does not say seconds, it says a shape: what happens to the work when the data doubles.
| Class | Name | Example from the shop | n doubles → the work |
|---|---|---|---|
| O(1) | constant | the first price | does not change |
| O(log n) | logarithmic | searching the sorted price list | one step more |
| O(n) | linear | the sum of the prices | ×2 |
| O(n log n) | "n log n" | sorting | a bit over ×2 |
| O(n²) | quadratic | double payments | ×4 |
| O(2ⁿ) | exponential | all combinations within budget | squared |
Here are the six functions. The prices are in euro cents, so they are whole numbers:
def first_price(prices): return prices[0]def total(prices): result = 0 for p in prices: result += p return resultdef double_payments(ids): found = 0 for i in range(len(ids)): for j in range(i + 1, len(ids)): if ids[i] == ids[j]: found += 1 return foundfirst_price makes one step, however many prices there are. total makes one per price. double_payments makes one per pair.
See it
O(log n): how many times you halve
Searching the sorted price list looks at the middle. If the price you want is bigger, the left half is not needed at all and is thrown away. Otherwise the right half goes. So after every check half is left:
def index_of(ordered, price): lo = 0 hi = len(ordered) - 1 while lo <= hi: mid = (lo + hi) // 2 if ordered[mid] == price: return mid elif ordered[mid] < price: lo = mid + 1 else: hi = mid - 1 return -1How many checks if the price is not there? As many times as you can halve n before nothing is left. That is the logarithm: log₂ n is "how many times you halve".
for n in [1000, 100000, 1000000, 1000000000]: halvings = 0 left = n while left > 0: left //= 2 halvings += 1 print(n, "→", halvings)1000 → 10100000 → 171000000 → 201000000000 → 30A thousand times more prices (from 1,000 to 1,000,000) is only 10 checks more. Doubling adds one. That is why O(log n) barely grows.
More about the boundaries of binary search (and where it goes wrong by one) comes in the lesson Binary search.
See it
O(n log n) and O(2ⁿ)
Sorting uses the built-in sorted:
def sorted_prices(prices): return sorted(prices)Good sorting algorithms split the array into halves (log n levels) and on every level go through everything once (n steps). That is why they are O(n log n). How exactly, you will see in the lesson on merge sort.
The combinations within budget: for every product there are two choices, "take it" or "leave it". That is why combos calls itself twice:
def combos(prices, i, total, budget): if i == len(prices): return 1 if total <= budget else 0 return (combos(prices, i + 1, total, budget) + combos(prices, i + 1, total + prices[i], budget))With 1 product there are 2 combinations, with 2 there are 4, with 3 there are 8. Every new product doubles everything there was so far. That is O(2ⁿ).
All six, run on five prices and five payments:
prices = [1299, 450, 3999, 899, 2450]sorted_list = sorted_prices(prices)payments = [100231, 100232, 100233, 100232, 100234]print("First price: " + str(first_price(prices)))print("2450 is at index " + str(index_of(sorted_list, 2450)))print("Total: " + str(total(prices)))print("Cheapest: " + str(sorted_list[0]) + ", most expensive: " + str(sorted_list[4]))print("Double payments: " + str(double_payments(payments)))print("Combinations up to 5000: " + str(combos(prices, 0, 0, 5000)))First price: 12992450 is at index 3Total: 9097Cheapest: 450, most expensive: 3999Double payments: 1Combinations up to 5000: 18See it
What doubling does
def combos(prices, i, total, budget): if i == len(prices): return 1 if total <= budget else 0 return (combos(prices, i + 1, total, budget) + combos(prices, i + 1, total + prices[i], budget))def first_price(prices): return prices[0]def index_of(ordered, price): lo = 0 hi = len(ordered) - 1 while lo <= hi: mid = (lo + hi) // 2 if ordered[mid] == price: return mid elif ordered[mid] < price: lo = mid + 1 else: hi = mid - 1 return -1def total(prices): result = 0 for p in prices: result += p return resultdef sorted_prices(prices): return sorted(prices)def double_payments(ids): found = 0 for i in range(len(ids)): for j in range(i + 1, len(ids)): if ids[i] == ids[j]: found += 1 return founddef combos(prices, i, total, budget): if i == len(prices): return 1 if total <= budget else 0 return (combos(prices, i + 1, total, budget) + combos(prices, i + 1, total + prices[i], budget))Step by step, in words
- The first price: one step with 4 prices and with 8. The work does not depend on n: O(1).
- Binary search looks at the middle and throws away half. 4 → 2 → 1 is 3 checks, 8 → 4 → 2 → 1 is 4. Twice the prices, one check more: O(log n).
- The sum visits every price once: 4 and 8 steps. Twice the prices, twice the work: O(n).
- Sorting merges level by level (here merge sort, as in Bench). There are log n levels, with at most n comparisons on each: 5 → 16. O(n log n).
- Double payments: every one with every one after it. 6 → 28: the triangle grows in two directions. O(n²).
- Combinations within budget: every product is "take it" or "leave it", and every new one doubles them. 16 → 256: with 8 products the work is the work for 4, squared. O(2ⁿ).
The six classes with 4 and with 8 prices. Every cube is one step, exactly as many as Bench counts. O(1) does not move. O(log n) adds one bar. O(n) doubles. O(n log n) doubles and gets one more level. O(n²) grows in two directions. O(2ⁿ) turns from a 4 × 4 square into a 16 × 16 square.
The idea
Constants and small terms drop out
The double payment report makes n(n − 1) / 2 = n²/2 − n/2 steps. Why is it O(n²) and not O(n²/2 − n/2)?
The smaller term disappears as n grows:
| n | n²/2 | n/2 | n/2 as a share of the total |
|---|---|---|---|
| 10 | 50 | 5 | 10% |
| 1,000 | 500,000 | 500 | 0.1% |
| 100,000 | 5,000,000,000 | 50,000 | 0.001% |
The constant does not change the shape. n²/2 and n² grow the same way: twice the data, four times the work. A constant is like a slower machine: it changes the seconds, but not what happens when the data doubles. And you already know that seconds lie.
So Big-O keeps only the fastest growing term, without the number in front of it: 3n + 5 is O(n), n²/2 − n/2 is O(n²), 2n log n + 7n is O(n log n).
More precisely, if you are curious
The exact definition: f(n) is O(g(n)) if there is a constant c and a threshold n₀ such that f(n) ≤ c · g(n) for every n ≥ n₀. That is, from some n on, f does not grow faster than g, up to a constant. So O(n) is also O(n²) (it is an upper bound), but we say the tightest one. There is also Θ (exactly that much) and Ω (at least that much). In conversations and interviews "O(n)" usually means Θ(n).
The base of the logarithm is not written either: log₂ n and log₁₀ n differ by a constant (log₂ n ≈ 3.32 · log₁₀ n).
See it
The six curves in the browser
Run the six functions in Bench. The axis is logarithmic: every mark is ×10. O(n) is a straight line, O(n²) is twice as steep, and O(2ⁿ) takes off right away.
- The first price
O(1) - Binary search
O(log n) - The sum of the prices
O(n) - Sorting (merge sort)
O(n log n) - Every pair of orders
O(n²) - Every combination within budget
O(2ⁿ)
| Algorithm | n = 10 | n = 1,000 | n = 100,000 |
|---|---|---|---|
| The first price | 1 | 1 | 1 |
| Binary search | 4 | 10 | 17 |
| The sum of the prices | 10 | 1,000 | 100,000 |
| Sorting (merge sort) | 21 | 8,709 | 1,536,371 |
| Every pair of orders | 45 | 499,500 | ≈ 4,995,000,000 |
| Every combination within budget | 1,024 | ≈ 10³⁰¹ | ≈ 10³⁰¹⁰² |
The answer for the million products: the first price, the search, the sum and the sorting will hold up. The double payments become 500 billion steps and have to move to the set from the previous lesson. And all combinations of a million products cannot be listed on any computer: 2 to the power of a million is a number with 301,030 digits.
More precisely, if you are curious
What the built-in sort uses in each language. sorted and list.sort are TimSort (since Python 3.11 with an improved merge policy, Powersort). O(n log n), and on almost sorted data close to O(n). Bench counts the comparisons of merge sort, because the built-in one cannot be counted from the outside. The class is the same.
Try it
Predict the output
The outer loop runs n = 8 times. The inner one halves until nothing is left. What does the program print?
n = 8steps = 0for i in range(n): left = n while left > 0: left //= 2 steps += 1print(steps)The inner loop goes 8, 4, 2, 1: 4 times, that is log₂ 8 + 1. The outer one runs it 8 times: 8 × 4 = 32. n times log n: exactly the shape of O(n log n).
Try it
Find the hidden loop
The function counts the different customers in the orders of the month. It has only one loop, and yet with 100,000 orders it is as slow as the double payment report:
def unique_customers(orders): seen = [] for customer in orders: if customer not in seen: seen.append(customer) return len(seen)orders = [7, 3, 7, 12, 3, 7]print(unique_customers(orders))3On which line is the second loop hiding?
Pick the line with the bug.
customer not in seen walks the list element by element until it finds the customer. That is a loop inside the loop: with many different customers the work is O(n²). One call of a operator is not one step. The fix is a set instead of a list, as in the previous lesson: then the check is one step and everything is O(n).
Exercise
How many combinations
Solve the task in your editor.
The steps of the combinations. Add a counter steps to combos: one step for every finished combination (when i reaches the end). Run it with 10, 11 and 12 products with prices of 500 + 100 · k cents and a budget of 5000, and print the steps:
10 products: 1024 steps11 products: 2048 steps12 products: 4096 stepsEvery product doubles the steps. How many are there with 20 products? And with 40? Work it out before you run it.
More tasks
- The class of every function. Write the Big-O of every function from the lesson "Time complexity: counting steps", including the sales by product (50 products × n orders).
- Binary search with a counter. Add a counter to
index_ofand search for a price that is not there in 1,000 and in 100,000 sorted prices. Compare with the program that halves. - A set again. Fix the counting of different customers with a set and compare the steps with 1,000 different customers.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: What all objects share: class attributes · Lists: append, remove and sort · dict and set · Time complexity: counting steps
- Continues in: Best, average and worst case · Space complexity · Amortized complexity · Linear search
- Tasks on this topic: Complexity