Big-O Notation
Reading time: 25 minutes
Big-O notation is the most widely used tool for describing algorithmic complexity. It provides an upper bound on the growth rate of an algorithm, answering the question: "In the worst case, how does this algorithm's resource usage scale with input size?"
When engineers discuss algorithm performance, they almost always use Big-O. Understanding it is essential for technical interviews, code reviews, and system design discussions. This page covers Big-O in depth, from the formal definition to practical application.
What Big-O means
Big-O notation describes an upper bound on an algorithm's growth rate. When we say an algorithm is O(n), we mean:
The algorithm's running time (or space usage) grows at most linearly with input size.
For sufficiently large inputs, the actual cost is bounded above by some constant multiple of n.
The algorithm might be faster than O(n) in some cases, but it will never be slower.
The "O" stands for "order of" and is sometimes read as "on the order of." O(n²) means "on the order of n squared."
Upper bound, not exact bound
A crucial point: Big-O gives an upper bound, not an exact bound. If an algorithm is O(n²):
It might actually be Θ(n²) — exactly quadratic
It might be Θ(n) — linear, which is also bounded by n²
It might even be O(1) — constant, which is also bounded by n²
When we say "this algorithm is O(n²)," we are stating a guarantee: it will not be worse than quadratic. We are not necessarily saying it is exactly quadratic.
In practice, engineers try to give the tightest upper bound they can prove. Saying binary search is O(n) is technically true but unhelpful; saying it is O(log n) is more informative.
The formal definition
For the mathematically inclined, here is the precise definition:
f(n) = O(g(n)) if and only if there exist positive constants c and n₀ such that:
f(n) ≤ c · g(n) for all n ≥ n₀
In plain language: f(n) is O(g(n)) if, past some starting point n₀, f(n) is always bounded above by a constant multiple of g(n).
Understanding the constants
The constant c accounts for implementation differences. One implementation might do 2n operations while another does 10n operations. Both are O(n) because they differ only by a constant factor.
The threshold n₀ accounts for small-input behavior. For small n, lower-order terms or constants might dominate. The definition only cares about large-n behavior.
Proving Big-O bounds
To prove f(n) = O(g(n)), find specific values of c and n₀ that satisfy the inequality.
Example 1: Prove 3n + 100 = O(n)
We need: 3n + 100 ≤ c·n for all n ≥ n₀
Choose c = 4 and n₀ = 100:
For n ≥ 100: 3n + 100 ≤ 3n + n = 4n ✓
(We used the fact that 100 ≤ n when n ≥ 100)
Therefore, 3n + 100 = O(n).
Example 2: Prove n² + 5n + 1000 = O(n²)
We need: n² + 5n + 1000 ≤ c·n² for all n ≥ n₀
Choose c = 7 and n₀ = 15:
For n ≥ 15: 5n ≤ n² (since 5 ≤ n)
For n ≥ 15: 1000 ≤ 5n² (since 1000/5 = 200 ≤ n² = 225)
So n² + 5n + 1000 ≤ n² + n² + 5n² = 7n² ✓
Therefore, n² + 5n + 1000 = O(n²).
Example 3: Prove 2ⁿ is NOT O(n²)
Suppose 2ⁿ = O(n²). Then there exist c and n₀ such that 2ⁿ ≤ c·n² for all n ≥ n₀.
But 2ⁿ grows faster than any polynomial. For any fixed c, there exists some n where 2ⁿ > c·n².
Contradiction. Therefore, 2ⁿ ≠ O(n²).
Why we drop constants and lower-order terms
Big-O focuses on growth rate, not exact values. This is why we simplify:
O(2n) = O(n): The factor of 2 is a constant.
O(n + 100) = O(n): The 100 is a lower-order term.
O(n² + n) = O(n²): The n term grows slower than n².
O(5n³ + 2n² + 7n + 1000) = O(n³): Only the dominant term matters.
Why this simplification is valid
Constants depend on hardware and implementation. The same algorithm runs at different speeds on different machines. A fast computer might have c = 1 while a slow one has c = 100. Both execute the same algorithm with the same growth rate.
Lower-order terms become negligible. When n is large, the dominant term overwhelms everything else:
At n = 1,000: n² = 1,000,000 while n = 1,000. The ratio is 1000:1.
At n = 1,000,000: n² = 10¹² while n = 10⁶. The ratio is 1,000,000:1.
We care about scalability. Big-O tells us how an algorithm behaves as data grows. The question "will this work with 10x more data?" is more important than "exactly how many milliseconds will it take?"
When constants DO matter
While Big-O ignores constants, real performance does not:
For small inputs: An O(n²) algorithm with small constants might beat an O(n log n) algorithm with large constants for inputs under a few hundred elements.
Within the same complexity class: When comparing two O(n) algorithms, constants decide which is faster.
In tight performance requirements: When you need sub-millisecond response times, constant factors matter significantly.
Use Big-O for strategic decisions (which algorithm?). Use benchmarking for tactical decisions (which implementation?).
Common Big-O complexity classes
Here are the complexity classes you will encounter most often, from fastest to slowest:
Big-O | Name | Example Operations | Practical Limit |
|---|---|---|---|
O(1) | Constant | Array access, hash lookup | Unlimited |
O(log n) | Logarithmic | Binary search | Billions |
O(n) | Linear | Linear search, single loop | Millions |
O(n log n) | Linearithmic | Efficient sorting | Millions |
O(n²) | Quadratic | Nested loops, simple sorting | Thousands |
O(n³) | Cubic | Triple nested loops | Hundreds |
O(2ⁿ) | Exponential | Recursive subsets | ~25 |
O(n!) | Factorial | Generating permutations | ~12 |
The "practical limit" column shows roughly how large n can be before the algorithm becomes impractically slow (assuming ~10⁸ operations per second and a few seconds of acceptable wait time).
Rules for calculating Big-O
Rule 1: Drop constants
Two sequential O(n) loops make O(2n), which simplifies to O(n).
Rule 2: Drop lower-order terms
O(n²) + O(n) = O(n² + n) = O(n²).
Rule 3: Different inputs use different variables
When inputs are independent, keep their sizes separate. Only use O(n²) when both dimensions come from the same input.
Rule 4: Nested operations multiply
If both dimensions come from the same input (like a square matrix where rows = cols = n), this becomes O(n²).
Rule 5: Sequential operations add
The dominant term O(n log n) wins.
Rule 6: Conditional branches take the worst case
Big-O describes worst case. Even if the O(n²) branch rarely executes, the function is O(n²).
Big-O for common operations
Knowing the Big-O of standard operations helps you analyze code quickly:
Array operations
Operation | Time Complexity | Notes |
|---|---|---|
| O(1) | Direct memory access |
| O(1) amortized | Occasionally O(n) for resize |
| O(1) | Remove from end |
| O(n) | Must shift all elements |
| O(n) | Must shift all elements |
| O(n) | May need to shift elements |
| O(n) | Linear search |
| O(n) | Linear search |
| O(n) | Scans until found |
| O(n) | Scans all elements |
| O(n) | Transforms all elements |
| O(n) | Copies elements |
| O(n + m) | Creates new array |
| O(n log n) | Comparison-based sort |
Object and Map operations
Operation | Time Complexity | Notes |
|---|---|---|
| O(1) average | Hash table lookup |
| O(1) average | Hash table insert |
| O(1) average | Hash table delete |
| O(1) average | Hash table lookup |
| O(n) | Iterates all keys |
| O(n) | Iterates all values |
| O(n) | Iterates all entries |
| O(1) average | Hash table lookup |
| O(1) average | Hash table insert |
| O(1) average | Hash table lookup |
Set operations
Operation | Time Complexity | Notes |
|---|---|---|
| O(1) average | Hash table insert |
| O(1) average | Hash table lookup |
| O(1) average | Hash table delete |
String operations
Operation | Time Complexity | Notes |
|---|---|---|
| O(1) | Direct access |
| O(1) | Stored property |
| O(n + m) | Creates new string |
| O(j - i) | Creates substring |
| O(n × m) | Worst case for naive search |
| O(n × m) | Worst case |
| O(n) | Scans entire string |
Analyzing code examples
Example 1: Simple linear search
The loop runs at most n times, with O(1) work per iteration. Total: O(n).
Example 2: Finding duplicates with nested loops
The nested loops compare n(n-1)/2 pairs = O(n²).
Example 3: Finding duplicates with a hash set
One loop, O(1) operations per iteration. Total: O(n).
Example 4: Recursive Fibonacci (exponential)
Each call spawns two more calls, creating a binary tree with 2ⁿ nodes.
Example 5: Memoized Fibonacci (linear)
Each of the n values is computed exactly once. Total: O(n).
Example 6: Binary search
Each iteration eliminates half the remaining elements. After log₂(n) iterations, only one element remains.
Example 7: Merge sort
The recursion has depth log n. At each level, the total work across all calls is O(n). Total: O(n log n).
Big-O vs. actual performance
Big-O describes asymptotic behavior, not actual speed. Keep in mind:
Constants matter for practical sizes
An O(n) algorithm with a constant factor of 1000 is slower than an O(n log n) algorithm with a constant factor of 1 for inputs smaller than about 10,000.
For small inputs, the "slower" algorithm might be faster in practice.
Cache behavior matters
Modern CPUs access memory hierarchically. Cache hits are 100x faster than cache misses.
An O(n) algorithm that accesses memory sequentially can vastly outperform an O(n) algorithm that accesses memory randomly, because the sequential version benefits from cache prefetching.
Big-O describes worst case (usually)
An algorithm that is O(n²) in the worst case might typically run in O(n) time for real inputs:
Quick sort is O(n²) worst case but O(n log n) average case
Hash table lookup is O(n) worst case but O(1) average case
Know when average case is more relevant than worst case.
Language and runtime overhead
The same algorithm has different constant factors in different languages:
JavaScript objects have more overhead than C structs
Python loops are slower than C loops
JIT compilation can change performance characteristics
Big-O is language-agnostic; real performance is not.
Common mistakes
Assuming O(1) means "fast"
O(1) means constant time—it does not get slower as input grows. But the constant might be large. An O(1) operation that takes 100ms is still O(1), just slow.
O(1) guarantees scalability, not speed.
Ignoring hidden loops
Many built-in methods contain hidden loops:
This looks like a simple loop, but includes and indexOf are each O(n). If there are k occurrences of target, this is O(k × n) = O(n²) worst case.
Ignoring string concatenation in loops
String concatenation creates a new string. In a loop, this can be O(1 + 2 + 3 + ... + n) = O(n²).
Confusing best and worst case
When asked for Big-O without qualification, give the worst case:
Linear search is O(n), not O(1) (even though best case is O(1))
Quick sort is O(n²), not O(n log n) (unless you specify average case)
Forgetting recursion stack space
Recursive calls consume stack space:
Even though the function allocates no data structures, the call stack grows to depth n.
Treating all loops as O(n)
Not all loops are O(n):
Analyze how the loop variable changes to determine iteration count.
Big-O in interviews
Technical interviews frequently test Big-O analysis. Common patterns:
"What is the time complexity?"
Give the worst-case Big-O. Be prepared to explain your reasoning.
"Can you do better?"
If you gave an O(n²) solution, the interviewer might be looking for O(n log n) or O(n). Consider:
Can sorting help?
Can a hash table reduce lookup time?
Can two pointers eliminate a nested loop?
"What about space complexity?"
Do not forget to analyze space. Trade-offs are common:
O(n) space to achieve O(n) time (hash table)
O(1) space at the cost of O(n²) time (no auxiliary storage)
"What are the trade-offs?"
Be ready to discuss:
Time vs. space
Worst case vs. average case
Simplicity vs. optimality
Summary
Big-O notation describes an upper bound on algorithmic growth rate. It is the primary tool for discussing algorithm complexity.
Key takeaways:
Big-O gives an upper bound: O(n²) means "no worse than quadratic."
Formal definition: f(n) = O(g(n)) if f(n) ≤ c·g(n) for large n.
Drop constants: O(2n) = O(n). O(100n) = O(n).
Drop lower-order terms: O(n² + n) = O(n²).
Nested loops multiply: Two nested loops are O(n²).
Sequential operations add: O(n) + O(n log n) = O(n log n).
Use different variables for independent inputs: O(a × b), not O(n²).
Watch for hidden loops: Built-in methods often have O(n) complexity.
Big-O describes scalability, not speed: Use benchmarking for actual performance.
In interviews: Give worst case, analyze space too, discuss trade-offs.
Big-O is a thinking tool. It helps you reason about algorithms without getting lost in implementation details. Master it, and you will make better design decisions throughout your engineering career.