Big O notation describes how an algorithm's running time or memory use grows as the input gets larger. It ignores constant factors and small inputs, and focuses on the growth rate that dominates for large n.
This guide explains Big O notation with examples: the formal definition, the common complexity classes, rules for analysing loops and recursion, related notations, and worked code examples in Python. It ends with common mistakes and how STEM Donkey can help with complexity analysis.
Big O Notation in Four Sentences
Big O gives an upper bound on growth: f(n) = O(g(n)) means f grows no faster than g, up to a constant factor, once n is large enough. To find it, count the basic operations as a function of n, keep the fastest-growing term and drop its constant.
A single loop over n items is usually O(n), two nested loops over n are O(n²), and halving the problem each step gives O(log n). Unless told otherwise, analyse the worst case.
What Big O Notation Means
Formally, f(n) = O(g(n)) if there exist positive constants c and n₀ such that 0 ≤ f(n) ≤ c·g(n) for every n ≥ n₀. In words, beyond some input size, f is never more than a constant multiple of g.
Worked example: proving a bound. Show that f(n) = 3n² + 5n + 2 is O(n²).
For n ≥ 1, we have 5n ≤ 5n² and 2 ≤ 2n². So 3n² + 5n + 2 ≤ 3n² + 5n² + 2n² = 10n².
Choosing c = 10 and n₀ = 1 satisfies the definition, so f(n) = O(n²).
The "=" in f(n) = O(g(n)) is a convention, not true equality. It reads as "f is in the set of functions that grow no faster than g". That is why 3n is O(n) and also, technically, O(n²): an upper bound does not have to be tight.
Common Complexity Classes
The table lists the classes you will meet most often, from fastest to slowest growth. The last column shows roughly how many basic steps each implies when n = 1,000.
| Big O | Name | Typical example | Steps at n = 1,000 |
|---|---|---|---|
| O(1) | Constant | Indexing an array, dictionary lookup on average | 1 |
| O(log n) | Logarithmic | Binary search | About 10 |
| O(n) | Linear | Scanning a list once | 1,000 |
| O(n log n) | Linearithmic | Merge sort, heapsort | About 10,000 |
| O(n²) | Quadratic | Comparing every pair, bubble sort | 1,000,000 |
| O(n³) | Cubic | Naive matrix multiplication | 1,000,000,000 |
| O(2ⁿ) | Exponential | Checking every subset | About 10³⁰¹ |
| O(n!) | Factorial | Checking every ordering | Far larger still |
The numbers show why growth rate matters more than speed of hardware. A faster computer helps an O(n log n) algorithm on large inputs; it cannot rescue an O(2ⁿ) one.
Rules for Simplifying Big O
- Drop constant factors: 5n is O(n), and n/2 is O(n).
- Keep only the dominant term: n² + 100n + 7 is O(n²).
- Sequential steps add: an O(n) step followed by an O(n²) step is O(n + n²) = O(n²).
- Nested steps multiply: an O(n) loop inside an O(n) loop is O(n²).
- Different inputs keep different letters: looping over a list of size n and then a list of size m is O(n + m), not O(n).
- Log bases do not matter: log₂ n and log₁₀ n differ by a constant factor, so both are O(log n).
Analysing Loops, with Code
For iterative code, count how many times the innermost operation runs in the worst case.
Worked example: quadratic versus linear duplicate check.
def has_duplicate_pairs(items):
n = len(items)
for i in range(n):
for j in range(i + 1, n):
if items[i] == items[j]:
return True
return False
def has_duplicate_set(items):
seen = set()
for x in items:
if x in seen:
return True
seen.add(x)
return False
In the worst case (no duplicates), the first function compares (n − 1) + (n − 2) + ... + 1 = n(n − 1)/2 pairs, which is O(n²). The second makes one pass with average O(1) set operations, so it is O(n) on average, at the cost of O(n) extra memory.
Watch for loops whose counter does not step by one. A loop where i doubles each time (i = 1, 2, 4, 8, ...) runs about log₂ n times, so it is O(log n).
Analysing Recursive Algorithms
For recursive code, write a recurrence relation for the running time T(n), then solve it.
| Algorithm | Recurrence | Solution |
|---|---|---|
| Binary search | T(n) = T(n/2) + O(1) | O(log n) |
| Merge sort | T(n) = 2T(n/2) + O(n) | O(n log n) |
| Linear recursion over a list | T(n) = T(n − 1) + O(1) | O(n) |
| Naive recursive Fibonacci | T(n) = T(n − 1) + T(n − 2) + O(1) | Exponential, O(2ⁿ) as an upper bound |
The master theorem solves recurrences of the form T(n) = aT(n/b) + f(n), where a ≥ 1 and b > 1. It compares f(n) with n^(log_b a). For merge sort, a = 2 and b = 2, so n^(log₂ 2) = n, which matches f(n) = n and gives O(n log n).
A recursion tree is a good check. For merge sort, each level does O(n) work in total and there are about log₂ n levels, which gives the same answer.
Big O, Big Omega and Big Theta
| Notation | Meaning | Example |
|---|---|---|
| O(g(n)) | Upper bound: grows no faster than g | Insertion sort is O(n²) |
| Ω(g(n)) | Lower bound: grows at least as fast as g | Any comparison sort is Ω(n log n) in the worst case |
| Θ(g(n)) | Tight bound: both O and Ω | Merge sort is Θ(n log n) |
In everyday use, people often say "Big O" when they mean a tight bound. In an algorithms assignment, use Θ when you have shown both bounds, and say which case you mean.
Best, Average and Worst Case
One algorithm can have different bounds depending on the input. Always say which case you are analysing.
- Insertion sort: best case O(n) on already sorted input, worst case O(n²) on reverse-sorted input.
- Quicksort: average O(n log n), worst case O(n²) with consistently poor pivots.
- Hash table lookup: average O(1), worst case O(n) when many keys collide.
- Amortised cost: appending to a Python list is O(1) amortised. An occasional resize costs O(n), but spread across many appends the average per append stays constant.
Space Complexity
Big O also describes memory. Count the extra memory an algorithm uses beyond its input.
- Swapping elements in place uses O(1) extra space.
- Building a set of seen items uses O(n) extra space.
- Recursion uses stack space: a recursion depth of n means O(n) space even if each call stores little.
- Merge sort on arrays typically needs O(n) extra space for merging.
Time and space often trade against each other, as in the duplicate check above. A good answer names the trade-off.
Common Big O Mistakes
| Mistake | Fix |
|---|---|
| Calling two separate loops O(n²) | Sequential loops add: O(n) + O(n) = O(n) |
| Ignoring the cost of built-in operations | x in list is O(n); list.insert(0, x) is O(n) |
| Using n for two different inputs | Use separate variables such as n and m |
| Forgetting recursion stack space | Count the maximum recursion depth |
| Not stating the case | Say worst, average or best case explicitly |
| Writing O(2n) or O(n² + n) | Simplify to O(n) and O(n²) |
Complexity analysis rewards patience. Count step by step, one loop at a time, and the answer usually falls out without guesswork.
How STEM Donkey Helps with Big O and Complexity Analysis
Send your algorithms questions, code or pseudocode and your course notes. A writer matched to your level prepares a custom solution that counts operations, writes and solves recurrences, states the case analysed and justifies each bound, with clear explanations.
Use it to understand the method, then analyse similar algorithms yourself. Free revisions within the original scope are included for 14 days.
Want Each Complexity Worked Out Step by Step?
Send your algorithms questions or code. You get a custom solution that counts the operations, justifies each bound and explains the result in plain terms.
Order Your Complexity AnalysisFree revisions within scope for 14 days · Full refund if late · Written from scratch for your order
Frequently Asked Questions
Constant time: the number of steps does not grow with the input size. Indexing an array is O(1).
For large inputs, yes. Doubling n adds only about one step to a logarithmic algorithm but doubles the work of a linear one.
Big O describes growth rate. Constants depend on hardware and implementation details, while the growth rate decides how an algorithm scales.
Big O is an upper bound; Big Theta is a tight bound, meaning the function is bounded above and below by the same growth rate.
Efficient comparison sorts such as merge sort and heapsort are O(n log n). Python's built-in sort is O(n log n) in the worst case.
Multiply the number of iterations of each loop. Two nested loops over n items are O(n²), but if the inner loop's range depends on the outer one, sum the iterations exactly.
No. It measures how running time grows with input size. Two O(n) algorithms can differ a lot in real speed.
The average cost per operation over a long sequence of operations, which smooths out occasional expensive steps such as resizing a dynamic array.