Time & Space Complexity
Scope — Walkthroughs — how to argue the complexity of an actual LC solution, one classic per technique. See also: complexity_cheatsheet.md — the lookup tables; complexity_drills.md — self-test drills.
What this doc is: a code-first companion. For each classic LeetCode problem we show the actual solution code, annotate it with
time/spacecomplexity, and explain WHY that complexity holds — plus the pattern it belongs to and similar LCs.Related docs (do not duplicate):
complexity_cheatsheet.md— reference tables + math intuitions (geometric/arithmetic series, Master Theorem)complexity_drills.md— quiz yourself: derive complexity from a snippetlc_pattern.md— pattern → problem mapping
Overview
How to Read Complexity (the 3 questions) Priority 4 of 5 — High value — a gap here costs you rounds
1. TIME : how many primitive ops as input n grows?
-> count loops, recursion calls, work-per-call
2. SPACE : how much EXTRA memory (excludes the input itself)?
-> data structures you allocate + recursion stack depth
3. CAN I DO BETTER? (interviewers always ask)
-> sort first? hashmap? two pointers? in-place?
The “n vs reasonable complexity” ruler Priority 5 of 5 — Must know — expect it in almost every loop
The ruler lives in one place: complexity_cheatsheet.md § 6 — n ≤ 20 invites O(2^n), n ≤ 10^5 invites O(n log n), and ~10^8 simple ops per second is the budget.
Two complexity traps interviewers love Priority 5 of 5 — Must know — expect it in almost every loop
| Trap | Wrong | Right | Why |
|---|---|---|---|
| Build heap | O(n log n) | O(n) | heapify = geometric series ∑ (see cheatsheet 3-1) |
| Recursion stack space | “O(n) nodes” | O(h) = tree height | DFS holds ONE path, not all nodes |
j starts at i |
O(n) | O(n²) | n+(n-1)+…+1 = n(n+1)/2 |
Slicing s[1:] in a loop |
O(n) | O(n²) | each slice copies O(n) chars |
0) Cheat Table — Classic LCs at a Glance Priority 5 of 5 — Must know — expect it in almost every loop
| # | Problem | Pattern | Time | Space |
|---|---|---|---|---|
| 1 | Two Sum | HashMap | O(n) | O(n) |
| 20 | Valid Parentheses | Stack | O(n) | O(n) |
| 21 | Merge Two Sorted Lists | Two pointers (list) | O(n+m) | O(1) |
| 3 | Longest Substring w/o Repeat | Sliding window | O(n) | O(min(n,Σ)) |
| 76 | Minimum Window Substring | Sliding window | O(n+m) | O(Σ) |
| 704 | Binary Search | Binary search | O(log n) | O(1) |
| 33 | Search Rotated Sorted Array | Binary search | O(log n) | O(1) |
| 875 | Koko Eating Bananas | Binary search on answer | O(n log m) | O(1) |
| 56 | Merge Intervals | Sort + sweep | O(n log n) | O(n) |
| 215 | Kth Largest Element | Heap / QuickSelect | O(n log k) / O(n) avg | O(k) / O(1) |
| 347 | Top K Frequent | Heap / bucket | O(n log k) / O(n) | O(n) |
| 23 | Merge K Sorted Lists | Heap | O(N log k) | O(k) |
| 104 | Max Depth Binary Tree | DFS | O(n) | O(h) |
| 102 | Level Order Traversal | BFS | O(n) | O(w) |
| 200 | Number of Islands | DFS/BFS grid | O(m·n) | O(m·n) |
| 207 | Course Schedule | Topological sort | O(V+E) | O(V+E) |
| 70 | Climbing Stairs | 1D DP | O(n) | O(1) |
| 322 | Coin Change | Unbounded knapsack DP | O(n·amount) | O(amount) |
| 300 | LIS | DP / patience sort | O(n²) / O(n log n) | O(n) |
| 78 | Subsets | Backtracking | O(n·2^n) | O(n) |
| 46 | Permutations | Backtracking | O(n·n!) | O(n) |
| 42 | Trapping Rain Water | Two pointers | O(n) | O(1) |
1) HashMap — O(1) lookup trades space for time
LC 1 — Two Sum
Pattern: store “what I’ve seen → where”. Turn an O(n²) pair search into O(n).
# python — LC 1
# time = O(n) : single pass over nums
# space = O(n) : hashmap can hold up to n entries
class Solution:
def twoSum(self, nums, target):
seen = {} # value -> index
for i, x in enumerate(nums):
if target - x in seen: # O(1) average lookup
return [seen[target - x], i]
seen[x] = i
Why O(n) time / O(n) space:
- One loop = n iterations; each does O(1) hashmap ops → O(n).
seengrows to at most n keys → O(n) extra memory.
Why not O(n²)? The brute force checks every pair (n(n+1)/2 comparisons). The hashmap
“remembers” complements so each element is examined once.
Similar: LC 49 Group Anagrams (O(n·k)), LC 128 Longest Consecutive Sequence (O(n)), LC 560 Subarray Sum = K.
2) Stack — match / undo in LIFO order
LC 20 — Valid Parentheses
# python — LC 20
# time = O(n) : each char pushed/popped at most once
# space = O(n) : worst case all opening brackets "((((("
class Solution:
def isValid(self, s):
pair = {')': '(', ']': '[', '}': '{'}
stack = []
for c in s:
if c in pair:
if not stack or stack.pop() != pair[c]:
return False
else:
stack.append(c)
return not stack
Why O(n)/O(n): single pass, push/pop are O(1). Stack can hold up to n unmatched openers.
LC 84 — Largest Rectangle in Histogram (monotonic stack)
# python — LC 84
# time = O(n) : each bar pushed once + popped once (amortized O(1) per bar)
# space = O(n) : the monotonic stack
class Solution:
def largestRectangleArea(self, heights):
stack = [] # indices, increasing heights
heights.append(0) # sentinel flushes the stack at the end
best = 0
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
top = stack.pop()
left = stack[-1] if stack else -1
width = i - left - 1
best = max(best, heights[top] * width)
stack.append(i)
return best
Why O(n) despite the inner while? Classic amortized argument: every index is pushed
exactly once and popped at most once, so total inner-loop work across the whole run is
≤ 2n → O(n), not O(n²).
Similar: LC 42 Trapping Rain Water, LC 496/503 Next Greater Element, LC 739 Daily Temperatures. See
monotonic_stack.md.
3) Sliding Window — O(n) over substrings/subarrays
LC 3 — Longest Substring Without Repeating Characters
# python — LC 3
# time = O(n) : left & right pointers each move forward ≤ n times
# space = O(min(n,Σ)): the window set, bounded by alphabet size Σ
class Solution:
def lengthOfLongestSubstring(self, s):
seen = set()
left = best = 0
for right, c in enumerate(s):
while c in seen: # shrink until window valid
seen.remove(s[left])
left += 1
seen.add(c)
best = max(best, right - left + 1)
return best
Why O(n), not O(n²)? The while looks nested, but left only ever increases and never
exceeds right. Across the whole run, left advances a total of ≤ n steps. So both pointers do
O(n) combined work → O(n).
Why O(min(n, Σ)) space? The set never holds more than the number of distinct chars, which is capped by both the string length n and the alphabet size Σ (e.g. 26 or 128).
Similar: LC 76 Min Window Substring (O(n+m)), LC 424 Longest Repeating Char Replacement, LC 209 Min Size Subarray Sum, LC 438 Find All Anagrams. See
sliding_window.md.
4) Binary Search — halve the search space → O(log n)
LC 704 — Binary Search
# python — LC 704
# time = O(log n) : search space halves each iteration
# space = O(1) : two pointers, iterative
class Solution:
def search(self, nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
Why O(log n)? How many times can you halve n until reaching 1? log₂(n) times.
LC 875 — Koko Eating Bananas (binary search on the ANSWER) Priority 4 of 5 — High value — a gap here costs you rounds
# python — LC 875
# time = O(n log m) : binary search over speeds [1..max_pile] (log m), each check scans n piles
# space = O(1)
import math
class Solution:
def minEatingSpeed(self, piles, h):
def hours(speed):
return sum(math.ceil(p / speed) for p in piles) # O(n)
lo, hi = 1, max(piles)
while lo < hi:
mid = (lo + hi) // 2
if hours(mid) <= h: # feasible -> try slower
hi = mid
else:
lo = mid + 1
return lo
Why O(n log m)? We binary search over the value range m = max(piles) (→ log m iterations),
and each feasibility check is an O(n) scan. The answer space is monotonic (“if speed s works, s+1 works”),
which is what makes binary search applicable.
Similar: LC 33 Search in Rotated Array (O(log n)), LC 153 Find Min in Rotated Array, LC 410 Split Array Largest Sum, LC 1011 Capacity to Ship Packages. See
binary_search.md.
5) Sort + Sweep — pay O(n log n) once, then O(n)
LC 56 — Merge Intervals
# python — LC 56
# time = O(n log n) : sorting dominates; the merge sweep is O(n)
# space = O(n) : output list (O(log n)..O(n) for the sort itself)
class Solution:
def merge(self, intervals):
intervals.sort(key=lambda x: x[0]) # O(n log n)
res = []
for s, e in intervals: # O(n) sweep
if res and s <= res[-1][1]:
res[-1][1] = max(res[-1][1], e) # overlap -> extend
else:
res.append([s, e])
return res
Why O(n log n)? Comparison sort is the bottleneck; the linear sweep after it is “free” by comparison. Lesson: when you see “intervals”, “overlap”, or “schedule”, sorting first usually unlocks a linear pass.
Similar: LC 57 Insert Interval, LC 252/253 Meeting Rooms (II uses a heap), LC 435 Non-overlapping Intervals, LC 1288 Remove Covered Intervals. See
intervals.md.
6) Heap (Priority Queue) — keep the top-k cheaply
LC 215 — Kth Largest Element
# python — LC 215 (min-heap of size k approach)
# time = O(n log k) : n pushes/pops on a heap capped at size k
# space = O(k) : heap holds at most k elements
import heapq
class Solution:
def findKthLargest(self, nums, k):
heap = []
for x in nums:
heapq.heappush(heap, x)
if len(heap) > k:
heapq.heappop(heap) # evict smallest -> heap keeps top-k
return heap[0] # smallest of the top-k = kth largest
Why O(n log k), not O(n log n)? The heap is bounded at size k, so each push/pop is
log k (not log n). Over n elements → O(n log k). Space is O(k) because we never store
more than k items.
Alternative — QuickSelect: O(n) average, O(1) extra space (but O(n²) worst case). Use the heap when you also want a streaming top-k.
Similar: LC 347 Top K Frequent, LC 23 Merge K Sorted Lists (O(N log k)), LC 295 Find Median from Data Stream (two heaps), LC 973 K Closest Points. See
heap.md.
7) Trees — time = nodes, space = HEIGHT (the classic gotcha) Priority 4 of 5 — High value — a gap here costs you rounds
LC 104 — Maximum Depth of Binary Tree
# python — LC 104
# time = O(n) : visit every node once
# space = O(h) : recursion stack = tree height (h = log n balanced, h = n skewed)
class Solution:
def maxDepth(self, root):
if not root:
return 0
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
Why space = O(h), NOT O(n)? ⭐ DFS recursion descends one path at a time. At any instant the
stack holds only the frames along the current root→leaf path = the height h. It does not hold
all n nodes simultaneously.
- Balanced tree →
h = log n→ O(log n) space - Skewed tree (linked-list shape) →
h = n→ O(n) space
LC 102 — Binary Tree Level Order Traversal (BFS)
# python — LC 102
# time = O(n) : each node enqueued/dequeued once
# space = O(w) : queue holds the widest level; w ≈ n/2 for a full tree -> O(n)
from collections import deque
class Solution:
def levelOrder(self, root):
if not root:
return []
res, q = [], deque([root])
while q:
level = []
for _ in range(len(q)): # snapshot current level size
node = q.popleft()
level.append(node.val)
if node.left: q.append(node.left)
if node.right: q.append(node.right)
res.append(level)
return res
DFS vs BFS space: DFS = O(height), BFS = O(width). For a balanced tree the last level holds ~n/2 nodes, so BFS is O(n) while DFS is O(log n). Choose traversal by tree shape.
Similar: LC 226 Invert Tree, LC 236 LCA (O(n)/O(h)), LC 124 Max Path Sum (O(n)/O(h)), LC 297 Serialize/Deserialize. See
tree.md/binary_tree.md.
8) Graphs — O(V + E)
LC 200 — Number of Islands (DFS on a grid)
# python — LC 200
# time = O(m·n) : every cell visited at most once
# space = O(m·n) : recursion stack worst case (grid is one big island / snake)
class Solution:
def numIslands(self, grid):
if not grid:
return 0
m, n = len(grid), len(grid[0])
def sink(r, c):
if r < 0 or c < 0 or r >= m or c >= n or grid[r][c] != '1':
return
grid[r][c] = '0' # mark visited in-place
sink(r+1, c); sink(r-1, c); sink(r, c+1); sink(r, c-1)
count = 0
for r in range(m):
for c in range(n):
if grid[r][c] == '1':
count += 1
sink(r, c)
return count
Why O(m·n) time? A grid with m·n cells is a graph with V = m·n vertices and E ≈ 4·m·n
edges. DFS/BFS is O(V+E) = O(m·n). Each cell is sunk (visited) once.
Why O(m·n) space? The recursion can go as deep as the number of cells in the worst case (a single snaking island). Convert to BFS with a queue to bound auxiliary structure similarly, or use Union-Find.
LC 207 — Course Schedule (topological sort / cycle detection)
# python — LC 207 (Kahn's BFS)
# time = O(V + E) : build graph O(E), process each node + edge once
# space = O(V + E) : adjacency list + indegree array + queue
from collections import deque, defaultdict
class Solution:
def canFinish(self, numCourses, prerequisites):
graph = defaultdict(list)
indeg = [0] * numCourses
for nxt, pre in prerequisites:
graph[pre].append(nxt)
indeg[nxt] += 1
q = deque(i for i in range(numCourses) if indeg[i] == 0)
done = 0
while q:
node = q.popleft()
done += 1
for nb in graph[node]:
indeg[nb] -= 1
if indeg[nb] == 0:
q.append(nb)
return done == numCourses # all taken => no cycle
Why O(V+E)? Building the graph touches every edge once (O(E)). The BFS dequeues each node once (O(V)) and relaxes each edge once (O(E)). Total O(V+E).
Similar: LC 133 Clone Graph, LC 210 Course Schedule II, LC 743 Network Delay (Dijkstra O((V+E)log V)), LC 684 Redundant Connection (Union-Find ~O(α)). See
graph.md/topology_sorting.md.
O(V + E) vs O(V × E) — add, or multiply? Priority 4 of 5 — High value — a gap here costs you rounds
The rule: add when every vertex and every edge is handled once in total; multiply when the whole edge set (or the whole graph) is handled again for each vertex or round. It is the same rule for time and for space. For time, count the steps. For space, count what is held in memory at the same time.
| You see | Time | Extra space (input not counted) | Example |
|---|---|---|---|
| One BFS / DFS over an adjacency list | O(V + E) | O(V + E) | LC 207, LC 399 |
V − 1 rounds, each relaxing all E edges |
O(V × E) | O(V) | Bellman-Ford, LC 743 |
| A full traversal started from every vertex | O(V × (V + E)) | depends what you keep | LC 1462, LC 2192 |
| Every vertex scans the raw edge list for its neighbours | O(V × E) | O(V) | a bug, not an algorithm |
Time — why one traversal is V + E, not V × E
# python
# IDEA: build an adjacency list once, then BFS — every vertex is queued once, every list scanned once
# time = O(V + E) : each vertex is dequeued once; each adjacency list is scanned once
# space = O(V + E): the adjacency list this code builds, plus seen + queue (O(V))
from collections import defaultdict, deque
def bfs(edges, src):
graph = defaultdict(list)
for u, v in edges: # O(E) to build
graph[u].append(v)
graph[v].append(u)
seen = {src}
queue = deque([src])
while queue: # runs once per vertex → V
node = queue.popleft()
for nb in graph[node]: # runs deg(node) times → Σ deg = 2E in total
if nb not in seen:
seen.add(nb)
queue.append(nb)
return seen
The trap: “the outer loop runs V times and the inner loop can run up to E times, so V × E.” The inner loop’s bound is right for one vertex and wrong for all of them together. Each vertex’s list is scanned exactly once, and the lengths of all the lists add up to the number of edges (the handshake sum):
deg(v1) + deg(v2) + … + deg(vV) = 2E (undirected; = E for a directed graph)
star graph: centre 0 joined to 1…9 V = 10, E = 9
graph[0] has 9 neighbours, graph[1…9] have 1 each
inner-loop steps, counted: 9 + 9·1 = 18 = 2E ✓ O(V + E)
the V × E estimate: 10 · 9 = 90 ✗ 5× too many
When one vertex has many neighbours, the others have few. The total is fixed: 2E for an undirected graph, or E for the outgoing lists of a directed one.
Time — where V × E really comes from
1. The edge set is scanned once per round — Bellman-Ford.
# python
# LC 743 - Network Delay Time
# IDEA: Bellman-Ford — relax every edge, up to V − 1 rounds, stop early once a round changes nothing
# time = O(V × E) : up to V − 1 rounds, each relaxing every edge
# space = O(V) : one dist array, overwritten every round (`times` is the input)
def networkDelayTime(times, n, k):
dist = [float('inf')] * (n + 1)
dist[k] = 0
for _ in range(n - 1): # V − 1 rounds
updated = False
for u, v, w in times: # all E edges, every round
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
updated = True
if not updated: # nothing changed: later rounds would not either
break
best = max(dist[1:])
return -1 if best == float('inf') else best
The loops multiply. The memory does not: every round writes into the same dist array, so space
stays O(V). Dijkstra solves LC 743 in O((V + E) log V); Bellman-Ford is the tool when weights can be
negative or the number of edges is bounded (LC 787). See Bellman-Ford.md.
2. A whole traversal is repeated from every vertex.
# python
# LC 1462 - Course Schedule IV
# IDEA: BFS from every course and keep each reachable set; a query is then one set lookup
# time = O(V × (V + E)) : V separate BFS runs, each O(V + E)
# space = O(V²) : one reachable set per course, kept for the queries
from collections import defaultdict, deque
def checkIfPrerequisite(numCourses, prerequisites, queries):
graph = defaultdict(list)
for pre, nxt in prerequisites:
graph[pre].append(nxt)
reach = [] # reach[s] = every course reachable from s
for start in range(numCourses): # V starts
seen = {start}
queue = deque([start])
while queue: # one full BFS = O(V + E)
node = queue.popleft()
for nb in graph[node]:
if nb not in seen:
seen.add(nb)
queue.append(nb)
reach.append(seen)
return [v in reach[u] for u, v in queries]
This is V × (V + E) = O(V² + V × E), which is not the same as V × E. Say both terms unless the graph is
known to be connected (then E ≥ V − 1 and it simplifies to O(V × E)).
3. The neighbours are found by scanning the edge list. If the code never builds an adjacency list
and instead runs for u, v in edges for every vertex it pops, each of the V vertices scans all E edges:
O(V × E). On the star graph above that is 90 steps instead of 18. The fix is to build the
adjacency list once, in O(E), which brings the traversal back to O(V + E).
Space — count the entries, not the levels of nesting
A hashmap’s space is the total number of entries stored under every key, not “one per key” and not “the map is nested, so multiply”.
A --- B graph = {
| | "A": ["B", "C"],
C --- D "B": ["A", "D"],
"C": ["A", "D"],
"D": ["B", "C"],
}
| Stored | Count | Why |
|---|---|---|
| keys | V = 4 | one per vertex |
| entries in all the lists | 2E = 8 | each undirected edge is stored twice (A→B and B→A) |
| total | O(V + E) | the constant 2 drops |
A nested map does not change the count. LC 399 Evaluate Division stores graph[a][b] = value and
graph[b][a] = 1 / value: the inner dicts hold each vertex’s own neighbours, so all of them together
hold 2E entries. That is still O(V + E).
Space becomes O(V × E) only when each of the V keys holds its own copy of all E edges:
# python
# IDEA: every vertex keeps its own full copy of the edge list (shown to count it, not to copy it)
# space = O(V × E): V lists, each of length E
copies = {vertex: list(edges) for vertex in graph}
On the square above that is 4 × 4 = 16 entries against the adjacency list’s 8. This is rarely what a problem needs. If you find yourself writing it, store the edges once and share them.
Two products that are easy to mislabel as V × E:
- An adjacency matrix is
V × Vcells whatever E is: O(V²). - A per-source result kept for later, as in LC 1462 above, is V sets of up to V vertices: O(V²).
How to say it in an interview
1. How many keys? one per vertex → O(V)
2. How many entries under ALL keys? each edge once/twice → + O(E) ⇒ O(V + E)
a copy of E per key → V × E ⇒ O(V × E)
3. Does a loop repeat over all edges? once in total → + E; once per vertex/round → × E
“The space is the total number of entries in the map, not the number of keys. An adjacency list stores V vertices and E edges, so it is O(V + E). It would be O(V × E) only if each of the V vertices kept its own copy of all E edges. For time it is the same idea: one BFS touches each edge once, so O(V + E); Bellman-Ford relaxes every edge in each of V − 1 rounds, so O(V × E).”
9) Dynamic Programming — define state, count states × work
LC 70 — Climbing Stairs (1D DP, space-optimized)
# python — LC 70
# time = O(n) : one pass building dp
# space = O(1) : only keep the last two values (rolling variables)
class Solution:
def climbStairs(self, n):
a, b = 1, 1 # ways(0), ways(1)
for _ in range(n - 1):
a, b = b, a + b # Fibonacci recurrence
return b
Why O(n)/O(1)? There are n subproblems, each O(1) to compute → O(n) time. The recurrence only depends on the previous two states, so we drop the full O(n) array down to O(1) rolling vars.
Space-optimization rule: if
dp[i]only readsdp[i-1],dp[i-2], keep variables, not arrays.
LC 322 — Coin Change (unbounded knapsack)
# python — LC 322
# time = O(n · amount) : for each of `amount` sub-targets, try all n coins
# space = O(amount) : 1D dp over target values
class Solution:
def coinChange(self, coins, amount):
INF = amount + 1
dp = [0] + [INF] * amount # dp[t] = min coins to make t
for t in range(1, amount + 1):
for c in coins:
if c <= t:
dp[t] = min(dp[t], dp[t - c] + 1)
return dp[amount] if dp[amount] != INF else -1
Why O(n·amount)? The state is “amount left to make” (amount+1 values), and each state tries
all n coins → n × amount transitions. This is pseudo-polynomial — it depends on the numeric
value amount, not just the input size.
LC 300 — Longest Increasing Subsequence (two complexity tiers)
# python — LC 300 (V1: classic DP)
# time = O(n²) : for each i, scan all j < i
# space = O(n)
class Solution:
def lengthOfLIS(self, nums):
dp = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
# python — LC 300 (V2: patience sorting + binary search)
# time = O(n log n) : n elements, each binary-searched into `tails`
# space = O(n)
import bisect
class Solution:
def lengthOfLIS(self, nums):
tails = [] # tails[k] = smallest tail of an LIS of length k+1
for x in nums:
i = bisect.bisect_left(tails, x)
if i == len(tails):
tails.append(x)
else:
tails[i] = x # replace -> keeps tails minimal
return len(tails)
Why the speedup? V1’s inner scan is O(n) → O(n²). V2 replaces that scan with an O(log n) binary
search into a sorted tails array → O(n log n). Same answer, better asymptotics — a textbook
“can you do better?” upgrade.
Similar: LC 1143 LCS (O(m·n)), LC 72 Edit Distance (O(m·n)), LC 53 Maximum Subarray / Kadane (O(n)), LC 5 Longest Palindromic Substring. See
dp.md/dp_pattern.md.
10) Backtracking — output-bounded: O(answers × cost per answer)
LC 78 — Subsets
# python — LC 78
# time = O(n · 2^n) : 2^n subsets, each costs O(n) to copy into the result
# space = O(n) : recursion depth + current path (output not counted as aux)
class Solution:
def subsets(self, nums):
res = []
def dfs(start, path):
res.append(path[:]) # O(n) copy
for i in range(start, len(nums)):
path.append(nums[i])
dfs(i + 1, path)
path.pop() # undo (backtrack)
dfs(0, [])
return res
Why O(n·2^n)? Each element is either in or out → 2^n subsets. Copying each subset into the result is O(n) → n·2^n. The recursion stack depth is only O(n) (the path length).
LC 46 — Permutations
# python — LC 46
# time = O(n · n!) : n! permutations, each O(n) to build/copy
# space = O(n) : recursion depth + used set
class Solution:
def permute(self, nums):
res = []
def dfs(path, remaining):
if not remaining:
res.append(path[:])
return
for i in range(len(remaining)):
dfs(path + [remaining[i]], remaining[:i] + remaining[i+1:])
dfs([], nums)
return res
Why O(n·n!)? There are n! orderings; producing each costs O(n). Backtracking complexity is
almost always (number of solutions) × (work per solution) — count the leaves of the recursion tree.
Similar: LC 77 Combinations, LC 39/40 Combination Sum, LC 51 N-Queens (O(n!)), LC 22 Generate Parentheses (Catalan number of results). See
backtrack.md.
11) Two Pointers — O(n) time, O(1) space sweet spot
LC 42 — Trapping Rain Water
# python — LC 42
# time = O(n) : single pass, two converging pointers
# space = O(1) : just a few scalars (vs O(n) for the prefix/suffix-array version)
class Solution:
def trap(self, height):
l, r = 0, len(height) - 1
left_max = right_max = water = 0
while l < r:
if height[l] < height[r]:
left_max = max(left_max, height[l])
water += left_max - height[l]
l += 1
else:
right_max = max(right_max, height[r])
water += right_max - height[r]
r -= 1
return water
Why O(1) space? The naive DP precomputes leftMax[] and rightMax[] arrays = O(n) space. The
two-pointer insight — “the smaller side bounds the water” — lets us track just two running maxima,
collapsing space to O(1) while staying O(n) time.
Similar: LC 11 Container With Most Water, LC 15 3Sum (O(n²)), LC 167 Two Sum II, LC 125 Valid Palindrome, LC 26/27 Remove Duplicates in place. See
2_pointers.md.
12) Quick Reference — “How to argue complexity in an interview”
SINGLE LOOP over n → O(n)
TWO NESTED LOOPS (full) → O(n²)
LOOP that halves/doubles → O(log n)
LOOP + inner binary search → O(n log n)
SORT anything → O(n log n) (then often an O(n) sweep)
RECURSION → (#calls) × (work per call); space = max depth
BACKTRACKING → (#solutions) × (cost each); space = recursion depth
HEAP of size k, n ops → O(n log k)
GRAPH traversal → O(V + E)
GRAPH, all edges per round → O(V × E) (Bellman-Ford; V traversals = O(V × (V + E)))
GRID traversal → O(rows · cols)
DP → (#states) × (transition cost)
AMORTIZED (each item ≤1 push/pop) → O(n) even with an inner while-loop
Space-complexity checklist
□ Did I count the recursion stack? (DFS = O(height), not O(1))
□ Is the output counted or excluded? (usually excluded as "aux space")
□ Can a 2D dp collapse to 1D rolling? (dp[i] depends only on dp[i-1]?)
□ Can two pointers replace an array? (prefix-array → running variable)
□ In-place possible? (mark grid, reverse array, swap)
Final sanity checks
1. Two O(n) passes = still O(n) (constants drop)
2. heapify = O(n); n× heappush = O(n log n) ← NOT the same!
3. Inner `while` ≠ automatically O(n²) — check amortized (sliding window, monotonic stack)
4. Slicing/concatenating inside a loop hides an O(n) cost → can make it O(n²)
5. HashMap is O(1) AVERAGE, O(n) worst (collisions) — say this out loud
6. Recursion space = stack depth = tree/recursion height
References
complexity_cheatsheet.md— reference tables + Big-O math (series, Master Theorem)complexity_drills.md— derive complexity from snippets (quiz format)lc_pattern.md— pattern → LC mapping- Big-O Cheat Sheet