Three Runners, Three Roads
Runner 1 gets the clear highway. No traffic, no red lights, straight tarmac. She arrives in record time — every single time, no matter what. This is your best case: conditions conspire perfectly in your algorithm's favour.
Runner 2 gets normal city streets. Some lights, occasional slow drivers, a roundabout or two. She finishes in a reasonable time — not great, not terrible. This is your average case: what you'd expect on a typical day with typical data.
Runner 3 gets rush-hour gridlock. Every junction is jammed, every light is red, a lorry has shed its load on the main road. He crawls in at the very end — exhausted and hours late. This is your worst case: the data that maximally punishes your algorithm.
When computer scientists talk about algorithm efficiency, they're analysing these three runners. Big-O describes Runner 3's ceiling. Big-Ω describes Runner 1's floor. Big-Θ describes what happens on average, every typical run. Knowing all three tells you everything about whether an algorithm will hold up — and where it will break.
Every time you write a loop, call a sort function, or query a database, you're implicitly choosing a road for your data to travel. The notations below give you a precise, mathematical language to describe that journey — and to compare it with any alternative.
An algorithm that runs in 10 ms on 100 items might take 10 seconds on 10,000 items if it's O(n²) — or still just 13 ms if it's O(n log n). Understanding asymptotic notation is the difference between code that scales and code that silently destroys your servers.
The Three Notations
Each notation answers a different question about your algorithm's performance. Used together they give you a complete picture of how it behaves across all possible inputs.
Use it when: designing systems that must meet SLA guarantees, or choosing between algorithms in a production context.
Use it when: proving that no algorithm in a problem class can do better than a certain bound (theoretical lower bounds).
Use it when: benchmarking real-world performance or analysing algorithms with consistent behaviour across all inputs.
O = "Oh no, the worst" · Ω = "At best, omega fast" · Θ = "THeta = THe exact truth". When you can only remember one, it's almost always Big-O that people want.
Asymptotic Notation — The Formal Theory
Why throw away detail? Because on small inputs everything is fast — a clumsy O(n²) sort beats a clever O(n log n) sort when n is 5, thanks to smaller constants. The interesting question is what happens when n grows large, and there the constant factors and lower-order terms become irrelevant noise. Asymptotic notation is the precise language for describing that horizon — and every definition below is built from the same three ingredients: two functions, constant multipliers, and a threshold n₀ beyond which the relationship is guaranteed to hold.
Every asymptotic bound says the same kind of thing: “for inputs at least as big as some threshold n₀, function f is hemmed in by a constant multiple of function g.” What changes between O, Ω, Θ, o and ω is only which side g hems f in from, and whether the bound is tight or strict.
Θ-Notation — Asymptotically Tight Bound
Θ is the central notation in CLRS. It is a two-sided bound: f(n) is squeezed between two constant multiples of g(n) once n is large enough. Formally, Θ(g(n)) is a set of functions:
0 ≤ c₁·g(n) ≤ f(n) ≤ c₂·g(n) for all n ≥ n₀ }
For every n to the right of n₀, the blue f(n) stays trapped between the green lower bound and the red upper bound — that is exactly what f(n) = Θ(g(n)) means.
O-Notation & Ω-Notation — The One-Sided Bounds
Drop one side of the sandwich and you get the one-sided bounds. O keeps only the ceiling; Ω keeps only the floor.
with 0 ≤ f(n) ≤ c·g(n), ∀ n ≥ n₀ }
with 0 ≤ c·g(n) ≤ f(n), ∀ n ≥ n₀ }
Same threshold idea, half the sandwich: O traps f from above, Ω traps f from below. Beyond n₀ the guarantee holds; before it, anything goes.
For any two functions f(n) and g(n), we have f(n) = Θ(g(n)) if and only if f(n) = O(g(n)) and f(n) = Ω(g(n)). In words: a tight bound is exactly an upper bound and a lower bound holding simultaneously. This is why proving Θ usually means proving O and Ω separately, then stitching them together.
A Worked Proof — ½n² − 3n = Θ(n²)
This is the canonical CLRS example. To prove a Θ bound we must produce actual
constants c₁, c₂ and a threshold n₀ that satisfy the definition. We need:
c₁n² ≤ ½n² − 3n ≤ c₂n² for all n ≥ n₀.
In practice you rarely grind through constants. The lower-order −3n term and the ½ coefficient both vanish asymptotically, leaving n². The same reasoning shows any polynomial adnd + … + a₁n + a₀ with ad > 0 is Θ(nd) — keep the highest-degree term, drop its coefficient.
little-o and little-ω — The Strict (Non-Tight) Bounds
O and Ω allow the bound to be tight (f can equal a constant multiple of g). The lowercase cousins forbid that: they describe bounds that are strictly looser, the way < differs from ≤.
limn→∞ f(n)/g(n) = 0. Example:
2n = o(n²).
limn→∞ f(n)/g(n) = ∞. Example:
n²/2 = ω(n).
The Real-Number Analogy
Cormen draws a memorable parallel: comparing the growth rates of two functions is like comparing two real numbers a and b. This single table makes all five notations click.
| Asymptotic Relation | Meaning | Number Analogy | Tight? |
|---|---|---|---|
f(n) = O(g(n)) | f grows no faster than g | a ≤ b | may be tight |
f(n) = Ω(g(n)) | f grows no slower than g | a ≥ b | may be tight |
f(n) = Θ(g(n)) | f grows at the same rate as g | a = b | always tight |
f(n) = o(g(n)) | f grows strictly slower than g | a < b | never tight |
f(n) = ω(g(n)) | f grows strictly faster than g | a > b | never tight |
Formal Properties of Asymptotic Notation
These properties (CLRS §3.1) let you manipulate bounds algebraically. Note which ones each notation obeys — the gaps are as instructive as the entries.
| Property | Statement | Holds For |
|---|---|---|
| Transitivity | f = O(g) and g = O(h) ⇒ f = O(h) | O, Ω, Θ, o, ω |
| Reflexivity | f(n) = O(f(n)) | O, Ω, Θ only |
| Symmetry | f = Θ(g) ⇔ g = Θ(f) | Θ only |
| Transpose symmetry | f = O(g) ⇔ g = Ω(f); f = o(g) ⇔ g = ω(f) | O↔Ω, o↔ω |
1. The “=” sign is one-directional. Writing f(n) = O(g(n)) really means
f(n) ∈ O(g(n)) — f belongs to the set of functions bounded by g. You can
never flip it: O(g(n)) = f(n) is meaningless.
2. Not every pair is comparable. Unlike real numbers, asymptotic notation has
no trichotomy. Two functions can fail to satisfy any of O, Ω, Θ with
respect to each other — for example n versus n1 + sin n, whose exponent
oscillates forever. Real numbers always satisfy exactly one of <, =, >; growth rates do not.
Growth Curves — Animated Diagram
Watch how each complexity class explodes (or barely grows) as input size n increases from 1 to 20. The vertical axis represents relative time units. Even at small n, the difference between O(n²) and O(n log n) is dramatic.
Values are scaled logarithmically to keep O(n²) visible alongside O(1). Real growth rates are far more extreme.
Common Data Structure Operations
The table below shows how the three complexity cases map to real operations across the most common data structures. Use it as a quick reference when choosing a structure for a specific access pattern.
| Data Structure | Operation | Best Case | Average Case | Worst Case | Space |
|---|---|---|---|---|---|
| Array | Access by index | O(1) | O(1) | O(1) | O(n) |
| Search (unsorted) | O(1) | O(n) | O(n) | — | |
| Insert / Delete (end) | O(1) | O(1) | O(1) | — | |
| Insert / Delete (middle) | O(n) | O(n) | O(n) | — | |
| Linked List | Access by index | O(1) | O(n) | O(n) | O(n) |
| Insert / Delete (head) | O(1) | O(1) | O(1) | — | |
| Search | O(1) | O(n) | O(n) | — | |
| Stack | Push / Pop / Peek | O(1) | O(1) | O(1) | O(n) |
| Search | O(1) | O(n) | O(n) | — | |
| Queue | Enqueue / Dequeue | O(1) | O(1) | O(1) | O(n) |
| Search | O(1) | O(n) | O(n) | — | |
| Hash Table | Insert / Search / Delete | O(1) | O(1) | O(n) | O(n) |
| Binary Search Tree | Insert / Search / Delete | O(1) | O(log n) | O(n) | O(n) |
An unbalanced BST degrades to O(n) when data is inserted in sorted order — it becomes a linked list. Always use a self-balancing variant (AVL tree, Red-Black tree) when input order is unpredictable. Balanced BSTs guarantee O(log n) for all three operations.
Real-World Impact: O(n²) vs O(n log n)
Abstract notation becomes concrete when you count actual steps. Sorting 1,000 items with bubble sort versus merge sort reveals a gap that grows catastrophically with scale.
| Metric | Value |
|---|---|
| Input size (n) | 1,000 items |
| Algorithm | Bubble Sort |
| Complexity | O(n²) |
| Comparisons | ~499,500 |
| Swaps (worst) | ~499,500 |
| Total operations | ~999,000 |
| At n = 10,000 | ~100,000,000 |
| At n = 100,000 | ~10,000,000,000 |
| Practical verdict | Unusable at scale |
| Metric | Value |
|---|---|
| Input size (n) | 1,000 items |
| Algorithm | Merge Sort |
| Complexity | O(n log n) |
| Comparisons | ~9,965 |
| Merge passes | ~10 |
| Total operations | ~9,965 |
| At n = 10,000 | ~132,877 |
| At n = 100,000 | ~1,660,964 |
| Practical verdict | Scales to millions |
Python Code — Annotated Examples
Linear Search — O(n) worst, O(1) best
def linear_search(arr, target):
# Best case: target is first element → O(1)
# Average case: target is in the middle → O(n/2) = O(n)
# Worst case: target is last or absent → O(n)
for i, val in enumerate(arr):
if val == target:
return i # Found — stops early in best/average case
return -1 # Not found — always scans every element
nums = [4, 7, 2, 9, 1, 5, 8, 3, 6]
print(linear_search(nums, 4)) # Best case: found at index 0 → 1 step
print(linear_search(nums, 6)) # Worst case: found at index 8 → 9 steps
print(linear_search(nums, 99)) # Miss: scans all 9 elements → 9 steps
Binary Search — O(log n) worst, O(1) best
def binary_search(arr, target):
# REQUIRES: arr must be sorted
# Best case: target is the middle element → O(1)
# Worst case: target is absent or at the edges → O(log n)
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2 # Integer division — halves the search space
if arr[mid] == target:
return mid # Found
elif arr[mid] < target:
left = mid + 1 # Discard left half
else:
right = mid - 1 # Discard right half
return -1 # Target not found
sorted_nums = [1, 3, 5, 7, 9, 11, 13, 15, 17]
# n = 9 → log₂(9) ≈ 3.17 → at most 4 iterations
print(binary_search(sorted_nums, 9)) # Best: hits middle (index 4) instantly
print(binary_search(sorted_nums, 1)) # Worst: 4 iterations to reach left edge
print(binary_search(sorted_nums, 99)) # Miss: 4 iterations, exhausts range
Bubble Sort — O(n²)
def bubble_sort(arr):
# O(n²) — two nested loops, each up to n iterations
# Best case O(n) with early exit when already sorted
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1): # Each pass bubbles largest to end
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break # Early exit: already sorted
return arr
data = [64, 34, 25, 12, 22, 11, 90]
print(bubble_sort(data)) # ~21 comparisons for n=7
Merge Sort — O(n log n)
def merge_sort(arr):
# O(n log n) — divides in half log n times, merges in O(n) each time
# Θ(n log n): same complexity for ALL inputs — no lucky case
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # Recurse left half
right = merge_sort(arr[mid:]) # Recurse right half
return _merge(left, right)
def _merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
return result + left[i:] + right[j:]
data = [64, 34, 25, 12, 22, 11, 90]
print(merge_sort(data)) # ~17 comparisons for n=7 vs bubble's ~21
The Complexity Ladder
These classes form a hierarchy from constant time bliss to exponential despair. Each rung of the ladder tells you what a million-item input actually means in practice.
At n = 1,000,000: Still exactly 1 operation. Doesn't matter if you have 10 or 10 billion items — the time is identical. This is the holy grail.
At n = 1,000,000: About 20 operations (log₂ 1,000,000 ≈ 20). Feels instant — even if you double your data, you only add one more step.
At n = 1,000,000: One million operations. Perfectly acceptable for a single scan — your bottleneck is usually I/O, not the algorithm.
At n = 1,000,000: About 20 million operations. The sweet spot for sorting — fast enough for most real systems, and provably optimal for comparison-based sorts.
At n = 1,000,000: One trillion operations. At modern CPU speeds (~10⁹ ops/sec), that's ~17 minutes. Completely infeasible for large inputs. Acceptable only for n < 10,000.
At n = 1,000,000: 2^1,000,000 operations — a number with 300,000 digits. Not even theoretically computable. Avoid without memoisation or dynamic programming.
When Each Notation Matters
Knowing the notations is half the job. Knowing when to reach for each is what separates a practitioner from someone who's just memorised definitions.
The Golden Rules
These seven rules cover the most common mistakes in complexity analysis. Internalise them and your Big-O derivations will rarely be wrong.
With Big-O, Big-Ω, and Big-Θ in your toolkit — plus the seven golden rules — you can analyse any algorithm, communicate its performance precisely, and make principled decisions about which data structure or algorithm to reach for in any situation. Practice by analysing every loop you write. It becomes instinctive within weeks.