Every complexity you need to know, in one place. Print this, bookmark it, tattoo it on your forearm — whatever works.
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.
O(n) means "scales linearly" — double the input, double the time. O(n²) means "scales quadratically" — double the input, quadruple the time. O(1) means "constant" — input size doesn't matter.
There are actually three notations that people conflate:
In interviews, nobody cares about the distinction. Just use Big-O and describe the worst case unless asked otherwise.
From fastest to slowest growing. Everything above the line is "good" for interviews; everything below gets you a raised eyebrow.
| Notation | Name | n = 10 | n = 100 | n = 1,000 | n = 1,000,000 | Verdict |
|---|---|---|---|---|---|---|
| O(1) | Constant | 1 | 1 | 1 | 1 | Instant |
| O(log n) | Logarithmic | 3 | 7 | 10 | 20 | Beautiful |
| O(√n) | Square root | 3 | 10 | 32 | 1,000 | Great |
| O(n) | Linear | 10 | 100 | 1,000 | 1,000,000 | Good |
| O(n log n) | Linearithmic | 33 | 664 | 10,000 | 20,000,000 | Fine |
| O(n²) | Quadratic | 100 | 10,000 | 1,000,000 | 10¹² | Slow |
| O(n³) | Cubic | 1,000 | 1,000,000 | 10⁹ | 10¹⁸ | Painful |
| O(2ⁿ) | Exponential | 1,024 | 10³⁰ | — | — | No |
| O(n!) | Factorial | 3,628,800 | — | — | — | Why |
Click a function in the legend to toggle it. Use the scale buttons to zoom into different ranges.
Forget the formulas. Here's the practical approach: look at the loops, look at the recursion, and count the work.
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).
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.
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.
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.
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.
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.
def fib_memo(n, 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]
Same recursion, but each subproblem is computed only once. There are n unique subproblems, each doing O(1) work. The memoization collapses the exponential tree into a linear chain.
# Looks O(n), is actually O(n²) in many languages
result = ""
for char in string_of_length_n:
result += char # creates a NEW string each time!
String concatenation in Python/Java creates a new string each time. Iteration i copies i characters. Total: 1 + 2 + ... + n = n(n-1)/2. Use "".join(list) or StringBuilder instead.
| 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(n·m) |
| 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) |
* amortized average · † given a pointer to the node · ‡ min/max only · m = key length · k = hash functions · α = inverse Ackermann (effectively constant)
| 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 | |||
| 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) | ❌ | 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(k) | ✅ | Small range integers |
| Radix Sort | O(d·n) | O(d·n) | O(d·n) | O(n+k) | ✅ | Fixed-length integers/strings |
| Bucket Sort | O(n+k) | O(n+k) | O(n²) | O(n+k) | ✅ | Uniform distribution |
| Tim Sort | O(n) | O(n log n) | O(n log n) | O(n) | ✅ | Python/Java default — real-world data |
k = range of values · d = number of digits
| 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) | Shortest path (positive 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(E)§ | O(V) | 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) | Minimum spanning tree (dense) |
| 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) | Strongly connected components (two-pass) |
V = vertices · E = edges · § depends on heuristic quality
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.
You're measuring auxiliary space — extra memory your algorithm uses beyond the input. The input itself doesn't count (unless you're modifying it in place, which might matter for the discussion).
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.| 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) |
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.
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 (typically 2x the size) 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:
Starting with capacity 1, doubling each time:
Insert #1: capacity 1→2, copy 1 element → 1 work
Insert #2: no resize → 1 work
Insert #3: capacity 2→4, copy 2 elements → 2 work
Insert #4: no resize → 1 work
Insert #5: capacity 4→8, copy 4 elements → 4 work
Insert #6-8: no resize → 1 work each
After n inserts: total copies = 1+2+4+...+n ≈ 2n
Average per insert: 2n/n = 2 = O(1)
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.
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.
Nested loops → O(n²). Sequential loops → O(n + n) = O(n). Two loops doesn't mean quadratic unless one is inside the other.
✅ Check if loops are nested or sequential.
Average case with a good hash function: yes. Worst case with collisions: O(n). In interviews, say "O(1) amortized" or "O(1) average." If the interviewer is picky, they'll appreciate the distinction.
✅ Say "O(1) average" or "O(1) amortized" — never just "O(1)."
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.
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 sort didn't help.
✅ Total = max of sequential steps, not the first one.
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.
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.
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).
| n (input size) | Max Acceptable 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 |
When you read a problem on LeetCode and see the constraints section, here's your instant filter: