Introduction to Big-O
Before we can compare algorithms, we need a language for describing their cost. Big-O notation is that language. It tells us how an algorithm's running time or memory usage scales as the input grows, ignoring machine-specific details like clock speed, cache size, or compiler optimizations.
Why We Need Big-O
Imagine two sorting algorithms. On your machine, Algorithm A sorts 1000 numbers in 2ms and Algorithm B takes 3ms. A is faster, right? Maybe not,
- A might run in $c_1 \cdot n^2$ time (quadratic).
- B might run in $c_2 \cdot n \log n$ time.
For $n = 1000$, A wins because $c_1$ is small. But for $n = 10^6$, A takes $c_1 \cdot 10^{12}$ operations while B takes $c_2 \cdot 2 \times 10^7$. B is now 50,000× faster.
Big-O captures this by stripping away the constants $c_1, c_2$ and focusing on the growth rate: $O(n^2)$ vs $O(n \log n)$. The growth rate tells you which algorithm wins as $n \to \infty$.
The RAM Model
To count "operations," we need a model of computation. The Random Access Machine (RAM) model assumes:
- Each simple operation (addition, subtraction, multiplication, comparison, assignment, memory access) takes 1 step.
- Each memory access (read/write to any array index) takes 1 step regardless of location.
- Loops and function calls are not single steps, their cost is the sum of their iterations/calls.
This is a simplification (real CPUs have caches, pipelines, branch prediction), but it's accurate enough for algorithm analysis. The RAM model lets us count operations and derive growth rates without worrying about hardware.
Counting Operations
Example 1: Linear Search
int linearSearch(vector<int>& arr, int target) {
for (int i = 0; i < arr.size(); i++) { // loop: runs n times
if (arr[i] == target) // 1 comparison
return i; // 1 return
}
return -1;
}
Best case: Target is at index 0. We do 1 comparison. Cost: $1 = O(1)$.
Worst case: Target isn't in the array. We do $n$ comparisons. Cost: $n = O(n)$.
Average case: Target is equally likely at any position. Expected comparisons: $\frac{1}{n}\sum_{i=1}^{n} i = \frac{n+1}{2} = O(n)$.
In all three analyses, the growth rate is the same (well, best case is $O(1)$ but we usually report worst case). The key: the loop runs proportional to n.
Example 2: Nested Loops
void allPairs(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n; i++) { // outer: n iterations
for (int j = 0; j < n; j++) { // inner: n iterations each
process(arr[i], arr[j]); // 1 operation
}
}
}
Total operations: $n \times n = n^2$. This is $O(n^2)$.
Example 3: Dependent Loops
void triangular(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) { // inner runs 0, 1, 2, ..., n-1 times
process(arr[i], arr[j]);
}
}
}
Total operations:
$$\sum_{i=0}^{n-1} i = 0 + 1 + 2 + \cdots + (n-1) = \frac{n(n-1)}{2} = \frac{n^2 - n}{2}$$Drop the lower-order term and constant: $\frac{n^2 - n}{2} = O(n^2)$.
Even though this does roughly half the work of the full $n \times n$ nested loop, both are $O(n^2)$. Big-O ignores constant factors.
The Formal Definition
In English: $f(n)$ is $O(g(n))$ if, beyond some threshold $n_0$, $f(n)$ is at most a constant multiple of $g(n)$.
Proof Example: $3n^2 + 5n + 7 = O(n^2)$
We need to find $c$ and $n_0$ such that $3n^2 + 5n + 7 \leq c \cdot n^2$ for all $n \geq n_0$.
For $n \geq 1$: $5n \leq 5n^2$ and $7 \leq 7n^2$. So:
$$3n^2 + 5n + 7 \leq 3n^2 + 5n^2 + 7n^2 = 15n^2$$Choose $c = 15$ and $n_0 = 1$. ✔
Proof Example: $n^2 \neq O(n)$
Assume for contradiction that $n^2 \leq c \cdot n$ for all $n \geq n_0$. Then $n \leq c$, which fails for $n > c$. Contradiction. ✔
The Drop Rules
These follow directly from the definition and make Big-O practical:
Rule 1: Drop constants
$5n = O(n)$, $100n^2 = O(n^2)$, $0.001 \cdot 2^n = O(2^n)$.
Why: The constant $c$ in the definition absorbs any multiplicative constant.
Rule 2: Drop lower-order terms
$n^2 + n = O(n^2)$, $n^3 + n^2 + n = O(n^3)$, $2^n + n^{100} = O(2^n)$.
Why: For large $n$, the highest-order term dominates. Formally, $n / n^2 \to 0$ as $n \to \infty$, so $n$ is negligible compared to $n^2$.
Rule 3: Sum rule
If you do task A in $O(f(n))$ then task B in $O(g(n))$ sequentially:
$$O(f(n)) + O(g(n)) = O(\max(f(n), g(n)))$$Rule 4: Product rule
If you do task A $O(f(n))$ times, each taking $O(g(n))$:
$$O(f(n)) \times O(g(n)) = O(f(n) \cdot g(n))$$Deriving Complexity from Code
Here's a systematic approach:
- Simple statements (assignments, arithmetic, comparisons): $O(1)$.
- Sequential blocks: Add the costs. Take the max (sum rule).
- Loops: Cost = (number of iterations) × (cost per iteration).
- Nested loops: Multiply the iteration counts (product rule).
- Conditionals: Cost = cost of the more expensive branch (worst case).
- Function calls: Replace the call with the function's cost and simplify.
- Recursion: Write a recurrence relation and solve it (covered in Part 4).
Practice: What's the complexity?
void mystery(int n) {
for (int i = 1; i < n; i *= 2) { // how many iterations?
for (int j = 0; j < n; j++) { // n iterations
doWork(); // O(1)
}
}
}
The outer loop: $i$ takes values $1, 2, 4, 8, \ldots$ until $i \geq n$. That's $\lfloor \log_2 n \rfloor + 1$ iterations. The inner loop runs $n$ times for each outer iteration.
$$T(n) = \log_2 n \times n = O(n \log n)$$Practice: Tricky dependent loop
void tricky(int n) {
for (int i = 1; i < n; i++) {
for (int j = 0; j < n; j += i) { // step size = i
doWork();
}
}
}
For each $i$, the inner loop runs $\lfloor n/i \rfloor$ times. Total:
$$T(n) = \sum_{i=1}^{n-1} \frac{n}{i} = n \sum_{i=1}^{n-1} \frac{1}{i} = n \cdot H_{n-1} \approx n \ln n = O(n \log n)$$Here $H_n$ is the $n$-th harmonic number, which is $\Theta(\ln n)$. This pattern appears in algorithms like the Sieve of Eratosthenes.
Best, Worst, and Average Case
Big-O describes the upper bound on growth, not which input you're analyzing. You can have:
- Worst-case $O(n^2)$: The maximum cost over all possible inputs of size $n$.
- Best-case $O(n)$: The minimum cost.
- Average-case $O(n \log n)$: The expected cost over a probability distribution of inputs.
When someone says "this algorithm is $O(n \log n)$" without qualification, they almost always mean worst-case. If they mean average case, they'll say so explicitly.
| Algorithm | Best | Average | Worst |
|---|---|---|---|
| Linear search | $O(1)$ | $O(n)$ | $O(n)$ |
| Binary search | $O(1)$ | $O(\log n)$ | $O(\log n)$ |
| Insertion sort | $O(n)$ | $O(n^2)$ | $O(n^2)$ |
| Quicksort | $O(n \log n)$ | $O(n \log n)$ | $O(n^2)$ |
| Merge sort | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ |
Common Pitfalls
- "Big-O means worst case", Wrong. Big-O is an upper bound. You can say the best case of linear search is $O(1)$. Big-O, Big-Ω, and Big-Θ are about bounds, not cases. Cases and bounds are orthogonal.
- Confusing $O$ with $\Theta$, $O(n^2)$ means "at most $n^2$ growth." An $O(n)$ algorithm is technically also $O(n^2)$. But when we say "the algorithm is $O(n^2)$," we usually mean it's a tight bound ($\Theta(n^2)$).
- Ignoring hidden constants, $O(n)$ with a constant of $10^9$ is slower than $O(n^2)$ for $n < 10^9$. Big-O is about asymptotic behavior, not small inputs.
- Forgetting that $\log$ base doesn't matter, $\log_2 n = \frac{\log_{10} n}{\log_{10} 2} = \Theta(\log n)$. All logarithm bases differ by a constant factor, so Big-O treats them identically.
Summary
- Big-O describes how an algorithm scales with input size, ignoring constants and small inputs.
- Formally: $f(n) = O(g(n))$ means $f(n) \leq c \cdot g(n)$ for all $n \geq n_0$.
- Use the RAM model to count operations: each simple instruction is 1 step.
- Drop constants and lower-order terms: $3n^2 + 5n + 7 = O(n^2)$.
- Loops → count iterations × cost per iteration. Nested loops → multiply.
- Best/worst/average are about which inputs you analyze; $O$/$\Omega$/$\Theta$ are about bounds.