Big-O Complexity Cheat Sheet
Compare growth rates, check assumptions, and account for both runtime and memory.
On This Page
What Is Big-O?
Big-O notation describes how an algorithm's runtime or space usage grows as the input size grows. It strips away constants and lower-order terms to focus on the shape of the growth.
A linear work model doubles when n doubles; a quadratic model grows fourfold. Big-O gives an asymptotic upper bound, rather than an exact runtime prediction. O(1) bounds work independently of n under the stated operation-cost model.
There are actually three notations that people conflate:
- O (Big-O) — upper bound. "At most this bad." State which input case and assumptions your bound describes.
- Ω (Big-Omega) — lower bound. "At least this much work." Useful for proving optimality.
- Θ (Big-Theta) — tight bound. "Exactly this growth rate." Technically the most precise, but people say "Big-O" when they mean this.
Big-O alone does not specify worst, average, or best case. State the case separately. Use Θ when you have a matching upper and lower bound.
The Hierarchy (Memorize This)
These representative functions are ordered by asymptotic growth. A feasible algorithm depends on input size, constants, output size, and the actual time and memory budget.
| Notation | Name | n = 10 | n = 100 | n = 1,000 | n = 1,000,000 | Growth |
|---|---|---|---|---|---|---|
| O(1) | Constant | 1 | 1 | 1 | 1 | Constant |
| O(log n) | Logarithmic | 3 | 7 | 10 | 20 | Logarithmic |
| O(√n) | Square root | 3 | 10 | 32 | 1,000 | Sublinear |
| O(n) | Linear | 10 | 100 | 1,000 | 1,000,000 | Linear |
| O(n log n) | Linearithmic | 33 | 664 | 10,000 | 20,000,000 | Linearithmic |
| O(n²) | Quadratic | 100 | 10,000 | 1,000,000 | 10¹² | Quadratic |
| O(n³) | Cubic | 1,000 | 1,000,000 | 10⁹ | 10¹⁸ | Cubic |
| O(2ⁿ) | Exponential | 1,024 | 10³⁰ | — | — | Exponential |
| O(n!) | Factorial | 3,628,800 | — | — | — | Factorial |
Growth Rate Comparison
Toggle functions with the legend buttons. Change the range, then enter n or move across the chart to inspect values. Curves above the vertical scale are clipped; the text readout still shows their values. These are representative functions, not benchmark timings.
How to Determine Complexity
Count the work in loops and recursive calls, including library operations and the cost of each comparison or copy.
The Rules
- Sequential blocks → add. Do A then B? O(A + B). Keep both terms when A and B are independent input sizes.
- Nested blocks → multiply. For each element, do something? O(outer) × O(inner).
- Drop constants. O(3n) = O(n). O(n/2) = O(n). Constants don't change the shape.
- Drop lower-order terms. O(n² + n) = O(n²). The bigger term dominates.
- Recursive calls → draw the recursion tree. Count total nodes × work per node.
Worked Examples
Example 1: Simple Loop
def find_max(arr):
maximum = arr[0] # O(1)
for x in arr: # runs n times
if x > maximum: # O(1) per iteration
maximum = x # O(1) per iteration
return maximum
One loop over n elements, constant work each time. Total: n × O(1).
Example 2: Nested Loops
def has_duplicate(arr):
for i in range(len(arr)): # n times
for j in range(i + 1, len(arr)):# n-1, n-2, ... 1 times
if arr[i] == arr[j]:
return True
return False
Inner loop runs n-1 + n-2 + ... + 1 = n(n-1)/2 times total. That's n²/2 - n/2. Drop constants and lower terms.
Example 3: Loop That Halves
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi: # how many times?
mid = (lo + hi) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1 # eliminate half
else:
hi = mid - 1 # eliminate half
return -1
Each iteration cuts the search space in half. Starting from n, you halve until you reach 1: n → n/2 → n/4 → ... → 1. That's log₂(n) steps.
Example 4: Loop Inside a Loop... But Not O(n²)
def two_pointer_sum(arr, target):
# arr is sorted
left, right = 0, len(arr) - 1
while left < right: # looks like one loop...
s = arr[left] + arr[right]
if s == target:
return (left, right)
elif s < target:
left += 1 # left moves right
else:
right -= 1 # right moves left
return None
Two pointers moving toward each other. Each step, one pointer moves. Total steps: at most n. Don't be tricked by the while loop — count how many times the body executes, not the nesting depth.
Example 5: Recursion (Fibonacci — Naive)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2) # two recursive calls
Each call branches into two. The recursion tree has depth n. Total nodes ≈ 2ⁿ. Each node does O(1) work. This is why naive Fibonacci is unusably slow past n ≈ 40.
Example 6: Sorting Then Scanning
def find_closest_pair(arr):
arr.sort() # O(n log n)
min_diff = float('inf')
for i in range(1, len(arr)): # O(n)
diff = arr[i] - arr[i-1]
min_diff = min(min_diff, diff)
return min_diff
Sort costs O(n log n). Linear scan costs O(n). Sequential → add → O(n log n + n). Drop the smaller term.
Example 7: Recursion With Memoization
def fib_memo(n, cache=None):
if cache is None:
cache = {}
if n in cache:
return cache[n]
if n <= 1:
return n
cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
return cache[n]
Each subproblem is computed once, giving O(n) arithmetic operations and O(n) cached values and recursion depth. This uses the usual unit-cost arithmetic model; large Fibonacci integers make individual additions more expensive. The fresh cache avoids sharing state between independent calls.
Example 8: String Building Trap
# Repeated copying can require O(n²) total work
result = ""
for char in string_of_length_n:
result += char # do not rely on an in-place optimization
If each concatenation copies the existing prefix, the total copied length is 1 + 2 + ... + n = n(n+1)/2, or Θ(n²). Interpreter optimizations may change a particular result. Use "".join(parts) in Python or a mutable builder when constructing many pieces.
Data Structure Operations
Average Case
| Data Structure | Access | Search | Insertion | Deletion | Space |
|---|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) | O(n) |
| Dynamic Array | O(1) | O(n) | O(1)* | O(n) | O(n) |
| Singly Linked List | O(n) | O(n) | O(1)† | O(1)† | O(n) |
| Doubly Linked List | O(n) | O(n) | O(1)† | O(1) | O(n) |
| Stack | O(n) | O(n) | O(1) | O(1) | O(n) |
| Queue | O(n) | O(n) | O(1) | O(1) | O(n) |
| Deque | O(n) | O(n) | O(1) | O(1) | O(n) |
| Hash Map | N/A | O(1)* | O(1)* | O(1)* | O(n) |
| Hash Set | N/A | O(1)* | O(1)* | O(1)* | O(n) |
| BST (balanced) | O(log n) | O(log n) | O(log n) | O(log n) | O(n) |
| BST (unbalanced) | O(n) | O(n) | O(n) | O(n) | O(n) |
| AVL / Red-Black Tree | O(log n) | O(log n) | O(log n) | O(log n) | O(n) |
| Binary Heap | O(1)‡ | O(n) | O(log n) | O(log n) | O(n) |
| Trie | N/A | O(m) | O(m) | O(m) | O(total characters) |
| Union-Find | N/A | O(α(n)) | N/A | N/A | O(n) |
| Segment Tree | N/A | O(log n) | O(log n) | N/A | O(n) |
| Fenwick Tree (BIT) | N/A | O(log n) | O(log n) | N/A | O(n) |
| Skip List | O(log n) | O(log n) | O(log n) | O(log n) | O(n) |
| Bloom Filter | N/A | O(k) | O(k) | N/A | O(m) |
| LRU Cache | O(1) | O(1) | O(1) | O(1) | O(n) |
| B-Tree | O(log n) | O(log n) | O(log n) | O(log n) | O(n) |
Dynamic-array insertion means append and is amortized. Hash bounds are expected averages under suitable hashing; resize costs are separately amortized. † Linked-list insert/delete assumes the node or required predecessor is known. ‡ Heap access means the root and deletion means extract-root. Trie m = key length; Bloom-filter m = allocated bits and k = hash functions. Union-Find bounds assume both path compression and union by rank/size. Range-tree search means range query; insertion means point update. All bounds assume constant-cost keys/hashes unless noted.
Worst Case (When It Matters)
| Data Structure | Access | Search | Insertion | Deletion |
|---|---|---|---|---|
| Hash Map | N/A | O(n) | O(n) | O(n) |
| BST (no balancing) | O(n) | O(n) | O(n) | O(n) |
| Skip List | O(n) | O(n) | O(n) | O(n) |
| Quick Sort (partition) | O(n²) — when pivot is always min/max | |||
Sorting Algorithms
| Algorithm | Best | Average | Worst | Space | Stable? | When to Use |
|---|---|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | ✅ | Never, except teaching |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | ❌ | Minimizing swaps |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | ✅ | Small or nearly sorted |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | ✅ | Stability needed, linked lists |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) average; O(n) worst | ❌ | General purpose, cache-friendly |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | ❌ | O(1) space guarantee |
| Counting Sort | O(n+k) | O(n+k) | O(n+k) | O(n+k) | ✅ | Small range integers; stable output array |
| Radix Sort | O(d·(n+k)) | O(d·(n+k)) | O(d·(n+k)) | O(n+k) | ✅ | Fixed-length integers/strings |
| Bucket Sort | O(n+k) | O(n+k) | O(n²) | O(n+k) | Depends on bucket sort | Expected uniform distribution |
| Tim Sort | O(n) | O(n log n) | O(n log n) | O(n) | ✅ | Python and Java object-array sorting |
k = counting range or radix · d = digits. Space is auxiliary space for the conventional array implementations. Quicksort can bound its stack to O(log n) by recursing only on the smaller partition. Radix stability requires a stable per-digit sort.
std::sort does not promise stability; use std::stable_sort when needed. Counting and radix sorts exploit bounded key representations, so the comparison-sort lower bound does not apply to them.
Graph Algorithms
| Algorithm | Time | Space | Use Case |
|---|---|---|---|
| BFS | O(V+E) | O(V) | Shortest path (unweighted), level-order |
| DFS | O(V+E) | O(V) | Connectivity, cycle detection, topo sort |
| Dijkstra | O((V+E) log V) | O(V) indexed; O(V+E) lazy | Shortest path (nonnegative weights) |
| Bellman-Ford | O(V·E) | O(V) | Shortest path (negative weights) |
| Floyd-Warshall | O(V³) | O(V²) | All-pairs shortest path |
| A* Search | O((V+E) log V)§ | O(V) indexed; O(V+E) lazy | Shortest path with heuristic (games, maps) |
| Topological Sort | O(V+E) | O(V) | Dependency ordering (DAGs) |
| Kruskal's MST | O(E log E) | O(V) | Minimum spanning tree (sparse) |
| Prim's MST | O((V+E) log V) | O(V) indexed; O(V+E) lazy | Minimum spanning tree; heap implementation |
| Tarjan's SCC | O(V+E) | O(V) | Strongly connected components |
| Edmonds-Karp | O(V·E²) | O(V+E) | Maximum flow |
| Kosaraju's SCC | O(V+E) | O(V+E) | Strongly connected components; includes reversed graph |
V = vertices · E = edges. Adjacency-list input storage is excluded unless the algorithm constructs a copy. Heap-based bounds depend on the queue implementation; a lazy queue can hold O(E) entries. § A* bound shown for a finite graph with a binary heap, nonnegative weights and a consistent heuristic; inconsistent heuristics may require reopening vertices. Heuristic quality affects explored work, not a universal O(E) guarantee. Kosaraju also needs O(V+E) if it builds a reversed graph.
Space Complexity
Time gets all the attention, but space complexity trips people up in interviews just as often. Same Big-O notation, but counting memory instead of operations.
What Counts as "Space"?
Auxiliary space measures extra live memory beyond the input. Include the recursion stack and any copies or output buffers, and state whether output storage is excluded. Total space also includes the original input. Modifying an input in place does not itself allocate a copy.
Common Space Gotchas
- Recursion eats stack space. A recursive DFS on a tree uses O(h) stack space where h is the height. For a balanced tree that's O(log n). For a skewed tree? O(n). You just blew your stack.
- Slicing creates copies. In Python,
arr[1:]creates a new list of n-1 elements. Do that inside a recursive call and you're allocating O(n) per level, O(n²) total. - Separate allocation work from peak space. Repeated prefix copying can take Θ(n²) time and total allocated bytes, while peak live string storage is O(n) when old prefixes are discarded. Retaining every prefix would take O(n²) space.
- Sorting might not be "free." Merge sort needs O(n) extra space. If they say "O(1) extra space," you need heap sort or an in-place algorithm.
Space-Time Tradeoffs
| Technique | Trades | Time | Space |
|---|---|---|---|
| Hash map lookup | Space → Time | O(1) lookup | O(n) |
| Memoization | Space → Time | Poly instead of exp | O(subproblems) |
| Two pointers | Time → Space | O(n) | O(1) |
| Bit manipulation | Space → Space | Same | O(1) vs O(n) |
| Rolling DP | Cleverness → Space | Same | O(n) → O(1) |
Amortized Analysis
Some operations are usually fast but occasionally slow. Amortized analysis averages the cost over a sequence of operations. It's not the same as average case — it's a guarantee over any sequence, not a probabilistic argument.
The Classic Example: Dynamic Array append()
When you append() to a Python list or push_back() in C++, most of the time it's O(1) — just write to the next slot. But when the array is full, it allocates a new array (by an implementation-dependent growth factor) and copies everything over. That single resize is O(n).
So is append O(1) or O(n)? It's O(1) amortized. Here's why:
Start empty with capacity 1; double when full:
Insert #1: no resize → write 1
Insert #2: capacity 1→2 → copy 1 + write 1
Insert #3: capacity 2→4 → copy 2 + write 1
Insert #4: no resize → write 1
Insert #5: capacity 4→8 → copy 4 + write 1
Insert #6-8: no resize → write 1 each
Across n appends: copies = 1 + 2 + 4 + ... < 2n
Writes = n; total work < 3n, or O(1) amortized per append.
The expensive resizes happen so rarely that they get "absorbed" by all the cheap inserts. This is the banker's method — each cheap insert "pays" a little extra into a bank, and when the expensive operation hits, there's enough saved up to cover it.
Other Amortized Bounds
- Hash map insert — expected O(1) with suitable hashing, plus amortized resize work
- Union-Find with path compression and union by rank/size — individual find can be O(log n), but any sequence of m operations is O(m·α(n)) total, so O(α(n)) amortized (basically constant)
- Splay tree operations — individual ops can be O(n), but any sequence of m ops is O(m log n) total
Common Complexity Mistakes
These trip up everyone — from beginners to people who should know better. Don't be embarrassed if you've made them; just stop making them.
❌ "This has two loops, so it's O(n²)"
Two full n-iteration loops in sequence take O(n); nesting them takes O(n²). Nested loops with shared pointers or changing bounds may do less work, so count their total iterations.
✅ Check if loops are nested or sequential.
❌ "HashMap is always O(1)"
Expected O(1) lookup depends on hashing and load-factor assumptions. An insertion sequence also includes occasional resize work, which can be O(1) amortized per insertion. These are different claims; neither removes worst-case collision costs.
✅ State the hashing assumptions and whether the bound is expected, amortized, or worst-case.
❌ "Recursion depth = time complexity"
A recursive function with depth n doesn't necessarily have O(n) time. If it branches at each level (like naive fibonacci), the time is O(branches^depth). Depth is about stack space, not time.
✅ Count total recursive calls, not just depth.
❌ "I used sorting so it's O(n log n) total"
Only if the work after sorting is ≤ O(n log n). If you sort in O(n log n) then run an O(n²) algorithm on the result, the total is O(n²). The later quadratic phase dominates.
✅ Total = max of sequential steps, not the first one.
❌ "The inner loop runs n times too, so it's O(n²)"
Not always. If the inner loop's bound depends on the outer variable (like for j in range(i)), the total is 0+1+2+...+(n-1) = n(n-1)/2 — still O(n²) in this case. But if the inner loop runs a shrinking amount (like binary search), it might be O(n log n).
✅ Sum the actual iterations — don't assume.
❌ Confusing O(n) space with O(1) space when using recursion
Recursive solutions look clean but use O(depth) stack space. A "constant space" solution means no extra allocations including the call stack. If they ask for O(1) space, they usually mean iterative.
✅ Recursion = O(depth) space. Always mention it.
❌ "O(log n) because I'm dividing something"
Only if you're dividing the problem size by a constant factor each step. Subtracting a constant gives O(n), not O(log n). n → n-1 → n-2 is linear. n → n/2 → n/4 is logarithmic.
✅ Dividing → O(log n). Subtracting → O(n).
Rules of Thumb
| n (input size) | Candidate complexity | Common Approaches |
|---|---|---|
| n ≤ 10 | O(n!) or O(2ⁿ) | Brute force, backtracking, permutations |
| n ≤ 20 | O(2ⁿ) | Bitmask DP, backtracking with pruning |
| n ≤ 100 | O(n³) | Floyd-Warshall, triple-nested loops |
| n ≤ 500 | O(n³) | Cubic DP, all-pairs |
| n ≤ 5,000 | O(n²) | 2D DP, brute-force pairs |
| n ≤ 100,000 | O(n log n) | Sorting, divide & conquer, segment tree |
| n ≤ 1,000,000 | O(n) | Two pointers, sliding window, hash map |
| n ≤ 10,000,000 | O(n) | Linear scan, counting, streaming |
| n > 10,000,000 | O(log n) or O(1) | Binary search, math, hash lookup |
Quick Constraint → Complexity Decoder
Constraints suggest approaches to investigate; they do not determine a unique solution:
- Constraint says n ≤ 10⁵ and the output is one number? → O(n) or O(n log n). Think hash map, sorting, two pointers.
- Constraint says n ≤ 10³ and it's a grid/matrix? → O(n²) or O(n² log n). DP on grid, BFS/DFS on matrix.
- Constraint says n ≤ 20? → Consider bitmask DP or backtracking, then check the actual branching and time limit.
- Constraint says n ≤ 10⁹? → O(log n) or O(1). Math or binary search. You're not iterating over that.
- Two constraints, like n ≤ 10⁵ and k ≤ 10? → Consider O(n·k), and identify what k controls before choosing an approach.
Implementation references
Language guarantees differ from textbook algorithm names. Check the Python sorting guide, Python string documentation, Java Arrays contract, C++ sorting specification, and Boost A* analysis.