Asymptotic Notations (Ω, Θ, O)
When we analyze algorithms, we want to describe how their running time (or space usage) grows as the input size grows — without getting bogged down in exact machine-level constants. Asymptotic notation gives us a precise mathematical language to do exactly that.
The Three Core Notations
There are three notations you will encounter in every algorithms textbook and interview:
Notation | Name | Meaning | Analogy |
|---|---|---|---|
O(f(n)) | Big O | Upper bound — at most this fast | Speed limit on a highway |
Ω(f(n)) | Big Omega | Lower bound — at least this slow | Minimum time a flight takes |
Θ(f(n)) | Big Theta | Tight bound — exactly this order | Rush-hour commute: always 30-40 min |
Big O — Upper Bound
Big O is the most widely used notation. We say T(n) = O(f(n)) if there exist positive constants c and n₀ such that T(n) ≤ c · f(n) for all n ≥ n₀. In plain English: beyond some input size, T(n) never exceeds a constant multiple of f(n). Big O describes the worst case ceiling.
Example — Linear Search: In the worst case you scan all n elements. T(n) = n, so T(n) = O(n). We could also say T(n) = O(n²) (n is certainly no worse than n²), but that is a loose bound and not useful.
def linear_search(arr, target):
for i in range(len(arr)): # runs at most n times
if arr[i] == target:
return i # could return early (best case O(1))
return -1
# Worst case: target not found → O(n)
# Best case: target is arr[0] → O(1)
# Both are valid Big O statements about DIFFERENT scenariosCommon Big O Classes (fastest to slowest)
Notation | Name | n = 100 ops | Example |
|---|---|---|---|
O(1) | Constant | 1 | Array index access, hash map lookup |
O(log n) | Logarithmic | ~7 | Binary search, balanced BST lookup |
O(n) | Linear | 100 | Linear scan, single loop |
O(n log n) | Linearithmic | ~700 | Merge sort, heap sort |
O(n²) | Quadratic | 10,000 | Bubble sort, nested loops |
O(n³) | Cubic | 1,000,000 | Naive matrix multiplication |
O(2ⁿ) | Exponential | ~1.27 × 10^30 | Brute-force subsets |
O(n!) | Factorial | astronomical | Brute-force permutations |
Big Omega (Ω) — Lower Bound
Big Omega gives a lower bound. We say T(n) = Ω(f(n)) if there exist positive constants c and n₀ such that T(n) ≥ c · f(n) for all n ≥ n₀. This means T(n) grows at least as fast as f(n). It is the floor, not the ceiling.
Example — Comparison-based sorting: Any algorithm that sorts by comparisons must make at least Ω(n log n) comparisons in the worst case. No comparison sort can beat this lower bound — it is a fundamental limit, not a limitation of any specific algorithm.
def find_min(arr):
min_val = arr[0]
for x in arr: # must look at every element
if x < min_val:
min_val = x
return min_val
# T(n) = Ω(n) — you MUST examine all n elements
# No shortcut exists without extra structure like a sorted arrayBig Theta (Θ) — Tight Bound
Big Theta gives a tight (exact) bound. T(n) = Θ(f(n)) if and only if: T(n) = O(f(n)) AND T(n) = Ω(f(n)). In other words, f(n) is both an upper and lower bound (up to constants). Theta is the most precise — it pins down exactly which complexity class the algorithm belongs to.
def sum_array(arr):
total = 0
for x in arr: # exactly n iterations, always
total += x
return total
# Every call touches exactly n elements.
# T(n) = Θ(n) — tight bound (not O(1), not O(n²))
# Merge sort:
# Best case: O(n log n)
# Worst case: O(n log n)
# Average case: O(n log n)
# → T(n) = Θ(n log n) because best == worst (same order)Little o and Little ω
Beyond the big three, there are two more notations that mean strictly less than or greater than (not equal up to constants):
Notation | Strict meaning | Example |
|---|---|---|
o(f(n)) | T(n) grows strictly slower than f(n) | n = o(n²) — n is strictly less than n² |
ω(f(n)) | T(n) grows strictly faster than f(n) | n² = ω(n) — n² is strictly more than n |
The formal definition of little-o uses a limit: lim(n→∞) T(n) / f(n) = 0 This ratio going to zero means T(n) is dominated by f(n) — T(n) is negligible relative to f(n) for large n. You rarely use little-o in practice, but it appears in advanced analysis and some interview discussions.
How to Determine the Bound of Your Algorithm
Identify the dominant operation (the one that runs the most)
Count how many times it runs as a function of n
Drop lower-order terms (n² + n → n²)
Drop constant coefficients (3n → n)
State the result with the appropriate notation
def example(arr):
n = len(arr)
result = 0 # O(1)
for x in arr: # O(n)
result += x
for i in range(n): # O(n²) — nested loop
for j in range(n):
result += arr[i] * arr[j]
return result
# Dominant term: n²
# T(n) = O(n²) — the O(n) and O(1) parts are absorbed
# Full breakdown: c1 + c2*n + c3*n² = O(n²)Drop Constants — Why It Works
Big O hides constants because we are describing a growth rate class, not an exact formula. Two algorithms both running in 2n and 1000n steps are in the same class — O(n) — because at scale, the constant factor matters far less than the shape of the curve.
# Both are O(n) — constant factors are absorbed:
def fast_scan(arr):
return sum(arr) # ~1 operation per element
def slow_scan(arr):
total = 0
for x in arr:
for _ in range(1000): # constant inner loop
total += x / 1000
return total
# At n = 1,000,000:
# fast_scan: ~1,000,000 ops
# slow_scan: ~1,000,000,000 ops
# 1000x difference, BUT both are O(n).
# In practice constants matter — Big O tells you the category.Multi-Variable Complexity
Sometimes complexity depends on multiple input variables. You cannot reduce these — both must appear in the notation.
def print_grid(rows, cols):
for r in range(rows): # 'rows' iterations
for c in range(cols): # 'cols' iterations
print(r, c)
# T = O(rows × cols) — cannot simplify to O(n²) unless rows == cols
# BFS/DFS on a graph:
# T = O(V + E)
# V = number of vertices, E = number of edges
# These are independent variables — both stay in the notationCommon Mistakes
Quick Reference: Identifying Complexity Patterns
Code Pattern | Complexity |
|---|---|
Single loop from 0 to n | O(n) |
Loop halving each step (i /= 2) | O(log n) |
Two nested loops 0..n | O(n²) |
Three nested loops 0..n | O(n³) |
Recursive halving (binary search) | O(log n) |
Recursive call on two halves + O(n) work | O(n log n) |
Recursive call on each subset | O(2ⁿ) |
Nested loop where inner starts at outer | O(n²) — triangular |
Interview Application
In interviews, you will be expected to:
State the Big O of your solution immediately after describing it
Distinguish between time complexity and space complexity
Know whether you are describing worst, average, or best case
Recognize when Theta applies (when best and worst case are identical)
Argue why a lower bound (Omega) proves your algorithm is optimal
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums): # O(n) — one pass
complement = target - num
if complement in seen: # O(1) — hash lookup
return [seen[complement], i]
seen[num] = i # O(1) — hash insert
return []
# Time complexity: O(n) — one loop, constant-time ops inside
# Space complexity: O(n) — hash map stores up to n entries
#
# Is this optimal?
# Lower bound: Ω(n) — must read all elements at least once
# Upper bound: O(n) — our algorithm achieves this
# → Θ(n) tight bound. This solution is asymptotically optimal!Summary
Notation | Bound type | Formal condition | When to use |
|---|---|---|---|
O(f(n)) | Upper | T(n) ≤ c·f(n) for large n | Describing worst case or ceiling |
Ω(f(n)) | Lower | T(n) ≥ c·f(n) for large n | Proving a problem is hard / optimality arguments |
Θ(f(n)) | Tight | Both O and Ω with same f | When best and worst case are the same order |
o(f(n)) | Strict upper | T(n)/f(n) → 0 | T(n) is dominated by f(n) |
ω(f(n)) | Strict lower | T(n)/f(n) → ∞ | T(n) dominates f(n) |