Time & Space Complexity

Interview MetaPriority 4 of 5 — High value — a gap here costs you roundsHigh value Updated Nov 12, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

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/space complexity, and explain WHY that complexity holds — plus the pattern it belongs to and similar LCs.

Related docs (do not duplicate):


Overview

How to Read Complexity (the 3 questions) Priority 4 of 5 — High value — a gap here costs you rounds

text
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
# 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).
  • seen grows 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
# 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
# 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
# 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)

python
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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):

text
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
# 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
# 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”.

text
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
# 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 × V cells 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

text
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
# 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 reads dp[i-1], dp[i-2], keep variables, not arrays.

LC 322 — Coin Change (unbounded knapsack)

python
# 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
# 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
# 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
# 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
# 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
# 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”

text
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

text
□ 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

text
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