🧭 Pattern Selection Flowchart — pick a pattern, jump to it below
Answer the questions to land on a pattern; click a blue box to jump to its Java card.
Coding-Interview Pattern Flowchart & Java Playbook
Reconstructed from the AlgoMonster algorithm-selection flowchart. Answer the yellow questions top-to-bottom; each blue box is a recommended pattern with a worked Java example below. For a full deep-dive on each pattern — brute force → optimal, multiple Java templates, and traced examples — see the In-Depth Patterns Guide → · for the ranked LeetCode problem sets (company tags, difficulty) with a solved example per category, see LeetCode by Pattern →
class TreeNode { int val; TreeNode left, right; TreeNode(int v){ val = v; } }
class ListNode { int val; ListNode next; ListNode(int v){ val = v; } }
// Imports assumed: java.util.* (List, Map, Set, Queue, Deque, PriorityQueue, Arrays, ...)struct TreeNode {
int val;
TreeNode *left, *right;
TreeNode(int v) : val(v), left(nullptr), right(nullptr) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode(int v) : val(v), next(nullptr) {}
};
// Includes assumed: <vector>, <unordered_map>, <unordered_set>, <queue>, <deque>, <algorithm>, ...class TreeNode:
def __init__(self, v):
self.val = v
self.left = None
self.right = None
class ListNode:
def __init__(self, v):
self.val = v
self.next = None
# Imports assumed: collections (deque, defaultdict), heapq, etc.class TreeNode {
val: number;
left: TreeNode | null = null;
right: TreeNode | null = null;
constructor(v: number) { this.val = v; }
}
class ListNode {
val: number;
next: ListNode | null = null;
constructor(v: number) { this.val = v; }
}
// Imports assumed: none needed (Array, Map, Set are built-in)The Decision Flowchart
Diamonds are questions, blue boxes are recommended patterns. Follow the labelled yes / no edges. Click any blue box to jump to its Java card.
Split into 5 connected charts so each stays readable. The grey ▶ Chart N box hands off to the next chart.
Chart 1 — Start · Graphs & Trees
flowchart TD
gRoot{"Is it a graph?"}
tree{"Is it a tree?"}
treeLevel{"Level-order / shortest-level answer?"}
treeBuild{"Count / generate many trees?"}
directed{"DAG-related?"}
shortestPath{"Shortest-path problem?"}
weighted{"Weighted graph?"}
connectivity{"Connectivity problem?"}
graphSmall{"Small constraints?"}
treeBfs["BFS — level order"]
treeDc["Divide and Conquer / Tree DP"]
treeDfs["DFS"]
topo["Topological Sort"]
dijkstra["Dijkstra's Algorithm"]
bfsSp["BFS — shortest path"]
dsu["Disjoint Set Union"]
graphDfs["DFS / Backtracking"]
graphBfs["BFS — state space"]
c2["NO — not a graph ▶ Chart 2"]:::link
gRoot -->|yes| tree
gRoot -->|no| c2
tree -->|yes| treeLevel
tree -->|no| directed
treeLevel -->|yes| treeBfs
treeLevel -->|no| treeBuild
treeBuild -->|yes| treeDc
treeBuild -->|no| treeDfs
directed -->|yes| topo
directed -->|no| shortestPath
shortestPath -->|yes| weighted
shortestPath -->|no| connectivity
weighted -->|yes| dijkstra
weighted -->|no| bfsSp
connectivity -->|yes| dsu
connectivity -->|no| graphSmall
graphSmall -->|yes| graphDfs
graphSmall -->|no| graphBfs
classDef q fill:#fff6e6,stroke:#b26a00,stroke-width:1.5px,color:#1a1a1a;
classDef leaf fill:#eef1fb,stroke:#3949ab,stroke-width:2px,color:#26317a,font-weight:bold;
classDef link fill:#eeeeee,stroke:#888,color:#333,stroke-dasharray:4 3;
class gRoot,tree,treeLevel,treeBuild,directed,shortestPath,weighted,connectivity,graphSmall q;
class treeBfs,treeDc,treeDfs,topo,dijkstra,bfsSp,dsu,graphDfs,graphBfs leaf;
click treeBfs "#L-tree-bfs"
click treeDc "#L-tree-dc"
click treeDfs "#L-tree-dfs"
click topo "#L-topo"
click dijkstra "#L-dijkstra"
click bfsSp "#L-bfs-sp"
click dsu "#L-dsu"
click graphDfs "#L-graph-dfs"
click graphBfs "#L-graph-bfs"
Chart 2 — Sorted / Search · Linked List · Hash · Intervals · Strings
flowchart TD
sortedSearch{"Sorted input or monotonic answer?"}
rangeQuery{"Dynamic range / order queries?"}
kth{"kth smallest / largest?"}
linkedList{"Linked list problem?"}
llPointer{"Fast/slow or fixed-gap pointers?"}
llMerge{"Merge many sorted lists?"}
hashTable{"Fast lookup, counting, grouping?"}
intervals{"Merge / insert / scan intervals?"}
partition{"In-place array partitioning?"}
stringSeg{"Split/match string with dictionary?"}
stringPattern{"Prefix or pattern matching?"}
fenwick["Fenwick / Segment Tree"]
binsearch["Binary Search"]
heapKth["Heap / Sorting"]
llTwo["Two Pointers"]
llHeap["Heap / Divide and Conquer"]
llManip["Linked List Manipulation"]
hash["Hash Table / Counting"]
intervalsLeaf["Sorting + Interval Scan"]
partitionLeaf["Two Pointers / Partitioning"]
trieMatch["Trie / String Matching"]
trieDp["Trie / DP / Memoization"]
c3["NO ▶ Chart 3"]:::link
sortedSearch -->|yes| rangeQuery
sortedSearch -->|no| kth
rangeQuery -->|yes| fenwick
rangeQuery -->|no| binsearch
kth -->|yes| heapKth
kth -->|no| linkedList
linkedList -->|yes| llPointer
linkedList -->|no| hashTable
llPointer -->|yes| llTwo
llPointer -->|no| llMerge
llMerge -->|yes| llHeap
llMerge -->|no| llManip
hashTable -->|yes| hash
hashTable -->|no| intervals
intervals -->|yes| intervalsLeaf
intervals -->|no| partition
partition -->|yes| partitionLeaf
partition -->|no| stringSeg
stringSeg -->|yes| stringPattern
stringSeg -->|no| c3
stringPattern -->|yes| trieMatch
stringPattern -->|no| trieDp
classDef q fill:#fff6e6,stroke:#b26a00,stroke-width:1.5px,color:#1a1a1a;
classDef leaf fill:#eef1fb,stroke:#3949ab,stroke-width:2px,color:#26317a,font-weight:bold;
classDef link fill:#eeeeee,stroke:#888,color:#333,stroke-dasharray:4 3;
class sortedSearch,rangeQuery,kth,linkedList,llPointer,llMerge,hashTable,intervals,partition,stringSeg,stringPattern q;
class fenwick,binsearch,heapKth,llTwo,llHeap,llManip,hash,intervalsLeaf,partitionLeaf,trieMatch,trieDp leaf;
click fenwick "#L-fenwick"
click binsearch "#L-binsearch"
click heapKth "#L-heap-kth"
click llTwo "#L-ll-two"
click llHeap "#L-ll-heap"
click llManip "#L-ll-manip"
click hash "#L-hash"
click intervalsLeaf "#L-intervals"
click partitionLeaf "#L-partition"
click trieMatch "#L-trie-match"
click trieDp "#L-trie-dp"
Chart 3 — Small Constraints · Sums · Subarrays · Max/Min
flowchart TD
smallConstraints{"Small constraints?"}
scEfficiency{"Is brute force enough?"}
bitmaskState{"Subset state?"}
sums{"Sum / additive problem?"}
subarrays{"Subarray / substring problem?"}
subWindow{"Maintain a valid window?"}
subStructure{"Need nearest greater/smaller bounds?"}
maxmin{"Compute a max / min?"}
mmBsHint{"Sorted index or monotonic answer?"}
mmStructure{"Need nearest greater/smaller bounds?"}
mmSubproblem{"Split into subproblems?"}
mmGreedy{"Greedy solution?"}
brute["Brute Force / Backtracking"]
bitmask["Bitmask DP"]
scDp["Dynamic Programming"]
prefix["Prefix Sums"]
window["Sliding Window"]
monoSub["Monotonic Stack"]
subDp["Dynamic Programming"]
bsAnswer["Binary Search on answer"]
monoMax["Monotonic Stack"]
maxDp["Dynamic Programming"]
greedy["Greedy Algorithms"]
c4["NO ▶ Chart 4"]:::link
smallConstraints -->|yes| scEfficiency
smallConstraints -->|no| sums
scEfficiency -->|yes| brute
scEfficiency -->|no| bitmaskState
bitmaskState -->|yes| bitmask
bitmaskState -->|no| scDp
sums -->|yes| prefix
sums -->|no| subarrays
subarrays -->|yes| subWindow
subarrays -->|no| maxmin
subWindow -->|yes| window
subWindow -->|no| subStructure
subStructure -->|yes| monoSub
subStructure -->|no| subDp
maxmin -->|yes| mmBsHint
maxmin -->|no| c4
mmBsHint -->|yes| bsAnswer
mmBsHint -->|no| mmStructure
mmStructure -->|yes| monoMax
mmStructure -->|no| mmSubproblem
mmSubproblem -->|yes| maxDp
mmSubproblem -->|no| mmGreedy
mmGreedy -->|yes| greedy
classDef q fill:#fff6e6,stroke:#b26a00,stroke-width:1.5px,color:#1a1a1a;
classDef leaf fill:#eef1fb,stroke:#3949ab,stroke-width:2px,color:#26317a,font-weight:bold;
classDef link fill:#eeeeee,stroke:#888,color:#333,stroke-dasharray:4 3;
class smallConstraints,scEfficiency,bitmaskState,sums,subarrays,subWindow,subStructure,maxmin,mmBsHint,mmStructure,mmSubproblem,mmGreedy q;
class brute,bitmask,scDp,prefix,window,monoSub,subDp,bsAnswer,monoMax,maxDp,greedy leaf;
click brute "#L-brute"
click bitmask "#L-bitmask"
click scDp "#L-sc-dp"
click prefix "#L-prefix"
click window "#L-window"
click monoSub "#L-mono-sub"
click subDp "#L-sub-dp"
click bsAnswer "#L-bs-answer"
click monoMax "#L-mono-max"
click maxDp "#L-max-dp"
click greedy "#L-greedy"
Chart 4 — Counting · Multiple Sequences · Find Indices · O(1) Memory
flowchart TD
counting{"Count number of ways?"}
countEff{"Is brute force enough?"}
seqCount{"Multiple sequences?"}
seqMono{"Monotonic condition?"}
seqSub{"Split into subproblems?"}
findIdx{"Find / enumerate indices?"}
findMono{"Monotonic condition?"}
constMem{"Need O(1) memory?"}
constMono{"Monotonic condition?"}
countBrute["Brute Force / Backtracking"]
countDp["Dynamic Programming"]
seqTwo["Two Pointers"]
seqDp["Dynamic Programming"]
idxTwo["Two Pointers"]
constTwo["Two Pointers"]
c5["NO ▶ Chart 5"]:::link
counting -->|yes| countEff
counting -->|no| seqCount
countEff -->|yes| countBrute
countEff -->|no| countDp
seqCount -->|yes| seqMono
seqCount -->|no| findIdx
seqMono -->|yes| seqTwo
seqMono -->|no| seqSub
seqSub -->|yes| seqDp
findIdx -->|yes| findMono
findIdx -->|no| constMem
findMono -->|yes| idxTwo
constMem -->|yes| constMono
constMem -->|no| c5
constMono -->|yes| constTwo
classDef q fill:#fff6e6,stroke:#b26a00,stroke-width:1.5px,color:#1a1a1a;
classDef leaf fill:#eef1fb,stroke:#3949ab,stroke-width:2px,color:#26317a,font-weight:bold;
classDef link fill:#eeeeee,stroke:#888,color:#333,stroke-dasharray:4 3;
class counting,countEff,seqCount,seqMono,seqSub,findIdx,findMono,constMem,constMono q;
class countBrute,countDp,seqTwo,seqDp,idxTwo,constTwo leaf;
click countBrute "#L-count-brute"
click countDp "#L-count-dp"
click seqTwo "#L-seq-two"
click seqDp "#L-seq-dp"
click idxTwo "#L-idx-two"
click constTwo "#L-const-two"
Chart 5 — Parsing · Design · Simulation · Math
flowchart TD
parseSym{"Need to parse symbols?"}
parseOpt{"Optimize / count valid spans?"}
designCheck{"Object with operation guarantees?"}
simCheck{"Direct transformation / simulation?"}
mathBit{"Math identities, powers, bit tricks?"}
numberTheory{"Number properties?"}
parseDp["Dynamic Programming"]
stackLeaf["Stack"]
design["Design + Data Structures"]
sim["Simulation / Basic DSA"]
numtheory["Number Theory"]
mathbit["Math / Bit Manipulation"]
specialized["Specialized / Advanced"]
parseSym -->|yes| parseOpt
parseSym -->|no| designCheck
parseOpt -->|yes| parseDp
parseOpt -->|no| stackLeaf
designCheck -->|yes| design
designCheck -->|no| simCheck
simCheck -->|yes| sim
simCheck -->|no| mathBit
mathBit -->|yes| numberTheory
mathBit -->|no| specialized
numberTheory -->|yes| numtheory
numberTheory -->|no| mathbit
classDef q fill:#fff6e6,stroke:#b26a00,stroke-width:1.5px,color:#1a1a1a;
classDef leaf fill:#eef1fb,stroke:#3949ab,stroke-width:2px,color:#26317a,font-weight:bold;
class parseSym,parseOpt,designCheck,simCheck,mathBit,numberTheory q;
class parseDp,stackLeaf,design,sim,numtheory,mathbit,specialized leaf;
click parseDp "#L-parse-dp"
click stackLeaf "#L-stack"
click design "#L-design"
click sim "#L-sim"
click numtheory "#L-numtheory"
click mathbit "#L-mathbit"
click specialized "#L-specialized"
Show the same flowchart as a plain text outline (fallback if the graph doesn't render)
- Is it a graph?
- YES ▸ Is it a tree?
- YES ▸ Level-order / shortest-level answer?
- YES ▸ BFS (level order)
- NO ▸ Count / generate many trees?
- YES ▸ Divide & Conquer / Tree DP
- NO ▸ DFS
- NO ▸ DAG-related?
- YES ▸ Topological Sort
- NO ▸ Shortest-path problem?
- YES ▸ Weighted graph?
- NO ▸ Connectivity problem?
- YES ▸ Disjoint Set Union
- NO ▸ Small constraints?
- YES ▸ DFS / Backtracking
- NO ▸ BFS (state space)
- YES ▸ Level-order / shortest-level answer?
- NO ▸ Sorted input or monotonic answer?
- YES ▸ Dynamic range / order queries?
- NO ▸ kth smallest / largest?
- YES ▸ Heap / Sorting
- NO ▸ Linked list problem?
- YES ▸ Fast/slow or fixed-gap pointers?
- YES ▸ Two Pointers
- NO ▸ Merge many sorted lists?
- YES ▸ Heap / Divide & Conquer
- NO ▸ Linked List Manipulation
- NO ▸ Fast lookup, counting, or grouping?
- YES ▸ Hash Table / Counting
- NO ▸ Merge / insert / scan intervals?
- YES ▸ Sorting + Interval Scan
- NO ▸ In-place array partitioning?
- YES ▸ Two Pointers / Partitioning
- NO ▸ Split/match string with dictionary?
- YES ▸ Prefix or pattern matching?
- NO ▸ Small constraints?
- YES ▸ Is brute force enough?
- YES ▸ Brute Force / Backtracking
- NO ▸ Subset state?
- YES ▸ Bitmask DP
- NO ▸ Dynamic Programming
- NO ▸ Sum / additive problem?
- YES ▸ Prefix Sums
- NO ▸ Subarray / substring problem?
- YES ▸ Maintain a valid window?
- YES ▸ Sliding Window
- NO ▸ Need nearest greater/smaller bounds?
- YES ▸ Monotonic Stack
- NO ▸ Dynamic Programming
- NO ▸ Compute a max / min?
- YES ▸ Sorted index or monotonic answer?
- YES ▸ Binary Search (on the answer)
- NO ▸ Need nearest greater/smaller bounds?
- YES ▸ Monotonic Stack
- NO ▸ Split into subproblems?
- YES ▸ Dynamic Programming
- NO ▸ Greedy solution?
- YES ▸ Greedy Algorithms
- NO ▸ Count number of ways?
- YES ▸ Is brute force enough?
- YES ▸ Brute Force / Backtracking
- NO ▸ Dynamic Programming
- NO ▸ Multiple sequences?
- YES ▸ Monotonic condition?
- YES ▸ Two Pointers
- NO ▸ Split into subproblems?
- YES ▸ Dynamic Programming
- NO ▸ Find / enumerate indices?
- YES ▸ Monotonic condition?
- YES ▸ Two Pointers
- NO ▸ Need O(1) memory?
- YES ▸ Monotonic condition?
- YES ▸ Two Pointers
- NO ▸ Need to parse symbols?
- YES ▸ Optimize / count valid spans?
- YES ▸ Dynamic Programming
- NO ▸ Stack
- NO ▸ Object with operation guarantees?
- YES ▸ Design + Supporting Data Structures
- NO ▸ Direct transformation / simulation?
- YES ▸ Simulation / Basic DSA
- NO ▸ Math identities, powers, or bit tricks?
- YES ▸ Number properties?
- YES ▸ Number Theory
- NO ▸ Math / Bit Manipulation
- NO ▸ Specialized / Advanced Pattern
- YES ▸ Number properties?
- YES ▸ Optimize / count valid spans?
- YES ▸ Monotonic condition?
- YES ▸ Monotonic condition?
- YES ▸ Monotonic condition?
- YES ▸ Is brute force enough?
- YES ▸ Sorted index or monotonic answer?
- YES ▸ Maintain a valid window?
- YES ▸ Is brute force enough?
- YES ▸ Fast/slow or fixed-gap pointers?
- YES ▸ Is it a tree?
Pattern Reference (one card per blue leaf)
2. Divide & Conquer / Tree DP
3. DFS — tree
4. Topological Sort
5. Dijkstra's Algorithm
6. BFS — unweighted shortest path
7. Disjoint Set Union
8. DFS / Backtracking
9. BFS — state space
10. Fenwick / Segment Tree
11. Binary Search
12. Heap / Sorting (kth)
13. Two Pointers — linked list
14. Heap — merge k lists
15. Linked List Manipulation
16. Hash Table / Counting
17. Sorting + Interval Scan
18. Two Pointers / Partitioning
19. Trie / String Matching
20. Trie / DP — segmentation
21. Brute Force / Backtracking
22. Bitmask DP
23. DP — small constraints
24. Prefix Sums
25. Sliding Window
26. Monotonic Stack — subarray
27. DP — subarray
28. Binary Search on the answer
29. Monotonic Stack — max/min
30. DP — max/min
31. Greedy Algorithms
32. Backtracking — counting
33. DP — counting
34. Two Pointers — two sequences
35. DP — two sequences
36. Two Pointers — find indices
37. Two Pointers — O(1) memory
38. DP — parsing
39. Stack — parsing
40. Design + Data Structures
41. Simulation / Basic DSA
42. Math / Bit Manipulation
43. Number Theory
44. Specialized / Advanced
Graphs & Trees
1 · BFS — Level Order, Shortest Path, State Space
Flowchart leaves: tree BFS shortest-path BFS state-space BFS
The idea
BFS explores a graph in rings outward from a source: all nodes at distance 1, then distance 2, and so on. Because it never visits a farther node before a nearer one, the first time BFS reaches a node in an unweighted graph, it has found the shortest path to it. A FIFO queue enforces this order; a visited set stops re-processing.
How it runs, step by step
- Put the source in the queue and mark it visited.
- Pop the front node. It is now "settled" — its distance is final.
- Push every unvisited neighbour, marking each visited at push time (not pop time — otherwise a node can enter the queue twice).
- Repeat until the queue empties. Track distance either per-node or by draining one whole level at a time.
🧰 API — Queue (FIFO) methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
Queue<Integer> q = new ArrayDeque<>();
q.offer(x); // add to back, O(1)
q.add(x); // add to back, throws if capacity fails (rare), O(1)
q.poll(); // remove & return front, null if empty, O(1)
q.remove(); // remove & return front, throws if empty, O(1)
q.peek(); // look at front without removing, null if empty, O(1)
q.element(); // look at front, throws if empty, O(1)
q.isEmpty(); // check empty, O(1)
q.size(); // number of elements, O(1)
for (int v : q) {} // iterate front-to-back, O(n)
#include <queue>
std::queue<int> q;
q.push(x); // add to back, O(1)
q.pop(); // remove front (no return value), O(1)
q.front(); // reference to front element, O(1)
q.back(); // reference to back element, O(1)
q.empty(); // check empty, O(1)
q.size(); // number of elements, O(1)
// std::queue is a container adapter — not directly iterable,
// use std::deque instead if you need to traverse it.
from collections import deque
q = deque()
q.append(x) # add to right (back), O(1)
q.popleft() # remove & return left (front), O(1)
q.appendleft(x) # add to left (front) — rare for a pure FIFO, O(1)
q.pop() # remove & return right (back) — rare for FIFO, O(1)
q[0] # peek front, O(1)
len(q) # size, O(1)
not q # check empty (True if empty)
for v in q: ... # iterate front-to-back, O(n)
const q: number[] = [];
q.push(x); // add to back, O(1)
q.shift(); // remove & return front — O(n)! NOT O(1), avoid at scale
q[0]; // peek front, O(1)
q.length === 0; // check empty, O(1)
q.length; // size, O(1)
for (const v of q) {} // iterate front-to-back, O(n)
// True O(1) FIFO workaround: index-pointer queue (avoids Array.shift's O(n) cost)
class Queue<T> {
private data: T[] = [];
private head = 0;
push(x: T): void { this.data.push(x); } // O(1)
pop(): T | undefined { // O(1) amortized
if (this.head >= this.data.length) return undefined;
return this.data[this.head++];
}
peek(): T | undefined { return this.data[this.head]; } // O(1)
get isEmpty(): boolean { return this.head >= this.data.length; } // O(1)
}
Brute force — repeated relaxation O(V·E)
Without a queue you could recompute "shortest so far" for every node by scanning all edges repeatedly until nothing improves (Bellman-Ford style). Correct, but wasteful on unweighted graphs where a single ordered sweep suffices.
int[] dist = new int[n];
Arrays.fill(dist, INF); dist[src] = 0;
boolean changed = true;
while (changed) { // up to V-1 passes
changed = false;
for (int[] e : edges) // relax every edge each pass
if (dist[e[0]] + 1 < dist[e[1]]) { dist[e[1]] = dist[e[0]] + 1; changed = true; }
}std::vector<int> dist(n, INF);
dist[src] = 0;
bool changed = true;
while (changed) { // up to V-1 passes
changed = false;
for (auto& e : edges) // relax every edge each pass
if (dist[e[0]] + 1 < dist[e[1]]) { dist[e[1]] = dist[e[0]] + 1; changed = true; }
}dist = [INF] * n
dist[src] = 0
changed = True
while changed: # up to V-1 passes
changed = False
for e in edges: # relax every edge each pass
if dist[e[0]] + 1 < dist[e[1]]:
dist[e[1]] = dist[e[0]] + 1
changed = Trueconst dist: number[] = new Array(n).fill(INF);
dist[src] = 0;
let changed = true;
while (changed) { // up to V-1 passes
changed = false;
for (const e of edges) // relax every edge each pass
if (dist[e[0]] + 1 < dist[e[1]]) { dist[e[1]] = dist[e[0]] + 1; changed = true; }
}Optimal — one BFS sweep O(V + E)
Template A · level-order (tree / grouped-by-depth)
root— top node of the tree; if it'snull/Nonethe tree is empty, so we return an empty list.q— the FIFO queue holding nodes waiting to be visited, front = next to process.sz— how many nodes are in the queue right now = the size of the current level. We freeze it before the inner loop so newly-added children don't get counted into this level.level— the values collected for the level we're currently draining.node— the node pulled from the front of the queue this iteration.res— the answer: a list of levels, each a list of node values top-to-bottom.
List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
int sz = q.size(); // freeze this level's size
List<Integer> level = new ArrayList<>();
for (int i = 0; i < sz; i++) {
TreeNode node = q.poll();
level.add(node.val);
if (node.left != null) q.offer(node.left);
if (node.right != null) q.offer(node.right);
}
res.add(level);
}
return res;
}std::vector<std::vector<int>> levelOrder(TreeNode* root) {
std::vector<std::vector<int>> res;
if (root == nullptr) return res;
std::queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int sz = q.size(); // freeze this level's size
std::vector<int> level;
for (int i = 0; i < sz; i++) {
TreeNode* node = q.front(); q.pop();
level.push_back(node->val);
if (node->left != nullptr) q.push(node->left);
if (node->right != nullptr) q.push(node->right);
}
res.push_back(level);
}
return res;
}def level_order(root):
res = []
if root is None:
return res
q = deque()
q.append(root)
while q:
sz = len(q) # freeze this level's size
level = []
for i in range(sz):
node = q.popleft()
level.append(node.val)
if node.left is not None:
q.append(node.left)
if node.right is not None:
q.append(node.right)
res.append(level)
return resfunction levelOrder(root: TreeNode | null): number[][] {
const res: number[][] = [];
if (root === null) return res;
const q: TreeNode[] = [root];
while (q.length > 0) {
const sz = q.length; // freeze this level's size
const level: number[] = [];
for (let i = 0; i < sz; i++) {
const node = q.shift()!;
level.push(node.val);
if (node.left !== null) q.push(node.left);
if (node.right !== null) q.push(node.right);
}
res.push(level);
}
return res;
}Template B · shortest path in an unweighted grid
grid— the maze as a 2-D array. Each cell is0= walkable (open) or1= blocked (wall).start— a 2-element array{row, col}: where you begin. (This was the cryptics.)target— a 2-element array{row, col}: where you want to reach. (This was the cryptict.)m,n— number of rows and columns, so we can bounds-check moves.DIR— the 4 moves you can make: down, up, right, left as{row-delta, col-delta}.seen— grid of booleans marking cells already queued, so we never revisit one.q— the BFS queue. Each entry is{row, col, distanceSoFar}.cur— the cell just popped:cur[0]=row,cur[1]=col,cur[2]=steps taken to get here.- Returns — fewest steps from
starttotarget, or-1if unreachable.
int shortestPath(int[][] grid, int[] start, int[] target) { // 0 = open, 1 = wall
int m = grid.length, n = grid[0].length;
int[][] DIR = {{1,0},{-1,0},{0,1},{0,-1}}; // down, up, right, left
boolean[][] seen = new boolean[m][n];
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{start[0], start[1], 0}); seen[start[0]][start[1]] = true;
while (!q.isEmpty()) {
int[] cur = q.poll(); // cur = {row, col, dist}
if (cur[0] == target[0] && cur[1] == target[1]) return cur[2];
for (int[] d : DIR) {
int r = cur[0]+d[0], col = cur[1]+d[1];
if (r>=0 && r<m && col>=0 && col<n && !seen[r][col] && grid[r][col]==0) {
seen[r][col] = true;
q.offer(new int[]{r, col, cur[2]+1});
}
}
}
return -1;
}int shortestPath(std::vector<std::vector<int>>& grid, std::vector<int>& start, std::vector<int>& target) { // 0 = open, 1 = wall
int m = grid.size(), n = grid[0].size();
int DIR[4][2] = {{1,0},{-1,0},{0,1},{0,-1}}; // down, up, right, left
std::vector<std::vector<bool>> seen(m, std::vector<bool>(n, false));
std::queue<std::array<int,3>> q;
q.push({start[0], start[1], 0}); seen[start[0]][start[1]] = true;
while (!q.empty()) {
auto cur = q.front(); q.pop(); // cur = {row, col, dist}
if (cur[0] == target[0] && cur[1] == target[1]) return cur[2];
for (auto& d : DIR) {
int r = cur[0]+d[0], col = cur[1]+d[1];
if (r>=0 && r<m && col>=0 && col<n && !seen[r][col] && grid[r][col]==0) {
seen[r][col] = true;
q.push({r, col, cur[2]+1});
}
}
}
return -1;
}def shortest_path(grid, start, target): # 0 = open, 1 = wall
m, n = len(grid), len(grid[0])
DIR = [(1,0),(-1,0),(0,1),(0,-1)] # down, up, right, left
seen = [[False] * n for _ in range(m)]
q = deque()
q.append((start[0], start[1], 0)); seen[start[0]][start[1]] = True
while q:
r0, c0, dist = q.popleft() # current row, col, steps so far
if r0 == target[0] and c0 == target[1]:
return dist
for dr, dc in DIR:
r, col = r0 + dr, c0 + dc
if 0 <= r < m and 0 <= col < n and not seen[r][col] and grid[r][col] == 0:
seen[r][col] = True
q.append((r, col, dist + 1))
return -1function shortestPath(grid: number[][], start: number[], target: number[]): number { // 0 = open, 1 = wall
const m = grid.length, n = grid[0].length;
const DIR = [[1,0],[-1,0],[0,1],[0,-1]]; // down, up, right, left
const seen: boolean[][] = Array.from({ length: m }, () => new Array(n).fill(false));
const q: number[][] = [[start[0], start[1], 0]];
seen[start[0]][start[1]] = true;
let head = 0;
while (head < q.length) {
const cur = q[head++]; // cur = [row, col, dist]
if (cur[0] === target[0] && cur[1] === target[1]) return cur[2];
for (const d of DIR) {
const r = cur[0]+d[0], col = cur[1]+d[1];
if (r>=0 && r<m && col>=0 && col<n && !seen[r][col] && grid[r][col]===0) {
seen[r][col] = true;
q.push([r, col, cur[2]+1]);
}
}
}
return -1;
}Template C · multi-source BFS (seed the queue with all sources)
grid— the input matrix; cells matching the "source" value (e.g. rotten oranges) seed the search.q— the BFS queue, pre-loaded with every source cell so they all expand outward at the same time.seen— marks cells already enqueued so a source (or reached cell) isn't added twice.- Returns — nothing itself — it just primes the queue before the standard ring-expansion BFS loop runs, which then returns the distance/steps.
// e.g. "rotting oranges" — every rotten cell starts at distance 0
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (grid[i][j] == 2) { q.offer(new int[]{i,j}); seen[i][j] = true; }
// then run the exact same ring-expansion loop// e.g. "rotting oranges" — every rotten cell starts at distance 0
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (grid[i][j] == 2) { q.push({i,j}); seen[i][j] = true; }
// then run the exact same ring-expansion loop# e.g. "rotting oranges" — every rotten cell starts at distance 0
for i in range(m):
for j in range(n):
if grid[i][j] == 2:
q.append((i, j))
seen[i][j] = True
# then run the exact same ring-expansion loop// e.g. "rotting oranges" — every rotten cell starts at distance 0
for (let i = 0; i < m; i++)
for (let j = 0; j < n; j++)
if (grid[i][j] === 2) { q.push([i, j]); seen[i][j] = true; }
// then run the exact same ring-expansion loopTemplate D · state-space BFS (states are strings/ints, edges are moves)
start— the initial state to search from.target— the state you're trying to reach.q— queue of states still to expand, processed one BFS "layer" at a time.seen— states already discovered, so the same state isn't re-queued.steps— how many moves have been taken so far; equals the layer/depth counter.cur— the state currently being expanded.nx— a neighboring state reachable fromcurvia one legal move.- Returns — the minimum number of moves from
starttotarget, or -1 iftargetis unreachable.
int bfsStates(String start, String target) {
Queue<String> q = new ArrayDeque<>();
Set<String> seen = new HashSet<>();
q.offer(start); seen.add(start);
int steps = 0;
while (!q.isEmpty()) {
for (int i = q.size(); i > 0; i--) {
String cur = q.poll();
if (cur.equals(target)) return steps;
for (String nx : neighbors(cur)) // generate legal moves
if (seen.add(nx)) q.offer(nx);
}
steps++;
}
return -1;
}int bfsStates(std::string start, std::string target) {
std::queue<std::string> q;
std::unordered_set<std::string> seen;
q.push(start); seen.insert(start);
int steps = 0;
while (!q.empty()) {
for (int i = q.size(); i > 0; i--) {
std::string cur = q.front(); q.pop();
if (cur == target) return steps;
for (const std::string& nx : neighbors(cur)) // generate legal moves
if (seen.insert(nx).second) q.push(nx);
}
steps++;
}
return -1;
}def bfs_states(start, target):
q = deque([start])
seen = {start}
steps = 0
while q:
for _ in range(len(q)):
cur = q.popleft()
if cur == target:
return steps
for nx in neighbors(cur): # generate legal moves
if nx not in seen:
seen.add(nx)
q.append(nx)
steps += 1
return -1function bfsStates(start: string, target: string): number {
const q: string[] = [start];
const seen = new Set<string>([start]);
let head = 0;
let steps = 0;
while (head < q.length) {
for (let sz = q.length - head; sz > 0; sz--) {
const cur = q[head++];
if (cur === target) return steps;
for (const nx of neighbors(cur)) { // generate legal moves
if (!seen.has(nx)) { seen.add(nx); q.push(nx); }
}
}
steps++;
}
return -1;
}Worked example — grid shortest path, traced
Grid (0 = open, 1 = wall), start S=(0,0), target T=(2,2):
0 0 1
1 0 0
1 1 00 0 1
1 0 0
1 1 00 0 1
1 0 0
1 1 00 0 1
1 0 0
1 1 0| Pop | Dist | New neighbours pushed | Queue after |
|---|---|---|---|
| (0,0) | 0 | (0,1)@1 [(1,0) is wall] | (0,1) |
| (0,1) | 1 | (1,1)@2 [(0,2) is wall] | (1,1) |
| (1,1) | 2 | (1,2)@3 | (1,2) |
| (1,2) | 3 | (2,2)@4 | (2,2) |
| (2,2) | 4 | = target → return 4 | — |
Answer 4. Notice each cell is settled exactly once and in non-decreasing distance order — that is why the first arrival is optimal.
Deque, push 0-weight to the front and 1-weight to the back (O(V+E), replaces Dijkstra here). Bidirectional BFS — search from both ends and meet in the middle, roughly square-rooting the frontier for word-ladder-style problems.
🧰 Quick reference templates
1. BFS — Level Order
Graph YES ▸ Tree YES ▸ Level-order answer YES
When the answer is grouped by depth (level averages, right-side view, zig-zag), process the tree one full level at a time. Snapshot queue.size() before draining so you know where each level ends.
List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
int sz = q.size(); // how many nodes on THIS level
List<Integer> level = new ArrayList<>();
for (int i = 0; i < sz; i++) {
TreeNode node = q.poll();
level.add(node.val);
if (node.left != null) q.offer(node.left);
if (node.right != null) q.offer(node.right);
}
res.add(level);
}
return res;
}vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if (root == nullptr) return res;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int sz = q.size(); // how many nodes on THIS level
vector<int> level;
for (int i = 0; i < sz; i++) {
TreeNode* node = q.front(); q.pop();
level.push_back(node->val);
if (node->left != nullptr) q.push(node->left);
if (node->right != nullptr) q.push(node->right);
}
res.push_back(level);
}
return res;
}def level_order(root):
res = []
if root is None:
return res
q = deque([root])
while q:
sz = len(q) # how many nodes on THIS level
level = []
for _ in range(sz):
node = q.popleft()
level.append(node.val)
if node.left is not None:
q.append(node.left)
if node.right is not None:
q.append(node.right)
res.append(level)
return resfunction levelOrder(root: TreeNode | null): number[][] {
const res: number[][] = [];
if (root === null) return res;
const q: TreeNode[] = [root];
while (q.length > 0) {
const sz = q.length; // how many nodes on THIS level
const level: number[] = [];
for (let i = 0; i < sz; i++) {
const node = q.shift()!;
level.push(node.val);
if (node.left !== null) q.push(node.left);
if (node.right !== null) q.push(node.right);
}
res.push(level);
}
return res;
}3 / (9, 20 / (15,7)). Level 0 drains 1 node → [3], enqueues 9,20. Level 1 drains 2 → [9,20], enqueues 15,7. Level 2 drains 2 → [15,7]. Result [[3],[9,20],[15,7]].6. BFS — Unweighted Shortest Path
... Shortest-path YES ▸ Weighted NO
Every edge costs 1, so plain BFS gives the fewest-steps answer. Grids are the classic case: enqueue neighbours, mark visited on enqueue (not dequeue) to avoid re-processing.
int shortestPath(int[][] grid, int[] start, int[] end) { // 0 = open, 1 = wall
int m = grid.length, n = grid[0].length;
int[][] dir = {{1,0},{-1,0},{0,1},{0,-1}};
boolean[][] seen = new boolean[m][n];
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{start[0], start[1], 0});
seen[start[0]][start[1]] = true;
while (!q.isEmpty()) {
int[] c = q.poll();
if (c[0] == end[0] && c[1] == end[1]) return c[2];
for (int[] d : dir) {
int r = c[0] + d[0], col = c[1] + d[1];
if (r >= 0 && r < m && col >= 0 && col < n
&& !seen[r][col] && grid[r][col] == 0) {
seen[r][col] = true;
q.offer(new int[]{r, col, c[2] + 1});
}
}
}
return -1;
}int shortestPath(vector<vector<int>>& grid, vector<int>& start, vector<int>& end) { // 0 = open, 1 = wall
int m = grid.size(), n = grid[0].size();
int dir[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
vector<vector<bool>> seen(m, vector<bool>(n, false));
queue<array<int,3>> q;
q.push({start[0], start[1], 0});
seen[start[0]][start[1]] = true;
while (!q.empty()) {
auto c = q.front(); q.pop();
if (c[0] == end[0] && c[1] == end[1]) return c[2];
for (auto& d : dir) {
int r = c[0] + d[0], col = c[1] + d[1];
if (r >= 0 && r < m && col >= 0 && col < n
&& !seen[r][col] && grid[r][col] == 0) {
seen[r][col] = true;
q.push({r, col, c[2] + 1});
}
}
}
return -1;
}def shortest_path(grid, start, end): # 0 = open, 1 = wall
m, n = len(grid), len(grid[0])
dirs = [(1, 0), (-1, 0), (0, 1), (0, -1)]
seen = [[False] * n for _ in range(m)]
q = deque([(start[0], start[1], 0)])
seen[start[0]][start[1]] = True
while q:
r0, c0, dist = q.popleft()
if r0 == end[0] and c0 == end[1]:
return dist
for dr, dc in dirs:
r, col = r0 + dr, c0 + dc
if 0 <= r < m and 0 <= col < n and not seen[r][col] and grid[r][col] == 0:
seen[r][col] = True
q.append((r, col, dist + 1))
return -1function shortestPath(grid: number[][], start: [number, number], end: [number, number]): number { // 0 = open, 1 = wall
const m = grid.length, n = grid[0].length;
const dir = [[1,0],[-1,0],[0,1],[0,-1]];
const seen: boolean[][] = Array.from({ length: m }, () => new Array(n).fill(false));
const q: [number, number, number][] = [[start[0], start[1], 0]];
seen[start[0]][start[1]] = true;
let head = 0;
while (head < q.length) {
const c = q[head++];
if (c[0] === end[0] && c[1] === end[1]) return c[2];
for (const d of dir) {
const r = c[0] + d[0], col = c[1] + d[1];
if (r >= 0 && r < m && col >= 0 && col < n
&& !seen[r][col] && grid[r][col] === 0) {
seen[r][col] = true;
q.push([r, col, c[2] + 1]);
}
}
}
return -1;
}(0,0), end (2,2). BFS radiates in rings; (2,2) first reached at distance 4 (Manhattan, no diagonals).9. BFS — State Space
... Connectivity NO ▸ Small constraints NO
When "nodes" are abstract states (lock combinations, board configs, word ladders), BFS from the start state finds the minimum number of transitions. Track visited states in a hash set.
int openLock(String[] deadends, String target) {
Set<String> dead = new HashSet<>(Arrays.asList(deadends));
if (dead.contains("0000")) return -1;
Set<String> visited = new HashSet<>();
Queue<String> q = new ArrayDeque<>();
q.offer("0000"); visited.add("0000");
int steps = 0;
while (!q.isEmpty()) {
for (int i = q.size(); i > 0; i--) {
String cur = q.poll();
if (cur.equals(target)) return steps;
for (int j = 0; j < 4; j++)
for (int d = -1; d <= 1; d += 2) {
char[] a = cur.toCharArray();
a[j] = (char)('0' + (a[j] - '0' + d + 10) % 10);
String nx = new String(a);
if (visited.add(nx) && !dead.contains(nx)) q.offer(nx);
}
}
steps++;
}
return -1;
}int openLock(vector<string>& deadends, string target) {
unordered_set<string> dead(deadends.begin(), deadends.end());
if (dead.count("0000")) return -1;
unordered_set<string> visited;
queue<string> q;
q.push("0000"); visited.insert("0000");
int steps = 0;
while (!q.empty()) {
for (int i = q.size(); i > 0; i--) {
string cur = q.front(); q.pop();
if (cur == target) return steps;
for (int j = 0; j < 4; j++)
for (int d = -1; d <= 1; d += 2) {
string a = cur;
a[j] = '0' + (a[j] - '0' + d + 10) % 10;
if (visited.insert(a).second && !dead.count(a)) q.push(a);
}
}
steps++;
}
return -1;
}def open_lock(deadends, target):
dead = set(deadends)
if "0000" in dead:
return -1
visited = {"0000"}
q = deque(["0000"])
steps = 0
while q:
for _ in range(len(q)):
cur = q.popleft()
if cur == target:
return steps
for j in range(4):
for d in (-1, 1):
a = list(cur)
a[j] = str((int(a[j]) + d + 10) % 10)
nx = "".join(a)
if nx not in visited and nx not in dead:
visited.add(nx)
q.append(nx)
steps += 1
return -1function openLock(deadends: string[], target: string): number {
const dead = new Set(deadends);
if (dead.has("0000")) return -1;
const visited = new Set<string>(["0000"]);
const q: string[] = ["0000"];
let steps = 0, head = 0;
while (head < q.length) {
for (let i = q.length - head; i > 0; i--) {
const cur = q[head++];
if (cur === target) return steps;
for (let j = 0; j < 4; j++)
for (let d = -1; d <= 1; d += 2) {
const a = cur.split("");
a[j] = String((Number(a[j]) + d + 10) % 10);
const nx = a.join("");
if (!visited.has(nx) && !dead.has(nx)) {
visited.add(nx);
q.push(nx);
}
}
}
steps++;
}
return -1;
}target = "0002", no deadends. Each wheel turn = 1 step; "0000"→"0001"→"0002" is 2 turns ⇒ 2.Sorted, Search & Range
🎯 LeetCode practice & worked example
11 · Graphs
Tests traversal and connectivity reasoning on grids and adjacency structures; reach for it whenever the problem involves neighbors, reachability, components, or dependency ordering and DFS/BFS/Union-Find naturally models it.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Number of Islands ★ | Medium | AmazonMetaGoogle | grid DFS/BFS flood fill, count components |
| 2 | Clone Graph ★ | Medium | MetaAmazonMicrosoft | DFS/BFS with visited hashmap to old->new node |
| 3 | Max Area of Island | Medium | AmazonGoogle | flood fill returning size, track max |
| 4 | Pacific Atlantic Water Flow | Medium | AmazonMeta | multi-source reverse BFS from both borders |
| 5 | Surrounded Regions | Medium | AmazonMeta | border-anchored DFS to mark safe cells, flip rest |
| 6 | Rotting Oranges ★ | Medium | AmazonMetaBloomberg | multi-source BFS with level = minutes |
| 7 | Walls and Gates | Medium | GoogleMeta | multi-source BFS from all gates |
| 8 | Course Schedule ★ | Medium | AmazonMetaGoogle | topological sort / cycle detection |
| 9 | Course Schedule II | Medium | AmazonMeta | Kahn's algorithm, emit order |
| 10 | Redundant Connection | Medium | GoogleAmazon | Union-Find, first edge that closes a cycle |
| 11 | Number of Connected Components in an Undirected Graph | Medium | MetaGoogle | Union-Find or DFS, count roots |
| 12 | Graph Valid Tree | Medium | GoogleMeta | Union-Find: n-1 edges and fully connected |
| 13 | Word Ladder ★ | Hard | AmazonMetaLinkedIn | BFS shortest path over implicit word graph |
Worked example — Course Schedule (LC 207, Medium)
Frequently asked at: Amazon, Meta, Google, Bloomberg.
Problem. There are numCourses courses labeled 0..numCourses-1. prerequisites[i] = [a, b] means you must take course b before course a. Return true if it is possible to finish all courses (i.e. the prerequisite graph has no cycle).
Idea. Model prerequisites as a directed graph and compute a topological order with Kahn's algorithm: track in-degree (number of unmet prerequisites) per course, seed a queue with all zero-in-degree courses, and repeatedly remove a course, decrementing its neighbors' in-degrees, pushing any that hit zero. If every course gets processed, there is no cycle and the schedule is feasible; if some remain stuck with positive in-degree, a cycle blocks them.
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
int[] inDegree = new int[numCourses];
for (int[] pre : prerequisites) {
int course = pre[0];
int prereq = pre[1];
graph.get(prereq).add(course); // prereq -> course
inDegree[course]++;
}
Queue<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
int processed = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
processed++;
for (int next : graph.get(course)) {
inDegree[next]--;
if (inDegree[next] == 0) {
queue.offer(next);
}
}
}
return processed == numCourses;
}
}
#include <vector>
#include <queue>
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses);
vector<int> inDegree(numCourses, 0);
for (auto& pre : prerequisites) {
int course = pre[0];
int prereq = pre[1];
graph[prereq].push_back(course); // prereq -> course
inDegree[course]++;
}
queue<int> q;
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
q.push(i);
}
}
int processed = 0;
while (!q.empty()) {
int course = q.front();
q.pop();
processed++;
for (int next : graph[course]) {
inDegree[next]--;
if (inDegree[next] == 0) {
q.push(next);
}
}
}
return processed == numCourses;
}
};from collections import deque
from typing import List
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
graph = [[] for _ in range(numCourses)]
inDegree = [0] * numCourses
for pre in prerequisites:
course = pre[0]
prereq = pre[1]
graph[prereq].append(course) # prereq -> course
inDegree[course] += 1
queue = deque()
for i in range(numCourses):
if inDegree[i] == 0:
queue.append(i)
processed = 0
while queue:
course = queue.popleft()
processed += 1
for next in graph[course]:
inDegree[next] -= 1
if inDegree[next] == 0:
queue.append(next)
return processed == numCoursesclass Solution {
canFinish(numCourses: number, prerequisites: number[][]): boolean {
const graph: number[][] = Array.from({ length: numCourses }, () => []);
const inDegree = new Array(numCourses).fill(0);
for (const pre of prerequisites) {
const course = pre[0];
const prereq = pre[1];
graph[prereq].push(course); // prereq -> course
inDegree[course]++;
}
const queue: number[] = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) {
queue.push(i);
}
}
let processed = 0;
while (queue.length > 0) {
const course = queue.shift()!;
processed++;
for (const next of graph[course]) {
inDegree[next]--;
if (inDegree[next] === 0) {
queue.push(next);
}
}
}
return processed === numCourses;
}
}Complexity. Time O(V + E) Space O(V + E) — each course and each edge is visited exactly once during graph build and BFS.
Walkthrough. numCourses=4, prerequisites=[[1,0],[2,0],[3,1],[3,2]]. Graph: 0->{1,2}, 1->{3}, 2->{3}. inDegree=[0,1,1,2]. Queue starts with [0]. Pop 0 (processed=1): decrement 1→0 (enqueue), decrement 2→0 (enqueue). Pop 1 (processed=2): decrement 3→1. Pop 2 (processed=3): decrement 3→0 (enqueue). Pop 3 (processed=4). processed==4==numCourses → return true.
🔬 Deeper dive & dry run
Why it works. Kahn's algorithm repeatedly removes nodes whose in-degree has dropped to 0 — courses with no remaining unmet prerequisites. If every node can eventually be removed this way, a full topological order exists and the graph is acyclic, because any node stuck in a cycle would never have all its incoming edges consumed and would permanently sit at in-degree ≥ 1.
When to reach for it. Prerequisite / dependency / task-ordering problems — the signal is a directed graph plus the question "can everything finish" or "give a valid order," which is exactly cycle detection plus topological sort.
Complexity. O(V + E) time — each vertex is enqueued/dequeued once and each edge is relaxed once; O(V + E) space for the adjacency list, in-degree array, and queue.
Pitfalls & variations. DFS-based cycle detection with only a single "visited" set fails to distinguish a back edge (real cycle) from a cross edge (fine) — a 3-color unvisited/in-progress/done scheme is required instead. Disconnected components must each independently bottom out at in-degree 0. The real correctness check is comparing the total processed count to numCourses at the end, not just that the queue eventually emptied.
Trace input: numCourses = 5, prerequisites = [[1,0],[2,0],[3,1],[3,2],[4,3]] (edges 0→1, 0→2, 1→3, 2→3, 3→4) — can all 5 courses be finished?
| Step | Popped from queue | Processed so far | In-degree updates | Queue after |
|---|---|---|---|---|
| 0 | init | [] | indegree = [0,1,1,2,1] | [0] |
| 1 | 0 | [0] | course1: 1→0 (push); course2: 1→0 (push) | [1,2] |
| 2 | 1 | [0,1] | course3: 2→1 (not pushed) | [2] |
| 3 | 2 | [0,1,2] | course3: 1→0 (push) | [3] |
| 4 | 3 | [0,1,2,3] | course4: 1→0 (push) | [4] |
| 5 | 4 | [0,1,2,3,4] | no outgoing edges | [] |
Result: processed.length == 5 == numCourses → true. The diamond dependency (0→{1,2}→3→4) resolves cleanly because every node reaches in-degree 0; a cycle anywhere would leave the queue empty early with processed.length < numCourses.
2 · DFS & Flood Fill
Flowchart leaves: tree DFS graph DFS
The idea
DFS goes as deep as possible before backing up. It answers "is there a path / reach everything connected / does a structure hold recursively" and is the natural fit for trees. Two forms: recursive (uses the call stack) and iterative (an explicit stack). On a graph you need a visited set; on a tree you don't (no cycles).
How it runs, step by step
- Visit the current node (mark it visited in a graph).
- For each neighbour/child, if unvisited, recurse into it.
- Combine children's results on the way back up (post-order) — this is where tree answers like height, sum, and validity are assembled.
Brute force — recompute reachability per query O(V·(V+E))
Counting connected regions by re-running a fresh search for every cell you have not yet coloured is the naive approach; the smarter version simply never revisits a coloured cell, giving one linear pass.
Optimal — single traversal O(V + E)
Template A · tree post-order aggregation (depth / diameter)
node— the current tree node being visited.l, r— the heights of the left and right subtrees, returned by the recursive calls.diameter— a running best-so-far value (kept outside the function) for the longest path seen between any two nodes.- Returns — the height of the subtree rooted at
node, reported up to the parent; the real answer,diameter, is accumulated as a side effect while recursing.
int diameter = 0;
int height(TreeNode node) {
if (node == null) return 0;
int l = height(node.left), r = height(node.right);
diameter = Math.max(diameter, l + r); // longest path THROUGH node
return 1 + Math.max(l, r); // height reported to parent
}int diameter = 0;
int height(TreeNode* node) {
if (node == nullptr) return 0;
int l = height(node->left), r = height(node->right);
diameter = std::max(diameter, l + r); // longest path THROUGH node
return 1 + std::max(l, r); // height reported to parent
}diameter = 0
def height(node):
global diameter
if node is None:
return 0
l, r = height(node.left), height(node.right)
diameter = max(diameter, l + r) # longest path THROUGH node
return 1 + max(l, r) # height reported to parentlet diameter = 0;
function height(node: TreeNode | null): number {
if (node === null) return 0;
const l = height(node.left), r = height(node.right);
diameter = Math.max(diameter, l + r); // longest path THROUGH node
return 1 + Math.max(l, r); // height reported to parent
}Template B · grid flood fill (count islands)
g— the grid of cells, where'1'= land and'0'= water.count— the number of islands found so far.sink(g, i, j)— recursively "drowns" every land cell connected to(i, j)by flipping it to'0', so the outer scan never recounts it.- Returns —
numIslandsreturns the total island count;sinkreturns nothing — it just mutates the grid in place.
int numIslands(char[][] g) {
int m = g.length, n = g[0].length, count = 0;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (g[i][j] == '1') { count++; sink(g, i, j); }
return count;
}
void sink(char[][] g, int i, int j) {
if (i<0 || i>=g.length || j<0 || j>=g[0].length || g[i][j] != '1') return;
g[i][j] = '0'; // mark visited in place
sink(g,i+1,j); sink(g,i-1,j); sink(g,i,j+1); sink(g,i,j-1);
}int numIslands(std::vector<std::vector<char>>& g) {
int m = g.size(), n = g[0].size(), count = 0;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (g[i][j] == '1') { count++; sink(g, i, j); }
return count;
}
void sink(std::vector<std::vector<char>>& g, int i, int j) {
if (i<0 || i>=(int)g.size() || j<0 || j>=(int)g[0].size() || g[i][j] != '1') return;
g[i][j] = '0'; // mark visited in place
sink(g,i+1,j); sink(g,i-1,j); sink(g,i,j+1); sink(g,i,j-1);
}def num_islands(g):
m, n, count = len(g), len(g[0]), 0
for i in range(m):
for j in range(n):
if g[i][j] == '1':
count += 1
sink(g, i, j)
return count
def sink(g, i, j):
if i < 0 or i >= len(g) or j < 0 or j >= len(g[0]) or g[i][j] != '1':
return
g[i][j] = '0' # mark visited in place
sink(g, i+1, j); sink(g, i-1, j); sink(g, i, j+1); sink(g, i, j-1)function numIslands(g: string[][]): number {
const m = g.length, n = g[0].length; let count = 0;
for (let i = 0; i < m; i++)
for (let j = 0; j < n; j++)
if (g[i][j] === '1') { count++; sink(g, i, j); }
return count;
}
function sink(g: string[][], i: number, j: number): void {
if (i<0 || i>=g.length || j<0 || j>=g[0].length || g[i][j] !== '1') return;
g[i][j] = '0'; // mark visited in place
sink(g,i+1,j); sink(g,i-1,j); sink(g,i,j+1); sink(g,i,j-1);
}Template C · iterative DFS (avoid stack overflow on deep graphs)
src— the node to start the traversal from.adj— the adjacency list;adj[u]lists the neighbors of nodeu.stack— an explicit stack standing in for the recursion call stack, so deep graphs can't overflow it.seen— marks nodes already visited so they aren't pushed twice.u— the node just popped off the stack (currently being processed).v— a neighbor ofubeing considered for visiting.- Returns — nothing here; the snippet just walks every reachable node, marking
seen(swap in your own logic to collect results).
Deque<Integer> stack = new ArrayDeque<>();
stack.push(src); seen[src] = true;
while (!stack.isEmpty()) {
int u = stack.pop();
for (int v : adj[u]) if (!seen[v]) { seen[v] = true; stack.push(v); }
}std::stack<int> stack;
stack.push(src); seen[src] = true;
while (!stack.empty()) {
int u = stack.top(); stack.pop();
for (int v : adj[u]) if (!seen[v]) { seen[v] = true; stack.push(v); }
}stack = [src]
seen[src] = True
while stack:
u = stack.pop()
for v in adj[u]:
if not seen[v]:
seen[v] = True
stack.append(v)const stack: number[] = [src];
seen[src] = true;
while (stack.length > 0) {
const u = stack.pop()!;
for (const v of adj[u]) if (!seen[v]) { seen[v] = true; stack.push(v); }
}Worked example — count islands, traced
1 1 0
0 1 0
0 0 11 1 0
0 1 0
0 0 11 1 0
0 1 0
0 0 11 1 0
0 1 0
0 0 1- (0,0) is land → count=1, flood sinks (0,0),(0,1),(1,1) into water.
- Scan continues; every cell of that blob is now '0', so it is not recounted.
- (2,2) is land → count=2, flood sinks just itself.
Answer 2 islands. Sinking in place is the trick that turns an O(V²) idea into one linear pass.
visited gives infinite loops on cycles.🔬 Deeper dive & dry run
Why it works. Flood fill treats connectivity as an equivalence relation: DFS from an unvisited land cell marks every cell reachable via land-adjacency as belonging to the same component before the outer scan moves on. The invariant is "once a component is entered, it is fully consumed" — so the outer loop's count of DFS launches equals exactly the number of connected components, no double counting possible because every visited cell is permanently marked.
When to reach for it. "Count islands / regions / connected components", "number of provinces/clusters", "fill/paint connected area", or any grid/graph problem asking to group nodes by reachability rather than by shortest distance (that would be BFS instead, though either works for pure counting).
Complexity. O(rows × cols) time and space — each cell is visited once; the explicit stack (or call stack for recursion) holds at most O(rows × cols) frontier cells in the worst case (e.g. a spiral-shaped island).
Pitfalls & variations. Recursive DFS on a huge grid can blow the call stack — switch to an explicit stack for grids that might exceed a few thousand cells. Forgetting to mark a cell visited immediately when pushed (vs. when popped) re-pushes duplicates. Variants: 8-directional connectivity (add diagonals), counting island area/perimeter instead of count, or flood-filling with a replacement value (classic "Flood Fill" LC problem) instead of a visited set.
Trace input: grid = [[1,1,0,0],[1,0,0,1],[0,0,1,1],[0,0,0,1]] (1 = land, 0 = water) — count connected land components (islands), 4-directional.
| Step | Action | Stack | Visited (new) | Islands | Note |
|---|---|---|---|---|---|
| 0 | outer scan hits (0,0)=1, unvisited | [(0,0)] | — | 0 | launch DFS #1 |
| 1 | pop (0,0), push land neighbors | [(1,0),(0,1)] | (0,0) | 0 | down & right are land |
| 2 | pop (0,1) | [(1,0)] | (0,1) | 0 | (0,2) and (1,1) are water |
| 3 | pop (1,0) | [] | (1,0) | 0 | (1,1) water, (2,0) water → stack empty |
| 4 | island #1 closed, size 3 | [] | — | 1 | {(0,0),(0,1),(1,0)} |
| 5 | outer scan hits (1,3)=1, unvisited | [(1,3)] | — | 1 | launch DFS #2 |
| 6 | pop (1,3), push down neighbor | [(2,3)] | (1,3) | 1 | (0,3) water, left (1,2) water |
| 7 | pop (2,3), push down & left | [(3,3),(2,2)] | (2,3) | 1 | both (3,3) and (2,2) are land |
| 8 | pop (2,2) | [(3,3)] | (2,2) | 1 | up/down/left all water |
| 9 | pop (3,3) | [] | (3,3) | 1 | left (3,2) is water → stack empty |
| 10 | island #2 closed, size 4 | [] | — | 2 | {(1,3),(2,3),(2,2),(3,3)} |
Result: 2 islands. The trace shows each DFS launch fully draining its stack before the outer scan resumes — that "drain before moving on" behavior is what makes the launch count equal the component count.
🧰 Quick reference templates
3. DFS — Tree
Graph YES ▸ Tree YES ▸ Level-order NO ▸ Count/generate NO
Default tree pattern: recurse to children, combine their results on the way back up (post-order). Depth, diameter, path sums, validation all fit this shape.
int maxDepth(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// Same shape, returns "height" but tracks a global answer — diameter:
int best = 0;
int height(TreeNode node) {
if (node == null) return 0;
int l = height(node.left), r = height(node.right);
best = Math.max(best, l + r); // longest path THROUGH this node
return 1 + Math.max(l, r);
}int maxDepth(TreeNode* root) {
if (root == nullptr) return 0;
return 1 + max(maxDepth(root->left), maxDepth(root->right));
}
// Same shape, returns "height" but tracks a global answer — diameter:
int best = 0;
int height(TreeNode* node) {
if (node == nullptr) return 0;
int l = height(node->left), r = height(node->right);
best = max(best, l + r); // longest path THROUGH this node
return 1 + max(l, r);
}def max_depth(root):
if root is None:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
# Same shape, returns "height" but tracks a global answer — diameter:
best = 0
def height(node):
global best
if node is None:
return 0
l, r = height(node.left), height(node.right)
best = max(best, l + r) # longest path THROUGH this node
return 1 + max(l, r)function maxDepth(root: TreeNode | null): number {
if (root === null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// Same shape, returns "height" but tracks a global answer — diameter:
let best = 0;
function height(node: TreeNode | null): number {
if (node === null) return 0;
const l = height(node.left), r = height(node.right);
best = Math.max(best, l + r); // longest path THROUGH this node
return 1 + Math.max(l, r);
}1 / (2 / (4,5), 3). height(4)=height(5)=1, so at node 2, l+r=2 ⇒ diameter 2 edges. maxDepth = 3.8. DFS / Backtracking
... Connectivity NO ▸ Small constraints YES
Small graphs let you explore exhaustively: enumerate all paths, flood-fill regions, or backtrack over choices. Add to the path before recursing, remove after (undo) to reuse the buffer.
List<List<Integer>> allPaths(int[][] graph) { // DAG, all paths node 0 → n-1
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
path.add(0);
dfs(graph, 0, path, res);
return res;
}
void dfs(int[][] g, int node, List<Integer> path, List<List<Integer>> res) {
if (node == g.length - 1) { res.add(new ArrayList<>(path)); return; }
for (int next : g[node]) {
path.add(next);
dfs(g, next, path, res);
path.remove(path.size() - 1); // undo choice
}
}vector<vector<int>> allPaths(vector<vector<int>>& graph) { // DAG, all paths node 0 → n-1
vector<vector<int>> res;
vector<int> path;
path.push_back(0);
dfs(graph, 0, path, res);
return res;
}
void dfs(vector<vector<int>>& g, int node, vector<int>& path, vector<vector<int>>& res) {
if (node == (int)g.size() - 1) { res.push_back(path); return; }
for (int next : g[node]) {
path.push_back(next);
dfs(g, next, path, res);
path.pop_back(); // undo choice
}
}def all_paths(graph): # DAG, all paths node 0 → n-1
res = []
path = [0]
dfs(graph, 0, path, res)
return res
def dfs(g, node, path, res):
if node == len(g) - 1:
res.append(list(path))
return
for nxt in g[node]:
path.append(nxt)
dfs(g, nxt, path, res)
path.pop() # undo choicefunction allPaths(graph: number[][]): number[][] { // DAG, all paths node 0 → n-1
const res: number[][] = [];
const path: number[] = [0];
dfs(graph, 0, path, res);
return res;
}
function dfs(g: number[][], node: number, path: number[], res: number[][]): void {
if (node === g.length - 1) { res.push([...path]); return; }
for (const next of g[node]) {
path.push(next);
dfs(g, next, path, res);
path.pop(); // undo choice
}
}graph = [[1,2],[3],[3],[]]. Paths 0→1→3 and 0→2→3 ⇒ [[0,1,3],[0,2,3]].🎯 LeetCode practice & worked example
7 · Trees
Tests recursive decomposition over hierarchical data — the tell is any question about depth, paths, ancestors, or "given a binary tree"; reach for DFS/BFS templates and passing state (bounds, counts) down or aggregating results up the recursion.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Invert Binary Tree ★ | Easy | GoogleAmazonMicrosoft | post-order swap children |
| 2 | Maximum Depth of Binary Tree ★ | Easy | AmazonAppleLinkedIn | DFS depth aggregation |
| 3 | Diameter of Binary Tree | Easy | AmazonMetaMicrosoft | height DFS with running max |
| 4 | Balanced Binary Tree | Easy | AmazonAdobeBloomberg | bottom-up height with early bail-out |
| 5 | Same Tree | Easy | AmazonBloomberg | paired recursive DFS |
| 6 | Subtree of Another Tree | Easy | AmazonMeta | same-tree check at every node |
| 7 | Lowest Common Ancestor of a Binary Search Tree ★ | Medium | AmazonMicrosoftFacebook | BST value comparison to steer descent |
| 8 | Binary Tree Level Order Traversal ★ | Medium | AmazonMicrosoftLinkedIn | BFS with queue, level snapshot |
| 9 | Binary Tree Right Side View | Medium | AmazonMetaBloomberg | BFS, keep last node per level |
| 10 | Count Good Nodes in Binary Tree | Medium | AmazonAdobe | DFS carrying max-so-far |
| 11 | Validate Binary Search Tree ★ | Medium | AmazonMicrosoftAppleBloomberg | recursive (low, high) bound propagation |
| 12 | Kth Smallest Element in a BST ★ | Medium | AmazonGoogleUber | iterative in-order traversal, count down |
| 13 | Construct Binary Tree from Preorder and Inorder Traversal ★ | Medium | AmazonMicrosoftMeta | index map + recursive split on root |
| 14 | Binary Tree Maximum Path Sum ★ | Hard | AmazonMicrosoftGoogle | post-order max single-branch gain + global max |
| 15 | Serialize and Deserialize Binary Tree ★ | Hard | AmazonGoogleMeta | pre-order encode with null markers |
Worked example — Validate Binary Search Tree (LC 98, Medium)
Frequently asked at: Amazon, Microsoft, Apple, Bloomberg.
Problem. Given the root of a binary tree, determine whether it is a valid binary search tree — for every node, all values in its left subtree must be strictly less than the node's value and all values in its right subtree must be strictly greater.
Idea. Checking only left.val < node.val < right.val at each node is insufficient — a node deep in the left subtree could still violate the ancestor's bound. Instead thread a valid (low, high) range down through the recursion: each node must fall strictly inside the range inherited from its ancestors, and it narrows that range for its own children.
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) { this.val = val; }
}
class Solution {
public boolean isValidBST(TreeNode root) {
return valid(root, null, null);
}
private boolean valid(TreeNode node, Integer low, Integer high) {
if (node == null) return true;
if ((low != null && node.val <= low) || (high != null && node.val >= high)) {
return false;
}
return valid(node.left, low, node.val) && valid(node.right, node.val, high);
}
}
struct TreeNode {
int val;
TreeNode *left, *right;
TreeNode(int val) : val(val), left(nullptr), right(nullptr) {}
};
class Solution {
public:
bool isValidBST(TreeNode* root) {
return valid(root, std::nullopt, std::nullopt);
}
private:
bool valid(TreeNode* node, std::optional<int> low, std::optional<int> high) {
if (node == nullptr) return true;
if ((low.has_value() && node->val <= low.value()) || (high.has_value() && node->val >= high.value())) {
return false;
}
return valid(node->left, low, node->val) && valid(node->right, node->val, high);
}
};class TreeNode:
def __init__(self, val=0):
self.val = val
self.left = None
self.right = None
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
return self._valid(root, None, None)
def _valid(self, node, low, high):
if node is None:
return True
if (low is not None and node.val <= low) or (high is not None and node.val >= high):
return False
return self._valid(node.left, low, node.val) and self._valid(node.right, node.val, high)class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val: number) {
this.val = val;
this.left = null;
this.right = null;
}
}
class Solution {
isValidBST(root: TreeNode | null): boolean {
return this.valid(root, null, null);
}
private valid(node: TreeNode | null, low: number | null, high: number | null): boolean {
if (node === null) return true;
if ((low !== null && node.val <= low) || (high !== null && node.val >= high)) {
return false;
}
return this.valid(node.left, low, node.val) && this.valid(node.right, node.val, high);
}
}Complexity. Time O(n) Space O(h) — every node is visited once; space is the recursion stack, proportional to tree height h.
Walkthrough. Input tree: root 5, left child 1, right child 4 (whose children are 3 and 6).
| Call | Bounds (low, high) | Result |
|---|---|---|
| valid(5, null, null) | (-∞, +∞) | 5 in range, recurse both sides |
| valid(1, null, 5) | (-∞, 5) | 1 < 5, valid leaf |
| valid(4, 5, null) | (5, +∞) | 4 ≤ 5 → false |
| Overall | — | returns false — the right subtree of 5 must contain only values > 5, but 4 breaks that |
🔬 Deeper dive & dry run
Why it works. A tree is a valid BST iff every node's value lies strictly inside the (low, high) bound inherited from all its ancestors, not just its immediate parent. A local check like node.val > node.left.val is insufficient because a node several levels down can violate a constraint set far above it; passing tightened bounds down the recursion captures that global constraint in O(1) work per node.
When to reach for it. Tree problems where a tempting local sibling/parent comparison is actually wrong because the real constraint spans multiple ancestor levels — BST validation is the classic example; similar bound-propagation shows up in range/coloring problems on trees.
Complexity. O(n) time — every node is visited once; O(h) space for the recursion or explicit stack, where h is the tree height.
Pitfalls & variations. Checking only node.val vs. its direct children's values misses violations from grandparents. Using <= instead of < silently allows duplicate values a strict BST forbids. Not short-circuiting on the first violation wastes work scanning subtrees that can no longer change the answer.
Trace input: root = 5, left = 3 (left = 1, right = 6), right = 8 — is this tree a valid binary search tree?
| Step | Popped call (node, low, high) | Check | Result | Pushed children |
|---|---|---|---|---|
| 0 | init | — | — | stack = [(5, -∞, +∞)] |
| 1 | (5, -∞, +∞) | -∞ < 5 < +∞ | true | push (8, 5, +∞), push (3, -∞, 5) |
| 2 | (3, -∞, 5) | -∞ < 3 < 5 | true | push (6, 3, 5), push (1, -∞, 3) |
| 3 | (1, -∞, 3) | -∞ < 1 < 3 | true (leaf) | nothing to push |
| 4 | (6, 3, 5) | 3 < 6 < 5 → 6 < 5 is false | INVALID — stop | (8, 5, +∞) never popped |
Result: Returns false. Node 6 only violates the tree once it inherits the tight upper bound 5 handed down from the root two levels above — a purely local check against its immediate parent (6 > 3) would have wrongly passed it.
3 · Backtracking
Flowchart leaves: brute force / backtracking (small constraints) counting via backtracking graph DFS / backtracking
The idea
When n is small (roughly ≤ 12–20) and you must enumerate or count all valid configurations, build a candidate one choice at a time. At each step: choose, recurse, un-choose (undo). Prune branches that can't lead to a solution as early as possible — pruning is what separates a usable backtracker from a timeout.
The universal shape
s— the state (partial candidate) being built up one choice at a time.c— one candidate choice, drawn fromchoicesFrom(s), being tried at this step.isComplete(s)— tells whethersis a full, valid answer worth recording.valid(c, s)— the pruning check — is choiceceven legal given the state built so far.- Returns — nothing directly; complete candidates are captured via
record(s), and every applied choice is undone sosis clean for the next branch.
void backtrack(State s) {
if (isComplete(s)) { record(s); return; }
for (Choice c : choicesFrom(s)) {
if (!valid(c, s)) continue; // prune
apply(c, s); // choose
backtrack(s); // explore
undo(c, s); // un-choose
}
}void backtrack(State& s) {
if (isComplete(s)) { record(s); return; }
for (Choice& c : choicesFrom(s)) {
if (!valid(c, s)) continue; // prune
apply(c, s); // choose
backtrack(s); // explore
undo(c, s); // un-choose
}
}def backtrack(s):
if is_complete(s):
record(s)
return
for c in choices_from(s):
if not valid(c, s):
continue # prune
apply(c, s) # choose
backtrack(s) # explore
undo(c, s) # un-choosefunction backtrack(s: State): void {
if (isComplete(s)) { record(s); return; }
for (const c of choicesFrom(s)) {
if (!valid(c, s)) continue; // prune
apply(c, s); // choose
backtrack(s); // explore
undo(c, s); // un-choose
}
}Brute force = backtracking without pruning exponential
Generating every one of the nⁿ / n! / 2ⁿ candidates and testing validity at the end. Backtracking is the same search but prunes invalid partial states before they explode.
Templates
A · subsets (each element in / out) O(2ⁿ · n)
nums— the input array whose elements are each either included or excluded.i— the index of the element currently being decided (skip vs. take).cur— the subset built so far along this branch.out— the accumulator collecting every complete subset onceireaches the end.- Returns — nothing directly; every subset is appended into
outas a side effect.
void subsets(int[] nums, int i, List<Integer> cur, List<List<Integer>> out) {
if (i == nums.length) { out.add(new ArrayList<>(cur)); return; }
subsets(nums, i+1, cur, out); // skip nums[i]
cur.add(nums[i]);
subsets(nums, i+1, cur, out); // take nums[i]
cur.remove(cur.size()-1);
}void subsets(std::vector<int>& nums, int i, std::vector<int>& cur, std::vector<std::vector<int>>& out) {
if (i == (int)nums.size()) { out.push_back(cur); return; }
subsets(nums, i+1, cur, out); // skip nums[i]
cur.push_back(nums[i]);
subsets(nums, i+1, cur, out); // take nums[i]
cur.pop_back();
}def subsets(nums, i, cur, out):
if i == len(nums):
out.append(list(cur))
return
subsets(nums, i+1, cur, out) # skip nums[i]
cur.append(nums[i])
subsets(nums, i+1, cur, out) # take nums[i]
cur.pop()function subsets(nums: number[], i: number, cur: number[], out: number[][]): void {
if (i === nums.length) { out.push([...cur]); return; }
subsets(nums, i+1, cur, out); // skip nums[i]
cur.push(nums[i]);
subsets(nums, i+1, cur, out); // take nums[i]
cur.pop();
}B · permutations O(n! · n)
nums— the input array being permuted.cur— the permutation built so far along this branch.used— marks which elements ofnumsare already placed incur, so they aren't reused.out— the accumulator collecting every complete permutation.- Returns — nothing directly; complete permutations are appended into
out.
void permute(int[] nums, List<Integer> cur, boolean[] used, List<List<Integer>> out){
if (cur.size()==nums.length){ out.add(new ArrayList<>(cur)); return; }
for (int i=0;i<nums.length;i++){
if (used[i]) continue;
used[i]=true; cur.add(nums[i]);
permute(nums, cur, used, out);
cur.remove(cur.size()-1); used[i]=false;
}
}void permute(std::vector<int>& nums, std::vector<int>& cur, std::vector<bool>& used, std::vector<std::vector<int>>& out){
if (cur.size()==nums.size()){ out.push_back(cur); return; }
for (int i=0;i<(int)nums.size();i++){
if (used[i]) continue;
used[i]=true; cur.push_back(nums[i]);
permute(nums, cur, used, out);
cur.pop_back(); used[i]=false;
}
}def permute(nums, cur, used, out):
if len(cur) == len(nums):
out.append(list(cur))
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
cur.append(nums[i])
permute(nums, cur, used, out)
cur.pop()
used[i] = Falsefunction permute(nums: number[], cur: number[], used: boolean[], out: number[][]): void {
if (cur.length === nums.length) { out.push([...cur]); return; }
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true; cur.push(nums[i]);
permute(nums, cur, used, out);
cur.pop(); used[i] = false;
}
}C · combinations / N-Queens counting with bitmask pruning
n— the board size (also the number of queens to place).row— the row currently being filled with a queen.cols, diag, anti— bitmasks marking which columns, "\" diagonals, and "/" diagonals are already under attack from queens placed in earlier rows.free— a bitmask of columns still safe to place a queen in for this row.bit— one specific safe column, extracted fromfreeas its lowest set bit.- Returns — the number of ways to complete a valid board from
rowonward; the outer call returns the total count of N-Queens solutions.
int totalNQueens(int n){ return place(n,0,0,0,0); }
int place(int n,int row,int cols,int diag,int anti){
if (row==n) return 1;
int free = ((1<<n)-1) & ~(cols|diag|anti); // columns still safe
int count=0;
while (free!=0){
int bit = free & -free; free -= bit; // pick lowest safe column
count += place(n,row+1, cols|bit, (diag|bit)<<1, (anti|bit)>>1);
}
return count;
}int totalNQueens(int n){ return place(n,0,0,0,0); }
int place(int n,int row,int cols,int diag,int anti){
if (row==n) return 1;
int free = ((1<<n)-1) & ~(cols|diag|anti); // columns still safe
int count=0;
while (free!=0){
int bit = free & -free; free -= bit; // pick lowest safe column
count += place(n,row+1, cols|bit, (diag|bit)<<1, (anti|bit)>>1);
}
return count;
}def total_n_queens(n):
return place(n, 0, 0, 0, 0)
def place(n, row, cols, diag, anti):
if row == n:
return 1
free = ((1 << n) - 1) & ~(cols | diag | anti) # columns still safe
count = 0
while free != 0:
bit = free & -free
free -= bit # pick lowest safe column
count += place(n, row+1, cols|bit, (diag|bit)<<1, (anti|bit)>>1)
return countfunction totalNQueens(n: number): number { return place(n,0,0,0,0); }
function place(n: number, row: number, cols: number, diag: number, anti: number): number {
if (row === n) return 1;
let free = ((1<<n)-1) & ~(cols|diag|anti); // columns still safe
let count = 0;
while (free !== 0) {
const bit = free & -free; free -= bit; // pick lowest safe column
count += place(n, row+1, cols|bit, (diag|bit)<<1, (anti|bit)>>1);
}
return count;
}Worked example — subsets of [1,2,3], traced
The recursion tree branches skip/take at each index:
i=0 {}
skip1 / \ take1
i=1 {} {1}
s/ \t s/ \t
{} {2} {1} {1,2}
s/\t s/\t s/\t s/\t
{} {3}{2}{2,3} {1}{1,3} {1,2}{1,2,3}i=0 {}
skip1 / \ take1
i=1 {} {1}
s/ \t s/ \t
{} {2} {1} {1,2}
s/\t s/\t s/\t s/\t
{} {3}{2}{2,3} {1}{1,3} {1,2}{1,2,3}i=0 {}
skip1 / \ take1
i=1 {} {1}
s/ \t s/ \t
{} {2} {1} {1,2}
s/\t s/\t s/\t s/\t
{} {3}{2}{2,3} {1}{1,3} {1,2}{1,2,3}i=0 {}
skip1 / \ take1
i=1 {} {1}
s/ \t s/ \t
{} {2} {1} {1,2}
s/\t s/\t s/\t s/\t
{} {3}{2}{2,3} {1}{1,3} {1,2}{1,2,3}The 8 leaves are exactly the 2³ subsets. Each edge is a choose; each return is an un-choose that pops the last element so the buffer is reused.
if(i>start && a[i]==a[i-1]) continue;) for unique subsets; cut a branch when a partial sum already exceeds the target; bound the remaining best-possible against the current best.🔬 Deeper dive & dry run
Why it works. Backtracking explores a decision tree where each path from the root encodes one partial/complete choice sequence, and the recursion maintains the invariant "path == the exact set of choices made on the current call stack". Because you undo (pop) a choice immediately after exploring its subtree, the same mutable path buffer can be reused for every branch — the tree is walked without ever materializing more than one branch's worth of state at a time.
When to reach for it. "Generate all subsets/permutations/combinations", "find all ways to partition/place X", constraint-satisfaction (N-Queens, Sudoku), or anything requiring exhaustive enumeration with early pruning of invalid branches.
Complexity. For subsets of an n-element set: O(2^n) time and results (every subset is emitted), O(n) extra space for the recursion stack/path buffer (excluding the output).
Pitfalls & variations. Forgetting to copy path before appending it to results stores a reference that later mutates — always snapshot (e.g. path[:]). Off-by-one on the start index causes duplicate or missing subsets. Variants: "subsets II" (dedupe with sorted input + skip-equal-siblings), permutations (loop over all unused indices instead of i..n), combination sum (allow reuse / prune by running total).
Trace input: nums = [1,2,3], backtrack(start, path) records path into result on every call, then loops i from start to n-1 choosing nums[i] — enumerate all 2³ subsets.
| Step | start | path | Action | result so far |
|---|---|---|---|---|
| 1 | 0 | [] | record | [[]] |
| 2 | 1 | [1] | choose nums[0]=1, record | [[], [1]] |
| 3 | 2 | [1,2] | choose nums[1]=2, record | +[1,2] |
| 4 | 3 | [1,2,3] | choose nums[2]=3, record | +[1,2,3] |
| 5 | 3 | [1,3] | pop 2 & 3, choose nums[2]=3, record | +[1,3] |
| 6 | 2 | [2] | pop back to [], choose nums[1]=2, record | +[2] |
| 7 | 3 | [2,3] | choose nums[2]=3, record | +[2,3] |
| 8 | 3 | [3] | pop back to [], choose nums[2]=3, record | +[3] |
Result: all 8 subsets: [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]. The trace shows every "record" happens at call entry — the include/exclude choice for each element is implicit in whether the loop reaches it before start advances past it.
🧰 Quick reference templates
21. Brute Force / Backtracking
... Small constraints YES ▸ Brute force enough YES
When n is tiny (≈ ≤ 12–20), enumerate all subsets/permutations/placements by recursion. Choose → recurse → undo. Prune illegal branches early.
List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, new ArrayList<>(), new boolean[nums.length], res);
return res;
}
void backtrack(int[] nums, List<Integer> cur, boolean[] used, List<List<Integer>> res) {
if (cur.size() == nums.length) { res.add(new ArrayList<>(cur)); return; }
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true; cur.add(nums[i]);
backtrack(nums, cur, used, res);
cur.remove(cur.size() - 1); used[i] = false; // undo
}
}vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
vector<int> cur;
vector<bool> used(nums.size(), false);
backtrack(nums, cur, used, res);
return res;
}
void backtrack(vector<int>& nums, vector<int>& cur, vector<bool>& used, vector<vector<int>>& res) {
if (cur.size() == nums.size()) { res.push_back(cur); return; }
for (int i = 0; i < (int)nums.size(); i++) {
if (used[i]) continue;
used[i] = true; cur.push_back(nums[i]);
backtrack(nums, cur, used, res);
cur.pop_back(); used[i] = false; // undo
}
}def permute(nums):
res = []
def backtrack(cur, used):
if len(cur) == len(nums):
res.append(cur[:])
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
cur.append(nums[i])
backtrack(cur, used)
cur.pop()
used[i] = False # undo
backtrack([], [False] * len(nums))
return resfunction permute(nums: number[]): number[][] {
const res: number[][] = [];
backtrack(nums, [], new Array(nums.length).fill(false), res);
return res;
}
function backtrack(nums: number[], cur: number[], used: boolean[], res: number[][]): void {
if (cur.length === nums.length) { res.push([...cur]); return; }
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true; cur.push(nums[i]);
backtrack(nums, cur, used, res);
cur.pop(); used[i] = false; // undo
}
}[1,2,3] ⇒ 3! = 6 permutations: [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1].🎯 LeetCode practice & worked example
10 · Backtracking
Tests systematic exploration of a decision tree with pruning; reach for it when the problem asks for "all possible" subsets, permutations, combinations, or valid placements and a brute-force search with undo is feasible.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Subsets ★ | Medium | AmazonMetaGoogle | include/exclude recursion |
| 2 | Combination Sum ★ | Medium | AmazonSnowflakeUber | unbounded pick, same-index revisit |
| 3 | Permutations | Medium | MicrosoftAmazonBloomberg | swap-in-place / used[] array |
| 4 | Subsets II | Medium | MetaAmazon | sort + skip duplicate siblings |
| 5 | Combination Sum II | Medium | AmazonMeta | sort + skip duplicate siblings, bounded pick |
| 6 | Word Search ★ | Medium | MicrosoftAmazonBloomberg | grid DFS with visited-mark/unmark |
| 7 | Palindrome Partitioning | Medium | AmazonMeta | prefix-check + recurse on suffix |
| 8 | Letter Combinations of a Phone Number ★ | Medium | AmazonUberGoogle | index-driven cartesian product |
| 9 | N-Queens | Hard | AmazonMicrosoftZenefits | row-by-row placement + column/diagonal sets |
Worked example — Subsets (LC 78, Medium)
Frequently asked at: Amazon, Meta, Google, Bloomberg.
Problem. Given an integer array nums of unique elements, return all possible subsets (the power set), in any order. Input [1,2,3] produces 8 subsets including [] and [1,2,3].
Idea. Every element is either included or excluded from the current subset. Walk the array index by index, at each index branch into "take it" and "skip it", and record the running subset (a copy) at every recursive call — that alone visits all 2^n leaves of the decision tree. Undo the choice (backtrack) after each branch so the shared list is reused correctly across siblings.
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> current = new ArrayList<>();
backtrack(nums, 0, current, result);
return result;
}
private void backtrack(int[] nums, int index, List<Integer> current,
List<List<Integer>> result) {
if (index == nums.length) {
result.add(new ArrayList<>(current));
return;
}
// Branch 1: exclude nums[index]
backtrack(nums, index + 1, current, result);
// Branch 2: include nums[index]
current.add(nums[index]);
backtrack(nums, index + 1, current, result);
current.remove(current.size() - 1); // undo
}
}
#include <vector>
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> current;
backtrack(nums, 0, current, result);
return result;
}
private:
void backtrack(vector<int>& nums, int index, vector<int>& current,
vector<vector<int>>& result) {
if (index == (int)nums.size()) {
result.push_back(current);
return;
}
// Branch 1: exclude nums[index]
backtrack(nums, index + 1, current, result);
// Branch 2: include nums[index]
current.push_back(nums[index]);
backtrack(nums, index + 1, current, result);
current.pop_back(); // undo
}
};from typing import List
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
current = []
self._backtrack(nums, 0, current, result)
return result
def _backtrack(self, nums, index, current, result):
if index == len(nums):
result.append(list(current))
return
# Branch 1: exclude nums[index]
self._backtrack(nums, index + 1, current, result)
# Branch 2: include nums[index]
current.append(nums[index])
self._backtrack(nums, index + 1, current, result)
current.pop() # undoclass Solution {
subsets(nums: number[]): number[][] {
const result: number[][] = [];
const current: number[] = [];
this.backtrack(nums, 0, current, result);
return result;
}
private backtrack(nums: number[], index: number, current: number[],
result: number[][]): void {
if (index === nums.length) {
result.push([...current]);
return;
}
// Branch 1: exclude nums[index]
this.backtrack(nums, index + 1, current, result);
// Branch 2: include nums[index]
current.push(nums[index]);
this.backtrack(nums, index + 1, current, result);
current.pop(); // undo
}
}Complexity. Time O(n * 2^n) Space O(n) — 2^n subsets are produced, each costing O(n) to copy; recursion depth is n.
Walkthrough. nums = [1,2]. index=0: exclude branch recurses with index=1, current=[]; there, exclude branch reaches index=2 → record []; include branch pushes 2 → current=[2] → record [2], then pops 2. Back at index=0's include branch, push 1 → current=[1], recurse to index=1: exclude → record [1]; include 2 → current=[1,2] → record [1,2], pop 2, pop 1. Final result: [[], [2], [1], [1,2]].
🔬 Deeper dive & dry run
Why it works. Backtracking explores the "include element i or not" decision tree via DFS over a single shared, mutable path. Every call records the current path as one complete subset (all partial paths are valid here, so nothing is pruned) and popping the last-added element after the recursive call returns restores the path so the next sibling branch starts clean — guaranteeing each of the 2^n combinations is generated exactly once.
When to reach for it. "Generate all combinations/permutations/subsets satisfying X" problems — the signal is needing every valid arrangement (not just one answer), combined with a natural choose/un-choose structure per element or index.
Complexity. O(2^n · n) time — 2^n subsets, each up to length n to build/record; O(n) auxiliary space for the recursion stack and current path (excluding the output storage itself).
Pitfalls & variations. Forgetting to pop (undo) after the recursive call corrupts every later sibling branch with leftover elements. Appending a reference to the mutable path instead of a copy makes every recorded subset alias the same, later-emptied list. Starting the inner loop at 0 instead of the current index produces permutations/duplicates instead of combinations.
Trace input: nums = [1,2,3] — generate every subset.
| Step | Call (start, path) | Action | Result so far |
|---|---|---|---|
| 0 | backtrack(0, []) | record path | [ [] ] |
| 1 | backtrack(1, [1]) | chose i=0→1; record path | [ [], [1] ] |
| 2 | backtrack(2, [1,2]) | chose i=1→2; record path | [ …, [1,2] ] |
| 3 | backtrack(3, [1,2,3]) | chose i=2→3; record path; start=3=n, no choices; return; pop 3 | [ …, [1,2,3] ] |
| 4 | back in backtrack(2,[1,2]) | loop exhausted; return; pop 2 → path=[1] | unchanged |
| 5 | backtrack(1,[1]) → backtrack(3,[1,3]) | chose i=2→3; record path | [ …, [1,3] ] |
| 6 | back in backtrack(1,[1]) | no more choices; return; pop 1 → path=[] | unchanged |
| 7 | backtrack(0,[]) → backtrack(2,[2]) | chose i=1→2; record path | [ …, [2] ] |
| 8 | backtrack(2,[2]) → backtrack(3,[2,3]) | chose i=2→3; record path | [ …, [2,3] ] |
| 9 | back in backtrack(2,[2]) | return; pop 3, pop 2 → path=[] | unchanged |
| 10 | backtrack(0,[]) → backtrack(3,[3]) | chose i=2→3; record path; return; pop 3; outer loop exhausted, done | [ …, [3] ] |
Result: Final result: [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]] — all 8 = 23 subsets, each recorded exactly once at the instant its call begins, before any further choices are layered on.
4 · Divide & Conquer / Tree DP
Flowchart leaf: count / generate many trees
The idea
Split a problem at a pivot into independent sub-problems, solve each recursively, then combine. "Count/generate all trees" is the poster child: fix each value as the root, recursively build all left subtrees and all right subtrees, and take the Cartesian product. Memoize sub-answers when ranges repeat.
Brute force — build then validate Catalan · n
Generating every labelling and checking the BST property afterwards revisits identical sub-structures over and over. D&C builds only valid shapes and (with memoization) reuses shared sub-ranges.
Generate all unique BSTs
lo, hi— the inclusive range of values still available to build a subtree from.root— the value chosen as the root of the current subtree; smaller values become the left subtree, larger ones the right.l, r— one candidate left subtree and one candidate right subtree, taken from the recursive results for the smaller ranges.res— the list of all distinct subtree shapes built for the range[lo, hi].- Returns — every structurally unique BST that can be built from the values in
[lo, hi]; the top-level call returns all unique BSTs for values1..n.
List<TreeNode> generateTrees(int n){ return build(1, n); }
List<TreeNode> build(int lo, int hi){
List<TreeNode> res = new ArrayList<>();
if (lo > hi){ res.add(null); return res; } // one empty shape
for (int root = lo; root <= hi; root++)
for (TreeNode l : build(lo, root-1)) // all left subtrees
for (TreeNode r : build(root+1, hi)){ // all right subtrees
TreeNode node = new TreeNode(root);
node.left = l; node.right = r;
res.add(node);
}
return res;
}std::vector<TreeNode*> generateTrees(int n){ return build(1, n); }
std::vector<TreeNode*> build(int lo, int hi){
std::vector<TreeNode*> res;
if (lo > hi){ res.push_back(nullptr); return res; } // one empty shape
for (int root = lo; root <= hi; root++)
for (TreeNode* l : build(lo, root-1)) // all left subtrees
for (TreeNode* r : build(root+1, hi)){ // all right subtrees
TreeNode* node = new TreeNode(root);
node->left = l; node->right = r;
res.push_back(node);
}
return res;
}def generate_trees(n):
return build(1, n)
def build(lo, hi):
res = []
if lo > hi:
res.append(None) # one empty shape
return res
for root in range(lo, hi+1):
for l in build(lo, root-1): # all left subtrees
for r in build(root+1, hi): # all right subtrees
node = TreeNode(root)
node.left = l
node.right = r
res.append(node)
return resfunction generateTrees(n: number): (TreeNode | null)[] { return build(1, n); }
function build(lo: number, hi: number): (TreeNode | null)[] {
const res: (TreeNode | null)[] = [];
if (lo > hi) { res.push(null); return res; } // one empty shape
for (let root = lo; root <= hi; root++)
for (const l of build(lo, root-1)) // all left subtrees
for (const r of build(root+1, hi)) { // all right subtrees
const node = new TreeNode(root);
node.left = l; node.right = r;
res.push(node);
}
return res;
}Just the count (Catalan via DP)
n— the number of nodes in the tree.dp[k]— the number of structurally unique BSTs that can be built fromknodes.nodes— the tree size currently being computed, built up from smaller sizes already solved.root— which position (1..nodes) is chosen as the root, splitting the rest into a left subtree of sizeroot-1and a right subtree of sizenodes-root.- Returns —
dp[n], the total count of unique BST shapes possible withnnodes.
int numTrees(int n){
int[] dp = new int[n+1];
dp[0] = dp[1] = 1;
for (int nodes=2; nodes<=n; nodes++)
for (int root=1; root<=nodes; root++)
dp[nodes] += dp[root-1] * dp[nodes-root]; // left size × right size
return dp[n];
}int numTrees(int n){
std::vector<int> dp(n+1, 0);
dp[0] = dp[1] = 1;
for (int nodes=2; nodes<=n; nodes++)
for (int root=1; root<=nodes; root++)
dp[nodes] += dp[root-1] * dp[nodes-root]; // left size × right size
return dp[n];
}def num_trees(n):
dp = [0] * (n + 1)
dp[0] = dp[1] = 1
for nodes in range(2, n + 1):
for root in range(1, nodes + 1):
dp[nodes] += dp[root-1] * dp[nodes-root] # left size × right size
return dp[n]function numTrees(n: number): number {
const dp = new Array<number>(n+1).fill(0);
dp[0] = dp[1] = 1;
for (let nodes = 2; nodes <= n; nodes++)
for (let root = 1; root <= nodes; root++)
dp[nodes] += dp[root-1] * dp[nodes-root]; // left size × right size
return dp[n];
}Worked example — n = 3
| Root | Left range | Right range | # shapes |
|---|---|---|---|
| 1 | ∅ (1 shape) | {2,3} (2 shapes) | 1×2 = 2 |
| 2 | {1} (1) | {3} (1) | 1×1 = 1 |
| 3 | {1,2} (2) | ∅ (1) | 2×1 = 2 |
Total = 2+1+2 = 5 = Catalan(3). The DP formula dp[3]=dp[0]dp[2]+dp[1]dp[1]+dp[2]dp[0] gives the same 5 in O(n²).
🔬 Deeper dive & dry run
Why it works. Tree DP via divide & conquer maintains the invariant "the recursive call on a subtree returns a locally-sufficient summary (here: subtree height) while updating a globally-shared best-so-far as a side effect". Post-order traversal guarantees both children's summaries are known before the parent combines them, so the parent never needs to re-descend — each edge is examined exactly once when contributing to its parent's answer.
When to reach for it. Any tree question phrased as "the answer might route through a node combining its subtrees" — diameter, max path sum, balanced-tree check, subtree counts/sums. Signal: the answer isn't a simple root-to-leaf value, it's a whole-tree property built from combining children.
Complexity. O(n) time — one post-order visit per node; O(h) space for the recursion stack (h = tree height, worst case O(n) for a skewed tree).
Pitfalls & variations. Computing height and diameter in two separate passes is a common O(n²) mistake — fold both into one post-order return. Forgetting that a null child contributes height 0, not −1, shifts the diameter off by one. Variants: max path sum allowing negative-value pruning, "diameter" measured in nodes vs. edges (know which the problem wants).
Trace input: binary tree rooted at 1: 1(2(4(7),5),3(·,6)) — find the tree's diameter (longest path between any two nodes, in edges) via one post-order pass.
| Step | Node | leftH | rightH | diam so far | height returned |
|---|---|---|---|---|---|
| 1 | 7 | 0 | 0 | 0 | 1 |
| 2 | 4 | 1 | 0 | 1 | 2 |
| 3 | 5 | 0 | 0 | 1 | 1 |
| 4 | 2 | 2 | 1 | 3 | 3 |
| 5 | 6 | 0 | 0 | 3 | 1 |
| 6 | 3 | 0 | 1 | 3 | 2 |
| 7 | 1 (root) | 3 | 2 | 5 | 4 |
Result: diameter = 5 edges, realized by the path 7→4→2→1→3→6. The trace shows the diameter updates only at nodes where a path could route through them (left height + right height), while the returned height is a separate, smaller number used purely by the parent.
🧰 Quick reference templates
2. Divide & Conquer / Tree DP
Graph YES ▸ Tree YES ▸ Level-order NO ▸ Count/generate many trees YES
To enumerate or count all structurally distinct trees, fix each possible root and recursively combine every left subtree with every right subtree. The Cartesian product of sub-results builds the whole set (this is the Catalan-number family).
List<TreeNode> generateTrees(int n) { return build(1, n); }
List<TreeNode> build(int lo, int hi) {
List<TreeNode> res = new ArrayList<>();
if (lo > hi) { res.add(null); return res; } // empty subtree
for (int root = lo; root <= hi; root++) {
for (TreeNode l : build(lo, root - 1))
for (TreeNode r : build(root + 1, hi)) {
TreeNode node = new TreeNode(root);
node.left = l; node.right = r;
res.add(node);
}
}
return res;
}vector<TreeNode*> generateTrees(int n) { return build(1, n); }
vector<TreeNode*> build(int lo, int hi) {
vector<TreeNode*> res;
if (lo > hi) { res.push_back(nullptr); return res; } // empty subtree
for (int root = lo; root <= hi; root++) {
for (TreeNode* l : build(lo, root - 1))
for (TreeNode* r : build(root + 1, hi)) {
TreeNode* node = new TreeNode(root);
node->left = l; node->right = r;
res.push_back(node);
}
}
return res;
}def generate_trees(n):
return build(1, n)
def build(lo, hi):
res = []
if lo > hi:
res.append(None) # empty subtree
return res
for root in range(lo, hi + 1):
for l in build(lo, root - 1):
for r in build(root + 1, hi):
node = TreeNode(root)
node.left = l
node.right = r
res.append(node)
return resfunction generateTrees(n: number): (TreeNode | null)[] {
return build(1, n);
}
function build(lo: number, hi: number): (TreeNode | null)[] {
const res: (TreeNode | null)[] = [];
if (lo > hi) { res.push(null); return res; } // empty subtree
for (let root = lo; root <= hi; root++) {
for (const l of build(lo, root - 1))
for (const r of build(root + 1, hi)) {
const node = new TreeNode(root);
node.left = l; node.right = r;
res.push(node);
}
}
return res;
}n=3. Roots 1,2,3 each fix one node; root 2 pairs left={1} with right={3}. Total = Catalan(3) = 5 distinct BSTs.5 · Topological Sort
Flowchart leaf: DAG-related
The idea
Order the vertices of a directed acyclic graph so every edge points forward — the classic "which order can I take these courses / build these targets" question. Two standard methods: Kahn's (BFS on in-degrees) and DFS post-order reversed. Kahn's also detects cycles for free: if you emit fewer than n nodes, a cycle blocked the rest.
Kahn's algorithm, step by step
- Compute in-degree (number of incoming edges) for every node.
- Queue all nodes with in-degree 0 — they depend on nothing.
- Pop one, append to the order, and decrement each successor's in-degree; if a successor hits 0, queue it.
- When the queue empties, if you emitted every node you have a valid order; otherwise the graph has a cycle.
Brute force — repeatedly scan for a free node O(V²)
Each round, linearly search for any node whose prerequisites are all done, output it, mark it done, repeat. Correct but quadratic; the in-degree queue makes it linear.
Optimal — Kahn's O(V + E)
n— the number of nodes.edges— the directed edges, each meaning "must come before".adj— adjacency list built fromedges.indeg— for each node, how many prerequisites it still has; a node is ready once this hits 0.q— the queue of nodes currently ready to be output (in-degree 0).order— the topological ordering being assembled.k— how many nodes have been placed intoorderso far.- Returns — the full topological order if all
nnodes were placed; an empty result if some nodes were never freed, meaning the graph has a cycle.
int[] topoSort(int n, int[][] edges){
List<Integer>[] adj = new List[n];
for (int i=0;i<n;i++) adj[i]=new ArrayList<>();
int[] indeg = new int[n];
for (int[] e : edges){ adj[e[0]].add(e[1]); indeg[e[1]]++; }
Queue<Integer> q = new ArrayDeque<>();
for (int i=0;i<n;i++) if (indeg[i]==0) q.offer(i);
int[] order = new int[n]; int k=0;
while (!q.isEmpty()){
int u = q.poll(); order[k++] = u;
for (int v : adj[u]) if (--indeg[v]==0) q.offer(v);
}
return k==n ? order : new int[0]; // empty ⇒ cycle
}vector<int> topoSort(int n, vector<vector<int>>& edges){
vector<vector<int>> adj(n);
vector<int> indeg(n, 0);
for (auto& e : edges){ adj[e[0]].push_back(e[1]); indeg[e[1]]++; }
queue<int> q;
for (int i=0;i<n;i++) if (indeg[i]==0) q.push(i);
vector<int> order(n); int k=0;
while (!q.empty()){
int u = q.front(); q.pop(); order[k++] = u;
for (int v : adj[u]) if (--indeg[v]==0) q.push(v);
}
return k==n ? order : vector<int>(); // empty ⇒ cycle
}def topo_sort(n, edges):
adj = [[] for _ in range(n)]
indeg = [0] * n
for e in edges:
adj[e[0]].append(e[1])
indeg[e[1]] += 1
q = deque()
for i in range(n):
if indeg[i] == 0:
q.append(i)
order = [0] * n
k = 0
while q:
u = q.popleft()
order[k] = u
k += 1
for v in adj[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return order if k == n else [] # empty ⇒ cyclefunction topoSort(n: number, edges: number[][]): number[] {
const adj: number[][] = Array.from({ length: n }, () => []);
const indeg = new Array(n).fill(0);
for (const e of edges) { adj[e[0]].push(e[1]); indeg[e[1]]++; }
const q: number[] = [];
for (let i = 0; i < n; i++) if (indeg[i] === 0) q.push(i);
const order = new Array(n).fill(0);
let k = 0;
let head = 0;
while (head < q.length) {
const u = q[head++]; order[k++] = u;
for (const v of adj[u]) if (--indeg[v] === 0) q.push(v);
}
return k === n ? order : []; // empty ⇒ cycle
}Worked example — 4 courses
Edges (prereq → course): 0→1, 0→2, 1→3, 2→3.
| Step | In-degrees | Queue | Emit |
|---|---|---|---|
| init | [0,1,1,2] | [0] | — |
| pop 0 | [-,0,0,2] | [1,2] | 0 |
| pop 1 | [-,-,0,1] | [2] | 0,1 |
| pop 2 | [-,-,-,0] | [3] | 0,1,2 |
| pop 3 | — | [] | 0,1,2,3 |
Order [0,1,2,3]; 4 of 4 emitted ⇒ no cycle. ([0,2,1,3] is equally valid.)
🔬 Deeper dive & dry run
Why it works. Kahn's algorithm maintains the invariant "a node is only safe to output once every prerequisite has already been output" — enforced by only enqueueing nodes whose in-degree has dropped to zero. Each time a node is processed, it "pays off" its outgoing edges by decrementing its neighbors' in-degree, so a neighbor becomes eligible the instant its last prerequisite is satisfied. If the algorithm terminates with fewer nodes output than exist, a cycle is blocking progress — some node's in-degree never reaches zero.
When to reach for it. "Course schedule / build order / task dependencies", "can all tasks be completed given prerequisites", or any DAG where you need an ordering (not necessarily unique) consistent with directed edges.
Complexity. O(V + E) time — each vertex enqueued/dequeued once, each edge relaxed once; O(V) space for the in-degree array and queue.
Pitfalls & variations. Building the adjacency list in the wrong direction (prerequisite→course vs. course→prerequisite) silently inverts the whole ordering. Forgetting the cycle check (len(order) < numCourses) makes the function silently return a partial, wrong answer instead of detecting infeasibility. DFS-based topo sort (post-order + reverse, with a 3-color visited state to catch back-edges) is the alternative implementation, often preferred when you also need explicit cycle-path reporting.
Trace input: 4 courses, prerequisite edges 0→1, 0→2, 1→3, 2→3 (edge u→v means "finish u before v") — find a valid course order, or detect it's impossible.
| Step | Popped | indegree[0..3] | Queue after | Order so far |
|---|---|---|---|---|
| 0 | — | [0,1,1,2] | [0] | [] |
| 1 | 0 | [0,0,0,2] | [1,2] | [0] |
| 2 | 1 | [0,0,0,1] | [2] | [0,1] |
| 3 | 2 | [0,0,0,0] | [3] | [0,1,2] |
| 4 | 3 | [0,0,0,0] | [] | [0,1,2,3] |
Result: order [0,1,2,3], length 4 = numCourses → feasible, no cycle. The trace shows course 3's in-degree dropping by one at both step 2 and step 3 — it only becomes eligible once both prerequisites 1 and 2 have fired.
🧰 Quick reference templates
4. Topological Sort (Kahn's algorithm)
Graph YES ▸ Tree NO ▸ DAG-related YES
Order the nodes of a directed acyclic graph so every edge points forward (course schedules, build order). Repeatedly emit nodes with in-degree 0; if fewer than n come out, there was a cycle.
int[] topoSort(int n, int[][] edges) {
List<Integer>[] adj = new List[n];
for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
int[] indeg = new int[n];
for (int[] e : edges) { adj[e[0]].add(e[1]); indeg[e[1]]++; }
Queue<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; i++) if (indeg[i] == 0) q.offer(i);
int[] order = new int[n]; int idx = 0;
while (!q.isEmpty()) {
int u = q.poll(); order[idx++] = u;
for (int v : adj[u]) if (--indeg[v] == 0) q.offer(v);
}
return idx == n ? order : new int[0]; // empty => cycle detected
}vector<int> topoSort(int n, vector<vector<int>>& edges) {
vector<vector<int>> adj(n);
vector<int> indeg(n, 0);
for (auto& e : edges) { adj[e[0]].push_back(e[1]); indeg[e[1]]++; }
queue<int> q;
for (int i = 0; i < n; i++) if (indeg[i] == 0) q.push(i);
vector<int> order(n); int idx = 0;
while (!q.empty()) {
int u = q.front(); q.pop(); order[idx++] = u;
for (int v : adj[u]) if (--indeg[v] == 0) q.push(v);
}
return idx == n ? order : vector<int>(); // empty => cycle detected
}def topo_sort(n, edges):
adj = [[] for _ in range(n)]
indeg = [0] * n
for u, v in edges:
adj[u].append(v)
indeg[v] += 1
q = deque(i for i in range(n) if indeg[i] == 0)
order = [0] * n
idx = 0
while q:
u = q.popleft()
order[idx] = u
idx += 1
for v in adj[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return order if idx == n else [] # empty => cycle detectedfunction topoSort(n: number, edges: number[][]): number[] {
const adj: number[][] = Array.from({ length: n }, () => []);
const indeg = new Array(n).fill(0);
for (const [u, v] of edges) { adj[u].push(v); indeg[v]++; }
const q: number[] = [];
for (let i = 0; i < n; i++) if (indeg[i] === 0) q.push(i);
const order = new Array(n).fill(0);
let idx = 0, head = 0;
while (head < q.length) {
const u = q[head++];
order[idx++] = u;
for (const v of adj[u]) if (--indeg[v] === 0) q.push(v);
}
return idx === n ? order : []; // empty => cycle detected
}n=4, edges 0→1, 0→2, 1→3, 2→3. In-degrees [0,1,1,2]. Emit 0 → drops 1,2 to 0 → emit 1,2 → drops 3 → emit 3. Order [0,1,2,3].6 · Dijkstra & the Shortest-Path Family
Flowchart leaf: weighted shortest path
The idea
Shortest paths from a source in a graph with non-negative weights. Dijkstra greedily settles the closest un-settled node using a min-heap. The invariant: when a node is popped for the first time, its shortest distance is final, because any other route would have to pass through a farther (hence not-yet-popped) node.
Step by step
dist[src]=0, everything else ∞; push(src,0).- Pop the smallest-distance entry. If it's stale (
d > dist[u]) skip it. - Relax each edge
u→v: ifdist[u]+w < dist[v], improvedist[v]and push the new pair. - Repeat until the heap empties.
Brute force — Bellman-Ford O(V·E)
Relax all edges V−1 times. Slower, but it is the correct choice when weights can be negative (Dijkstra breaks on negative edges), and it detects negative cycles.
for (int i=0;i<n-1;i++)
for (int[] e : edges) // e = {u, v, w}
if (dist[e[0]]+e[2] < dist[e[1]]) dist[e[1]] = dist[e[0]]+e[2];for (int i=0;i<n-1;i++)
for (auto& e : edges) // e = {u, v, w}
if (dist[e[0]]+e[2] < dist[e[1]]) dist[e[1]] = dist[e[0]]+e[2];for i in range(n - 1):
for e in edges: # e = [u, v, w]
if dist[e[0]] + e[2] < dist[e[1]]:
dist[e[1]] = dist[e[0]] + e[2]for (let i = 0; i < n - 1; i++)
for (const e of edges) // e = [u, v, w]
if (dist[e[0]] + e[2] < dist[e[1]]) dist[e[1]] = dist[e[0]] + e[2];Optimal — Dijkstra with a binary heap O(E log V)
adj— weighted adjacency list;adj[u]holds each neighborvand edge weightw.src— the starting node.dist— the best (smallest) distance found so far fromsrcto each node.pq— a min-heap of(node, distance)pairs, always expanding the currently-closest unvisited node next.u, d— the node popped frompqand the distance it was recorded with when pushed.d > dist[u]— a staleness check that skips a queue entry for a node already improved since it was pushed.v, w— a neighbor ofuand the weight of the edge to it.- Returns — an array of shortest distances from
srcto every other node.
int[] dijkstra(List<int[]>[] adj, int src, int n){ // adj[u] = {v, w}
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b)->a[1]-b[1]);
pq.offer(new int[]{src,0});
while (!pq.isEmpty()){
int[] top = pq.poll();
int u = top[0], d = top[1];
if (d > dist[u]) continue; // stale entry, skip
for (int[] e : adj[u]){
int v = e[0], w = e[1];
if (dist[u]+w < dist[v]){
dist[v] = dist[u]+w;
pq.offer(new int[]{v, dist[v]});
}
}
}
return dist;
}vector<int> dijkstra(vector<vector<pair<int,int>>>& adj, int src, int n){ // adj[u] = {v, w}
vector<int> dist(n, INT_MAX);
dist[src] = 0;
auto cmp = [](const pair<int,int>& a, const pair<int,int>& b){ return a.second > b.second; };
priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(cmp)> pq(cmp);
pq.push({src, 0});
while (!pq.empty()){
auto top = pq.top(); pq.pop();
int u = top.first, d = top.second;
if (d > dist[u]) continue; // stale entry, skip
for (auto& e : adj[u]){
int v = e.first, w = e.second;
if (dist[u]+w < dist[v]){
dist[v] = dist[u]+w;
pq.push({v, dist[v]});
}
}
}
return dist;
}def dijkstra(adj, src, n): # adj[u] = [(v, w), ...]
dist = [float('inf')] * n
dist[src] = 0
pq = [(0, src)] # (d, u)
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue # stale entry, skip
for v, w in adj[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
heapq.heappush(pq, (dist[v], v))
return distfunction dijkstra(adj: [number, number][][], src: number, n: number): number[] { // adj[u] = [v, w]
const dist = new Array(n).fill(Infinity);
dist[src] = 0;
const pq: [number, number][] = [[src, 0]]; // [u, d]; array used as a simple priority queue
while (pq.length) {
pq.sort((a, b) => a[1] - b[1]);
const [u, d] = pq.shift()!;
if (d > dist[u]) continue; // stale entry, skip
for (const [v, w] of adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push([v, dist[v]]);
}
}
}
return dist;
}Worked example, traced
Edges: 0→1(4), 0→2(1), 2→1(2), 1→3(1), 2→3(5). Source 0.
| Pop (node,d) | Relaxations | dist after |
|---|---|---|
| (0,0) | 1←4, 2←1 | [0,4,1,∞] |
| (2,1) | 1←1+2=3 (better!), 3←1+5=6 | [0,3,1,6] |
| (1,3) | 3←3+1=4 (better!) | [0,3,1,4] |
| (1,4) stale | 4>dist[1]=3 → skip | [0,3,1,4] |
| (3,4) | no outgoing improvements | [0,3,1,4] |
Final distances [0,3,1,4]. The stale (1,4) entry shows why the d > dist[u] guard matters — it silently discards outdated heap entries instead of re-processing them.
🔬 Deeper dive & dry run
Why it works. Dijkstra maintains the invariant "once a node is popped from the min-priority-queue with a distance matching its current best estimate, that distance is final" — because every edge weight is non-negative, no undiscovered path through an unvisited (necessarily ≥ current-min) node could ever beat it. The algorithm greedily settles nodes in non-decreasing order of shortest distance, relaxing (dist[v] = min(dist[v], dist[u] + w)) each outgoing edge as a node settles.
When to reach for it. "Shortest/cheapest path" with weighted, non-negative edges — network latency, flight cost, weighted grids. If negative edges are possible, this invariant breaks and you need Bellman-Ford instead.
Complexity. O((V + E) log V) with a binary-heap priority queue — each edge can push one heap entry, each push/pop is O(log V). O(V) space for dist[] plus O(E) for stale heap entries.
Pitfalls & variations. Not checking for stale/outdated heap entries (a node can be pushed multiple times at different distances before settling) causes redundant or, if mishandled, incorrect relaxations. Using Dijkstra on a graph with negative weights silently gives a wrong answer instead of erroring. Variants: A* (Dijkstra + admissible heuristic to bias the frontier), 0-1 BFS / plain BFS as cheaper special cases when weights are uniform.
Trace input: weighted graph, edges A-B:4, A-C:1, C-B:2, B-D:5, C-D:8, D-E:3, B-E:10, source A — find shortest distance to every node, in particular to E.
| Step | Pop (node,dist) | Relaxations | PQ after | dist[A,B,C,D,E] |
|---|---|---|---|---|
| 0 | — | — | [(0,A)] | [0,∞,∞,∞,∞] |
| 1 | (A,0) | B: 0+4=4; C: 0+1=1 | [(1,C),(4,B)] | [0,4,1,∞,∞] |
| 2 | (C,1) | B: 1+2=3<4; D: 1+8=9 | [(3,B),(4,B)*,(9,D)] | [0,3,1,9,∞] |
| 3 | (B,3) | D: 3+5=8<9; E: 3+10=13 | [(4,B)*,(8,D),(9,D)*,(13,E)] | [0,3,1,8,13] |
| 4 | (B,4) — stale | skip, dist[B] already 3 | [(8,D),(9,D)*,(13,E)] | [0,3,1,8,13] |
| 5 | (D,8) | E: 8+3=11<13 | [(9,D)*,(11,E),(13,E)*] | [0,3,1,8,11] |
| 6 | (D,9) — stale | skip, dist[D] already 8 | [(11,E),(13,E)*] | [0,3,1,8,11] |
| 7 | (E,11) | settle E — final | [(13,E)*] | [0,3,1,8,11] |
Result: shortest A→E = 11, via A-C-B-D-E (1+2+5+3). The trace's two stale pops (steps 4 & 6, marked *) show why the "skip if popped distance ≠ current best" guard is required — the queue can hold multiple outdated entries for the same node.
🧰 Quick reference templates
5. Dijkstra's Algorithm
... Shortest-path YES ▸ Weighted YES
Shortest path with non-negative weights. A min-heap always expands the closest unfinalized node; the first time you pop a node its distance is final. Skip stale heap entries.
int[] dijkstra(List<int[]>[] adj, int src, int n) { // adj[u] = {v, weight}
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
pq.offer(new int[]{src, 0});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int u = cur[0], d = cur[1];
if (d > dist[u]) continue; // stale entry
for (int[] e : adj[u]) {
int v = e[0], w = e[1];
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.offer(new int[]{v, dist[v]});
}
}
}
return dist;
}vector<int> dijkstra(vector<vector<array<int,2>>>& adj, int src, int n) { // adj[u] = {v, weight}
vector<int> dist(n, INT_MAX);
dist[src] = 0;
auto cmp = [](const array<int,2>& a, const array<int,2>& b) { return a[1] > b[1]; };
priority_queue<array<int,2>, vector<array<int,2>>, decltype(cmp)> pq(cmp);
pq.push({src, 0});
while (!pq.empty()) {
auto cur = pq.top(); pq.pop();
int u = cur[0], d = cur[1];
if (d > dist[u]) continue; // stale entry
for (auto& e : adj[u]) {
int v = e[0], w = e[1];
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({v, dist[v]});
}
}
}
return dist;
}def dijkstra(adj, src, n): # adj[u] = [(v, weight), ...]
dist = [float('inf')] * n
dist[src] = 0
pq = [(0, src)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue # stale entry
for v, w in adj[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
heapq.heappush(pq, (dist[v], v))
return distfunction dijkstra(adj: [number, number][][], src: number, n: number): number[] { // adj[u] = [v, weight]
const dist = new Array(n).fill(Infinity);
dist[src] = 0;
// simple binary min-heap keyed by distance, standing in for java.util.PriorityQueue
const heap: [number, number][] = [[0, src]]; // [dist, node]
const push = (item: [number, number]) => {
heap.push(item);
let i = heap.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (heap[p][0] <= heap[i][0]) break;
[heap[p], heap[i]] = [heap[i], heap[p]];
i = p;
}
};
const pop = (): [number, number] => {
const top = heap[0];
const last = heap.pop()!;
if (heap.length > 0) {
heap[0] = last;
let i = 0;
while (true) {
const l = 2 * i + 1, r = 2 * i + 2;
let s = i;
if (l < heap.length && heap[l][0] < heap[s][0]) s = l;
if (r < heap.length && heap[r][0] < heap[s][0]) s = r;
if (s === i) break;
[heap[s], heap[i]] = [heap[i], heap[s]];
i = s;
}
}
return top;
};
while (heap.length > 0) {
const [d, u] = pop();
if (d > dist[u]) continue; // stale entry
for (const [v, w] of adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
push([dist[v], v]);
}
}
}
return dist;
}0→1(4), 0→2(1), 2→1(2), 1→3(1). Pop 0(0) → set 1=4,2=1. Pop 2(1) → relax 1 to 3. Pop 1(3) → set 3=4. Distances [0,3,1,4].🎯 LeetCode practice & worked example
12 · Advanced Graphs
Tests weighted shortest-path and minimum-spanning-tree reasoning plus graph construction from implicit constraints; reach for it when edges carry weights or costs and you need shortest distance, minimum connecting cost, or a valid total order.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Reconstruct Itinerary | Hard | GoogleAmazon | Eulerian path via Hierholzer's / lexicographic DFS |
| 2 | Min Cost to Connect All Points ★ | Medium | AmazonGoogleMeta | MST via Prim's with min-heap |
| 3 | Network Delay Time ★ | Medium | GoogleAmazonUber | Dijkstra's shortest path from a source |
| 4 | Swim in Rising Water | Hard | GoogleAmazon | binary search + BFS, or Dijkstra on min-max path |
| 5 | Alien Dictionary ★ | Hard | MetaAirbnbGoogle | build order graph from adjacent word pairs, topo sort |
| 6 | Cheapest Flights Within K Stops | Medium | AmazonGoogle | Bellman-Ford relaxed for at most K edges |
Worked example — Network Delay Time (LC 743, Medium)
Frequently asked at: Google, Amazon, Uber, Bloomberg.
Problem. Given n nodes labeled 1..n and directed weighted edges times[i] = [u, v, w] (signal travels u→v in w time), plus a source node k, return the time for all n nodes to receive a signal sent from k, or -1 if some node is unreachable.
Idea. This is exactly single-source shortest paths on a non-negative weighted directed graph, so run Dijkstra's algorithm: maintain a min-heap of (distance, node) pairs, always expand the currently-closest unvisited node, and relax its outgoing edges. The answer is the maximum finite distance across all nodes (the last node to receive the signal); if any node's distance is never finalized, return -1.
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
for (int[] t : times) {
int u = t[0], v = t[1], w = t[2];
graph.get(u).add(new int[]{v, w});
}
int[] dist = new int[n + 1];
java.util.Arrays.fill(dist, Integer.MAX_VALUE);
dist[k] = 0;
// {distance, node}
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{0, k});
boolean[] visited = new boolean[n + 1];
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int d = curr[0], node = curr[1];
if (visited[node]) continue;
visited[node] = true;
for (int[] edge : graph.get(node)) {
int next = edge[0], weight = edge[1];
if (!visited[next] && d + weight < dist[next]) {
dist[next] = d + weight;
pq.offer(new int[]{dist[next], next});
}
}
}
int maxDist = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == Integer.MAX_VALUE) {
return -1;
}
maxDist = Math.max(maxDist, dist[i]);
}
return maxDist;
}
}
#include <vector>
#include <queue>
#include <climits>
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<pair<int,int>>> graph(n + 1);
for (auto& t : times) {
int u = t[0], v = t[1], w = t[2];
graph[u].push_back({v, w});
}
vector<int> dist(n + 1, INT_MAX);
dist[k] = 0;
// {distance, node}
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
pq.push({0, k});
vector<bool> visited(n + 1, false);
while (!pq.empty()) {
auto curr = pq.top(); pq.pop();
int d = curr.first, node = curr.second;
if (visited[node]) continue;
visited[node] = true;
for (auto& edge : graph[node]) {
int next = edge.first, weight = edge.second;
if (!visited[next] && d + weight < dist[next]) {
dist[next] = d + weight;
pq.push({dist[next], next});
}
}
}
int maxDist = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == INT_MAX) {
return -1;
}
maxDist = max(maxDist, dist[i]);
}
return maxDist;
}
};import heapq
from typing import List
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
graph = [[] for _ in range(n + 1)]
for t in times:
u, v, w = t[0], t[1], t[2]
graph[u].append((v, w))
dist = [float('inf')] * (n + 1)
dist[k] = 0
# (distance, node)
pq = [(0, k)]
visited = [False] * (n + 1)
while pq:
d, node = heapq.heappop(pq)
if visited[node]:
continue
visited[node] = True
for next, weight in graph[node]:
if not visited[next] and d + weight < dist[next]:
dist[next] = d + weight
heapq.heappush(pq, (dist[next], next))
maxDist = 0
for i in range(1, n + 1):
if dist[i] == float('inf'):
return -1
maxDist = max(maxDist, dist[i])
return maxDistclass Solution {
networkDelayTime(times: number[][], n: number, k: number): number {
const graph: [number, number][][] = Array.from({ length: n + 1 }, () => []);
for (const t of times) {
const [u, v, w] = t;
graph[u].push([v, w]);
}
const dist = new Array(n + 1).fill(Infinity);
dist[k] = 0;
// [distance, node]
let pq: [number, number][] = [[0, k]];
const visited = new Array(n + 1).fill(false);
while (pq.length > 0) {
pq.sort((a, b) => a[0] - b[0]);
const [d, node] = pq.shift()!;
if (visited[node]) continue;
visited[node] = true;
for (const [next, weight] of graph[node]) {
if (!visited[next] && d + weight < dist[next]) {
dist[next] = d + weight;
pq.push([dist[next], next]);
}
}
}
let maxDist = 0;
for (let i = 1; i <= n; i++) {
if (dist[i] === Infinity) {
return -1;
}
maxDist = Math.max(maxDist, dist[i]);
}
return maxDist;
}
}Complexity. Time O(E log V) Space O(V + E) — each edge triggers at most one heap insertion, and the heap holds up to E entries.
Walkthrough. times=[[2,1,1],[2,3,1],[3,4,1]], n=4, k=2. dist=[_,MAX,0,MAX,MAX]. Pop (0,2): relax 1→dist[1]=1, push (1,1); relax 3→dist[3]=1, push (1,3). Pop (1,1): node 1 has no outgoing edges. Pop (1,3): relax 4→dist[4]=2, push (2,4). Pop (2,4): no outgoing edges. Final dist=[_,1,0,1,2]; max = 2 → return 2.
🔬 Deeper dive & dry run
Why it works. Dijkstra's greedily finalizes the closest remaining node first: once a node is popped from the min-heap holding the smallest tentative distance, no path discovered later — through any longer, still-unprocessed route — can ever produce something smaller, because all edge weights are non-negative. That guarantee is exactly what lets each popped distance be treated as final.
When to reach for it. Shortest path in a weighted graph with non-negative edges from a single (or few) source(s) — the signal is non-uniform weights (uniform weights ⇒ plain BFS suffices) and no negative edges (negative ⇒ need Bellman-Ford instead).
Complexity. O(E log V) time with a binary-heap priority queue (each relaxation may push once, heap operations are O(log V)); O(V + E) space for the adjacency list, distance array, and heap.
Pitfalls & variations. Not skipping stale heap entries for already-finalized nodes wastes work (harmless if guarded, wrong if it overwrites a final distance). Using BFS when weights aren't uniform gives incorrect shortest paths. Forgetting that some node may stay at distance ∞ (unreachable) — the correct answer is then -1, not the max of a still-infinite array.
Trace input: times = [[2,1,1],[2,3,1],[3,4,1]], n = 4 nodes, source k = 2 — time for a signal sent from node 2 to reach every node.
| Step | Action | Node finalized | dist[1,2,3,4] | Heap after |
|---|---|---|---|---|
| 0 | init | — | [∞, 0, ∞, ∞] | [(0,2)] |
| 1 | pop (0,2) | node 2, dist=0 | [∞, 0, ∞, ∞] | [] |
| 2 | relax 2→1 (w=1): 0+1=1 < ∞ | — | [1, 0, ∞, ∞] | [(1,1)] |
| 3 | relax 2→3 (w=1): 0+1=1 < ∞ | — | [1, 0, 1, ∞] | [(1,1),(1,3)] |
| 4 | pop (1,1) | node 1, dist=1 (no outgoing edges) | [1, 0, 1, ∞] | [(1,3)] |
| 5 | pop (1,3) | node 3, dist=1 | [1, 0, 1, ∞] | [] |
| 6 | relax 3→4 (w=1): 1+1=2 < ∞ | — | [1, 0, 1, 2] | [(2,4)] |
| 7 | pop (2,4) | node 4, dist=2 (no outgoing edges) | [1, 0, 1, 2] | [] |
Result: All 4 nodes finalized: dist = {1:1, 2:0, 3:1, 4:2}. Answer = max(dist) = 2, the time to reach the farthest node (4), via the chain 2→3→4.
7 · Union-Find (Disjoint Set Union)
Flowchart leaf: connectivity
The idea
Maintain a partition of n elements into disjoint sets with two near-O(1) operations: find(x) (which set?) and union(a,b) (merge two sets). Perfect for "are these connected", "count components", Kruskal's MST, and detecting cycles as you add edges. Two optimizations make it almost constant: path compression (flatten the tree during find) and union by rank/size (attach the smaller tree under the larger).
🧰 API — Union-Find / Disjoint Set Union methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
// Create
int[] parent = new int[n];
int[] rank = new int[n];
for (int i = 0; i < n; i++) parent[i] = i; // n singleton sets: {0}, {1}, ..., {n-1}
int find(int x); // return the representative (root) of x's set, O(log n) amortized, ~O(1) with path compression
void union(int a, int b); // merge the sets containing a and b (by rank/size), O(log n) amortized
boolean connected(int a, int b); // true iff a and b are in the same set: find(a) == find(b), O(log n) amortized
int count(); // number of remaining disjoint sets, O(1) if tracked with a counter
// Create
vector<int> parent(n), rnk(n, 0);
for (int i = 0; i < n; i++) parent[i] = i; // n singleton sets
int find(int x); // path-compressed root lookup, O(log n) amortized, ~O(1)
void unite(int a, int b); // union by rank/size, O(log n) amortized
bool connected(int a, int b); // find(a) == find(b), O(log n) amortized
int count(); // number of disjoint sets, O(1) if tracked
# Create
parent = list(range(n)) # parent[i] = i -> n singleton sets
rank = [0] * n # for union by rank (optional)
find(x: int) -> int # path-compressed root lookup, O(log n) amortized, ~O(1)
union(a: int, b: int) -> None # union by rank/size, O(log n) amortized
connected(a: int, b: int) -> bool # find(a) == find(b), O(log n) amortized
count() -> int # number of disjoint sets, O(1) if tracked
// Create
class DSU {
parent: number[];
rankArr: number[];
count_: number;
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i); // n singleton sets
this.rankArr = new Array(n).fill(0);
this.count_ = n;
}
find(x: number): number { return x; } // path-compressed root lookup, O(log n) amortized, ~O(1)
union(a: number, b: number): void {} // union by rank/size, O(log n) amortized
connected(a: number, b: number): boolean { return false; } // find(a) === find(b), O(log n) amortized
count(): number { return this.count_; } // number of disjoint sets, O(1)
}
Brute force — BFS/DFS per query O(query · (V+E))
Answering each "connected?" by a fresh graph search is fine for a few queries but collapses under many interleaved unions and queries — exactly where DSU shines with near-constant amortized cost.
Optimal — DSU with both optimizations ~O(α(n)) per op
parent— for each node, a pointer toward its set's representative ("root");parent[x] == xmeansxis a root.rank— a rough measure of each tree's height/size, used to always attach the smaller tree under the bigger one.count— how many disjoint sets (components) currently exist.find(x)— walks up parent pointers tox's root, compressing the path along the way so future lookups are faster.ra, rb— the set representatives (roots) ofaandb, compared to decide whether they're already connected.- Returns —
findreturns the representative (root) of a node's set;unionreturnsfalseifaandbwere already in the same set (no-op),trueif it merged two sets.
class DSU {
int[] parent, rank; int count;
DSU(int n){
parent = new int[n]; rank = new int[n]; count = n;
for (int i=0;i<n;i++) parent[i]=i;
}
int find(int x){
while (parent[x]!=x){ parent[x]=parent[parent[x]]; x=parent[x]; } // compress
return x;
}
boolean union(int a,int b){
int ra=find(a), rb=find(b);
if (ra==rb) return false; // already same set (edge forms a cycle)
if (rank[ra]<rank[rb]){ int t=ra; ra=rb; rb=t; }
parent[rb]=ra; // smaller under larger
if (rank[ra]==rank[rb]) rank[ra]++;
count--; // one fewer component
return true;
}
}class DSU {
public:
vector<int> parent, rank; int count;
DSU(int n){
parent.resize(n); rank.assign(n, 0); count = n;
for (int i=0;i<n;i++) parent[i]=i;
}
int find(int x){
while (parent[x]!=x){ parent[x]=parent[parent[x]]; x=parent[x]; } // compress
return x;
}
bool unite(int a,int b){ // 'union' is a reserved keyword in C++
int ra=find(a), rb=find(b);
if (ra==rb) return false; // already same set (edge forms a cycle)
if (rank[ra]<rank[rb]){ int t=ra; ra=rb; rb=t; }
parent[rb]=ra; // smaller under larger
if (rank[ra]==rank[rb]) rank[ra]++;
count--; // one fewer component
return true;
}
};class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.count = n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x] # compress
return x
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False # already same set (edge forms a cycle)
if self.rank[ra] < self.rank[rb]:
ra, rb = rb, ra
self.parent[rb] = ra # smaller under larger
if self.rank[ra] == self.rank[rb]:
self.rank[ra] += 1
self.count -= 1 # one fewer component
return Trueclass DSU {
parent: number[];
rank: number[];
count: number;
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.rank = new Array(n).fill(0);
this.count = n;
}
find(x: number): number {
while (this.parent[x] !== x) {
this.parent[x] = this.parent[this.parent[x]];
x = this.parent[x]; // compress
}
return x;
}
union(a: number, b: number): boolean {
let ra = this.find(a), rb = this.find(b);
if (ra === rb) return false; // already same set (edge forms a cycle)
if (this.rank[ra] < this.rank[rb]) { [ra, rb] = [rb, ra]; }
this.parent[rb] = ra; // smaller under larger
if (this.rank[ra] === this.rank[rb]) this.rank[ra]++;
this.count--; // one fewer component
return true;
}
}Worked example — number of provinces
n=5. Process edges (0,1), (1,2), (3,4):
| Union | parent[] (roots) | count |
|---|---|---|
| start | 0 1 2 3 4 | 5 |
| (0,1) | 0 0 2 3 4 | 4 |
| (1,2) | 0 0 0 3 4 | 3 |
| (3,4) | 0 0 0 3 3 | 2 |
find(0)==find(2) → true (both root 0). Components = 2: {0,1,2} and {3,4}. Trying union(0,2) returns false (same set) — that "false" is how Kruskal skips cycle-forming edges.
🔬 Deeper dive & dry run
Why it works. DSU maintains the invariant "two elements are in the same set if and only if find resolves them to the same root", and union preserves it by attaching one root under the other. Path compression (pointing every visited node directly at the root during find) and union by rank/size keep the trees shallow, so the invariant stays cheap to check even after many unions.
When to reach for it. "Number of connected components/provinces/islands (as a list of pairwise connections rather than a grid)", "are these two nodes connected", "detect a cycle while adding edges one at a time" (redundant connection problems), Kruskal's MST.
Complexity. With path compression + union by rank: O(α(n)) amortized per operation (effectively constant), O(n) space for the parent (and optional rank) array.
Pitfalls & variations. Skipping path compression and union by rank degrades find to O(n) worst case on a skewed chain. Forgetting to decrement a running "component count" on successful union forces an O(n) final pass to count distinct roots. Variants: weighted union-find (track a relative offset/ratio between nodes), DSU with rollback (for offline queries needing undo).
Trace input: isConnected matrix for 5 cities, upper triangle checked pair by pair — connections present at (0,1), (1,2), (3,4) — count the number of provinces (connected components).
| Step | Pair (i,j) | matrix[i][j] | Action | parent[0..4] | Provinces |
|---|---|---|---|---|---|
| 0 | — | — | init | [0,1,2,3,4] | 5 |
| 1 | (0,1) | 1 | union — root(0)=0, root(1)=1, differ | [0,0,2,3,4] | 4 |
| 2 | (0,2) | 0 | skip | [0,0,2,3,4] | 4 |
| 3 | (0,3) | 0 | skip | [0,0,2,3,4] | 4 |
| 4 | (0,4) | 0 | skip | [0,0,2,3,4] | 4 |
| 5 | (1,2) | 1 | union — root(1)=0, root(2)=2, differ | [0,0,0,3,4] | 3 |
| 6 | (1,3) | 0 | skip | [0,0,0,3,4] | 3 |
| 7 | (1,4) | 0 | skip | [0,0,0,3,4] | 3 |
| 8 | (2,3) | 0 | skip | [0,0,0,3,4] | 3 |
| 9 | (2,4) | 0 | skip | [0,0,0,3,4] | 3 |
| 10 | (3,4) | 1 | union — root(3)=3, root(4)=4, differ | [0,0,0,3,3] | 2 |
Result: 2 provinces — roots {0,3}. Cities 0,1,2 collapse into root 0; cities 3,4 collapse into root 3. The provinces count only ever decreases, and only on a successful union between two distinct roots — the eight skipped pairs never touch parent[].
🧰 Quick reference templates
7. Disjoint Set Union (Union-Find)
... Shortest-path NO ▸ Connectivity YES
"Are these connected / how many components / are these two in the same group?" DSU merges sets in near-O(1) amortized with path compression + union by rank.
class DSU {
int[] parent, rank; int count;
DSU(int n) {
parent = new int[n]; rank = new int[n]; count = n;
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x; // path compression
}
boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false; // already connected
if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; }
parent[rb] = ra;
if (rank[ra] == rank[rb]) rank[ra]++;
count--;
return true;
}
}class DSU {
public:
vector<int> parent, rank_; int count;
DSU(int n) {
parent.resize(n); rank_.assign(n, 0); count = n;
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x; // path compression
}
bool unite(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false; // already connected
if (rank_[ra] < rank_[rb]) swap(ra, rb);
parent[rb] = ra;
if (rank_[ra] == rank_[rb]) rank_[ra]++;
count--;
return true;
}
};class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.count = n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x # path compression
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False # already connected
if self.rank[ra] < self.rank[rb]:
ra, rb = rb, ra
self.parent[rb] = ra
if self.rank[ra] == self.rank[rb]:
self.rank[ra] += 1
self.count -= 1
return Trueclass DSU {
parent: number[];
rank: number[];
count: number;
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.rank = new Array(n).fill(0);
this.count = n;
}
find(x: number): number {
while (this.parent[x] !== x) { this.parent[x] = this.parent[this.parent[x]]; x = this.parent[x]; }
return x; // path compression
}
union(a: number, b: number): boolean {
let ra = this.find(a), rb = this.find(b);
if (ra === rb) return false; // already connected
if (this.rank[ra] < this.rank[rb]) { const t = ra; ra = rb; rb = t; }
this.parent[rb] = ra;
if (this.rank[ra] === this.rank[rb]) this.rank[ra]++;
this.count--;
return true;
}
}n=5, unions (0,1),(1,2),(3,4). After merges count = 2 components: {0,1,2} and {3,4}. find(0)==find(2) is true.8 · Binary Search — on an Array and on the Answer
Flowchart leaves: binary search (sorted) binary search on the answer (max/min)
The idea
If the search space is monotonic — sorted values, or a yes/no predicate that flips once from false to true — you can halve it each step. Two flavours: searching a sorted array for a value/boundary, and searching the answer range for the smallest/largest value that satisfies a feasibility test. The second is the interview power move: "minimize the max…" / "maximize the min…" almost always means binary-search-the-answer.
Brute force — linear scan / try every candidate O(n) or O(range)
Walk the array element by element, or try every possible answer value and test feasibility. Correct but ignores the monotonic structure that lets you skip half the space each comparison.
Template A · exact / boundary search in a sorted array O(log n)
a— the sorted array being searched.target— the value whose boundary position you want.lo, hi— the current search window, kept half-open as[lo, hi)so it shrinks cleanly to a single point.mid— the midpoint index being tested each iteration.- Returns — the first index whose value is
>= target(the "insertion point"); equals the array length if every element is smaller.
int lowerBound(int[] a, int target){ // first index with a[i] >= target
int lo = 0, hi = a.length; // [lo, hi) half-open
while (lo < hi){
int mid = lo + (hi - lo)/2;
if (a[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo; // insertion point; == length if none
}int lowerBound(vector<int>& a, int target){ // first index with a[i] >= target
int lo = 0, hi = a.size(); // [lo, hi) half-open
while (lo < hi){
int mid = lo + (hi - lo)/2;
if (a[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo; // insertion point; == length if none
}def lower_bound(a, target): # first index with a[i] >= target
lo, hi = 0, len(a) # [lo, hi) half-open
while lo < hi:
mid = lo + (hi - lo) // 2
if a[mid] < target:
lo = mid + 1
else:
hi = mid
return lo # insertion point; == length if nonefunction lowerBound(a: number[], target: number): number { // first index with a[i] >= target
let lo = 0, hi = a.length; // [lo, hi) half-open
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (a[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo; // insertion point; == length if none
}Template B · binary search on the answer (feasibility predicate)
piles— the sizes of the banana piles to eat through.h— the number of hours available.lo, hi— the current range of candidate eating speeds still being narrowed down.mid— the candidate speed being tested this iteration.feasible(piles, speed, h)— the predicate: can all piles be eaten withinhhours at thisspeed?hours— the total hoursfeasiblecomputes it would take at the candidate speed.- Returns — the smallest eating speed that still finishes all piles within
hhours.
// smallest speed so Koko finishes all piles within h hours
int minEatingSpeed(int[] piles, int h){
int lo = 1, hi = 0;
for (int p : piles) hi = Math.max(hi, p);
while (lo < hi){
int mid = lo + (hi - lo)/2;
if (feasible(piles, mid, h)) hi = mid; // can do it ⇒ try slower
else lo = mid + 1; // too slow ⇒ speed up
}
return lo; // smallest feasible speed
}
boolean feasible(int[] piles, int speed, int h){
long hours = 0;
for (int p : piles) hours += (p + speed - 1) / speed; // ceil
return hours <= h;
}// smallest speed so Koko finishes all piles within h hours
int minEatingSpeed(vector<int>& piles, int h){
int lo = 1, hi = 0;
for (int p : piles) hi = max(hi, p);
while (lo < hi){
int mid = lo + (hi - lo)/2;
if (feasible(piles, mid, h)) hi = mid; // can do it ⇒ try slower
else lo = mid + 1; // too slow ⇒ speed up
}
return lo; // smallest feasible speed
}
bool feasible(vector<int>& piles, int speed, int h){
long long hours = 0;
for (int p : piles) hours += (p + speed - 1) / speed; // ceil
return hours <= h;
}def min_eating_speed(piles, h):
# smallest speed so Koko finishes all piles within h hours
lo, hi = 1, max(piles)
while lo < hi:
mid = lo + (hi - lo) // 2
if feasible(piles, mid, h):
hi = mid # can do it ⇒ try slower
else:
lo = mid + 1 # too slow ⇒ speed up
return lo # smallest feasible speed
def feasible(piles, speed, h):
hours = 0
for p in piles:
hours += (p + speed - 1) // speed # ceil
return hours <= h// smallest speed so Koko finishes all piles within h hours
function minEatingSpeed(piles: number[], h: number): number {
let lo = 1, hi = 0;
for (const p of piles) hi = Math.max(hi, p);
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (feasible(piles, mid, h)) hi = mid; // can do it ⇒ try slower
else lo = mid + 1; // too slow ⇒ speed up
}
return lo; // smallest feasible speed
}
function feasible(piles: number[], speed: number, h: number): boolean {
let hours = 0;
for (const p of piles) hours += Math.ceil(p / speed); // ceil
return hours <= h;
}Worked example — binary search on the answer, traced
piles=[3,6,7,11], h=8. Feasible speeds form the monotonic pattern F F F T T …; we want the first T. Range [1,11]:
| lo | hi | mid | hours at mid | feasible? | move |
|---|---|---|---|---|---|
| 1 | 11 | 6 | 1+1+2+2=6 | yes | hi=6 |
| 1 | 6 | 3 | 1+2+3+4=10 | no | lo=4 |
| 4 | 6 | 5 | 1+2+2+3=8 | yes | hi=5 |
| 4 | 5 | 4 | 1+2+2+3=8 | yes | hi=4 |
| 4 | 4 | lo==hi → answer 4 | |||
Answer 4. Note we never enumerate speeds — 4 probes cover the whole range 1..11.
[lo,hi] (loop while lo<=hi) or [lo,hi) (loop while lo<hi), and use lo+(hi-lo)/2 to dodge integer overflow. For "largest feasible", mirror the moves.🔬 Deeper dive & dry run
Why it works. Binary search on the answer requires a monotonic feasible(x) predicate: if speed x works, every larger speed also works (and every smaller speed that fails, all smaller speeds also fail). That monotonicity turns "find the minimum feasible x" into ordinary binary search over the answer space [lo, hi], converging by halving regardless of how expensive feasible(x) is to evaluate.
When to reach for it. "Minimum/maximum X such that condition holds", "smallest capacity/speed/threshold to finish within a limit" — Koko Eating Bananas, Ship Packages Within D Days, split array largest sum. Signal: the array itself isn't sorted for a value search; instead you're searching an answer range against a checkable predicate.
Complexity. O(n log(range)) — log(range) binary-search steps, each paying O(n) to evaluate feasible. Space O(1) beyond input.
Pitfalls & variations. Using mid = (lo+hi)//2 with hi = mid on the feasible branch but forgetting lo = mid + 1 on the infeasible branch causes an infinite loop. Off-by-one on whether hi starts at max(piles) or sum(piles) changes correctness for edge cases like a single giant pile. Variants: search where the predicate is "cost ≤ budget" instead of "time ≤ limit" — the shape is identical, only the checker changes.
Trace input: piles = [3,6,7,11], h = 8 hours — find the minimum eating speed k so sum(ceil(pile/k)) ≤ h.
| Step | lo | hi | mid | hours(mid) | feasible? | Action |
|---|---|---|---|---|---|---|
| 0 | 1 | 11 | — | — | — | search space = candidate speeds 1..max(piles) |
| 1 | 1 | 11 | 6 | 1+1+2+2=6 | yes (6≤8) | hi = 6 |
| 2 | 1 | 6 | 3 | 1+2+3+4=10 | no (10>8) | lo = 4 |
| 3 | 4 | 6 | 5 | 1+2+2+3=8 | yes (8≤8) | hi = 5 |
| 4 | 4 | 5 | 4 | 1+2+2+3=8 | yes (8≤8) | hi = 4 → lo==hi, stop |
Result: minimum speed k = 4. The trace shows the window [lo,hi] shrinking from 11 candidates to 1 in four steps (log2(11)≈3.5), and that speed 3 (checked implicitly, would take 10h) fails while 4 exactly meets the 8h budget.
🧰 Quick reference templates
11. Binary Search
... Sorted/monotonic YES ▸ Dynamic queries NO
Sorted array + a target ⇒ halve the search space each step. Use lo + (hi-lo)/2 to avoid overflow. Variants: leftmost/rightmost insertion point, first-true predicate.
int search(int[] nums, int target) {
int lo = 0, hi = nums.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1; // not found (or `lo` = insertion point)
}int search(vector<int>& nums, int target) {
int lo = 0, hi = nums.size() - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1; // not found (or `lo` = insertion point)
}def search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1 # not found (or `lo` = insertion point)function search(nums: number[], target: number): number {
let lo = 0, hi = nums.length - 1;
while (lo <= hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (nums[mid] === target) return mid;
else if (nums[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1; // not found (or `lo` = insertion point)
}[1,3,5,7,9], target 7. mid=5(no,<7)→lo=3; mid=7 ⇒ index 3. Three comparisons for five elements.28. Binary Search on the Answer
... Compute a max/min YES ▸ Sorted index or monotonic answer YES
If "is X feasible?" is monotonic (feasible above/below some threshold), binary-search the answer itself and test feasibility. Minimizes a value subject to a constraint (Koko bananas, ship capacity, split array).
int minEatingSpeed(int[] piles, int h) {
int lo = 1, hi = 0;
for (int p : piles) hi = Math.max(hi, p);
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (hoursNeeded(piles, mid) <= h) hi = mid; // feasible → try slower
else lo = mid + 1;
}
return lo;
}
long hoursNeeded(int[] piles, int speed) {
long h = 0;
for (int p : piles) h += (p + speed - 1) / speed; // ceil division
return h;
}int minEatingSpeed(vector<int>& piles, int h) {
int lo = 1, hi = 0;
for (int p : piles) hi = max(hi, p);
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (hoursNeeded(piles, mid) <= h) hi = mid; // feasible → try slower
else lo = mid + 1;
}
return lo;
}
long hoursNeeded(vector<int>& piles, int speed) {
long h = 0;
for (int p : piles) h += (p + speed - 1) / speed; // ceil division
return h;
}def min_eating_speed(piles, h):
lo, hi = 1, 0
for p in piles:
hi = max(hi, p)
while lo < hi:
mid = lo + (hi - lo) // 2
if hours_needed(piles, mid) <= h: # feasible → try slower
hi = mid
else:
lo = mid + 1
return lo
def hours_needed(piles, speed):
h = 0
for p in piles:
h += (p + speed - 1) // speed # ceil division
return hfunction minEatingSpeed(piles: number[], h: number): number {
let lo = 1, hi = 0;
for (const p of piles) hi = Math.max(hi, p);
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (hoursNeeded(piles, mid) <= h) hi = mid; // feasible → try slower
else lo = mid + 1;
}
return lo;
}
function hoursNeeded(piles: number[], speed: number): number {
let h = 0;
for (const p of piles) h += Math.floor((p + speed - 1) / speed); // ceil division
return h;
}piles=[3,6,7,11], h=8. Binary search over speeds 1…11 finds the smallest speed that finishes in ≤8 hours ⇒ 4.🎯 LeetCode practice & worked example
5 · Binary Search
Tests whether you can spot a monotonic "yes/no" boundary — not just in a sorted array but in the space of possible answers — and drive it out in O(log n); the tell is "sorted", "rotated sorted", or "minimize/maximize the smallest value that still works".
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Binary Search ★ | Easy | AmazonGoogleMicrosoft | classic lo/hi midpoint search |
| 2 | Search a 2D Matrix ★ | Medium | AmazonMetaMicrosoft | treat matrix as flattened sorted array |
| 3 | Koko Eating Bananas ★ | Medium | GoogleAmazonBloomberg | binary search on the answer (eating speed) |
| 4 | Find Minimum in Rotated Sorted Array ★ | Medium | AmazonMicrosoftMeta | binary search on the rotation pivot |
| 5 | Search in Rotated Sorted Array ★ | Medium | AmazonMetaMicrosoftBloomberg | identify sorted half, then decide side |
| 6 | Time Based Key-Value Store ★ | Medium | AmazonGoogleTikTok | binary search over timestamps per key |
| 7 | Median of Two Sorted Arrays ★ | Hard | GoogleAmazonApple | binary search on partition point |
| 8 | Find K Closest Elements | Medium | AmazonGoogle | binary search on window start index |
| 9 | Capacity To Ship Packages Within D Days | Medium | AmazonGoogle | binary search on the answer (capacity) |
Worked example — Search in Rotated Sorted Array (LC 33, Medium)
Frequently asked at: Amazon, Meta, Microsoft, Bloomberg
Problem. Given a sorted array rotated at an unknown pivot and a target value, return the index of the target in O(log n) time, or -1 if it is not present.
Idea. At every midpoint, one of the two halves [lo, mid] or [mid, hi] must still be internally sorted even though the whole array isn't. Determine which half is sorted by comparing nums[lo] to nums[mid], then check whether the target falls inside that sorted half's value range to decide which side to keep.
class Solution {
public int search(int[] nums, int target) {
int lo = 0, hi = nums.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[lo] <= nums[mid]) {
if (nums[lo] <= target && target < nums[mid]) {
hi = mid - 1;
} else {
lo = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[hi]) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
}
return -1;
}
}class Solution {
public:
int search(vector<int>& nums, int target) {
int lo = 0, hi = nums.size() - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[lo] <= nums[mid]) {
if (nums[lo] <= target && target < nums[mid]) {
hi = mid - 1;
} else {
lo = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[hi]) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
}
return -1;
}
};class Solution:
def search(self, nums: List[int], target: int) -> int:
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]:
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else:
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1class Solution {
search(nums: number[], target: number): number {
let lo = 0, hi = nums.length - 1;
while (lo <= hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (nums[mid] === target) return mid;
if (nums[lo] <= nums[mid]) {
if (nums[lo] <= target && target < nums[mid]) {
hi = mid - 1;
} else {
lo = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[hi]) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
}
return -1;
}
}Complexity. Time O(log n) Space O(1) — every iteration discards half of the remaining search space.
Walkthrough. Input nums = [4,5,6,7,0,1,2], target = 0:
| lo, mid, hi | nums[lo..hi] view | sorted half | decision |
|---|---|---|---|
| 0, 3, 6 | [4,5,6,7,0,1,2] | left [4,5,6,7] since nums[0]<=nums[3] | target 0 not in [4,7) -> go right, lo=4 |
| 4, 5, 6 | [0,1,2] | left [0,1] since nums[4]<=nums[5] | target 0 in [0,1) -> go left, hi=4 |
| 4, 4, 4 | [0] | nums[mid]==target | return mid = 4 |
Answer: index 4.
🔬 Deeper dive & dry run
Why it works. At any midpoint of a rotated sorted array, at least one half (relative to mid) is guaranteed to be internally sorted. Comparing nums[lo] to nums[mid] identifies which half is sorted; a plain range check then tells you whether the target could be in that sorted half. Either way you can discard one half every iteration, so the search stays O(log n) despite the rotation.
When to reach for it. Arrays described as "sorted but rotated" or, more generally, any search space with a monotonic predicate ("too small / candidate / too big") you can evaluate at a single index without scanning — that predicate is what you're actually binary searching on.
Complexity. O(log n) time — the search space halves every iteration; O(1) space.
Pitfalls & variations. Using strict < instead of <= when comparing nums[lo] and nums[mid] breaks on 2-element windows. Duplicate values (LC81) can make both halves look "equal," defeating the sortedness test and forcing a linear fallback (lo++/hi--). Miscomputing mid±1 causes infinite loops.
Trace input: nums = [5,6,7,8,9,10,1,2,3], target = 3 — find the index of target in a rotated sorted array.
| Step | lo | hi | mid | nums[mid] | which half sorted | target in range? | action |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 8 | — | — | — | — | start |
| 1 | 0 | 8 | 4 | 9 | nums[0]=5 <= 9 → left sorted | 3 in [5,9]? no | lo = mid+1 = 5 |
| 2 | 5 | 8 | 6 | 1 | nums[5]=10 <= 1? no → right sorted | 3 in [1,3]? yes | lo = mid+1 = 7 |
| 3 | 7 | 8 | 7 | 2 | nums[7]=2 <= 2 → left sorted | 3 in [2,2]? no | lo = mid+1 = 8 |
| 4 | 8 | 8 | 8 | 3 | — | nums[mid] == target | return 8 |
Result: return 8 (nums[8] = 3). The trace visits both branches of the algorithm — "left half sorted" (steps 1, 3) and "right half sorted" (step 2) — converging in ⌈log₂9⌉ ≈ 4 iterations.
9 · Heap / Priority Queue
Flowchart leaves: kth smallest/largest merge k sorted lists
The idea
A binary heap gives O(log n) insert and O(log n) extract-min/max with O(1) peek. Reach for it whenever you repeatedly need "the current best/worst among a changing set": top-k, kth element, streaming medians, merging sorted sequences, or always expanding the cheapest option (Dijkstra, Prim).
🧰 API — Priority Queue / Binary Heap methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
// Create
PriorityQueue<Integer> pq = new PriorityQueue<>(); // natural ordering -> MIN-heap
PriorityQueue<Integer> maxPq = new PriorityQueue<>(Collections.reverseOrder()); // max-heap via comparator
PriorityQueue<int[]> byKey = new PriorityQueue<>((a, b) -> a[0] - b[0]); // custom comparator (order by a field)
pq.offer(x); // insert x, O(log n)
pq.add(x); // same as offer(), throws instead of returning false on failure, O(log n)
pq.poll(); // remove & return the head (smallest, or per comparator), O(log n); null if empty
pq.remove(); // same as poll() but throws NoSuchElementException if empty, O(log n)
pq.peek(); // view the head without removing, O(1); null if empty
pq.element(); // same as peek() but throws if empty, O(1)
pq.size(); // number of elements, O(1)
pq.isEmpty(); // true if no elements, O(1)
pq.remove(x); // remove a specific element (linear scan), O(n)
pq.contains(x); // membership check, O(n)
#include <queue>
priority_queue<int> pq; // MAX-heap by default
priority_queue<int, vector<int>, greater<int>> minPq; // MIN-heap via greater<> comparator
priority_queue<pair<int,int>> pairPq; // orders by first element, then second
pq.push(x); // insert x, O(log n)
pq.emplace(args...); // construct element in place and insert, O(log n)
pq.pop(); // remove the top element, O(log n)
pq.top(); // access the top element without removing, O(1)
pq.size(); // number of elements, O(1)
pq.empty(); // true if no elements, O(1)
import heapq
heap = [] # heapq operates on a plain list; it is ALWAYS a MIN-heap
heapq.heappush(heap, x) # push x, O(log n)
heapq.heappop(heap) # pop & return the smallest item, O(log n)
heapq.heapify(heap) # turn an existing list into a heap in-place, O(n)
heapq.heappushpop(heap, x) # push x, then pop & return the smallest (1 pass, faster than push+pop), O(log n)
heapq.heapreplace(heap, x) # pop the smallest, then push x (result may be > x), O(log n)
heap[0] # peek at the smallest item without popping, O(1)
heapq.nlargest(k, heap) # k largest elements as a sorted list, O(n log k)
heapq.nsmallest(k, heap) # k smallest elements as a sorted list, O(n log k)
# Max-heap trick: negate values going in/out (or push (-priority, item) tuples)
heapq.heappush(heap, -x) # push -x
-heapq.heappop(heap) # pop & negate back to get the true max
// No built-in heap / priority queue in the JS/TS standard library. Two common approaches:
// 1) Hand-rolled binary heap backed by an array
class MinHeap<T> {
private data: T[] = [];
constructor(private cmp: (a: T, b: T) => number) {}
push(x: T): void { /* append, then sift up */ } // O(log n)
pop(): T | undefined { /* swap root/last, sift down */ } // O(log n)
peek(): T | undefined { return this.data[0]; } // O(1)
get size(): number { return this.data.length; } // O(1)
}
// 2) A library, e.g. `heap-js` or `@datastructures-js/priority-queue`
import { MinPriorityQueue } from '@datastructures-js/priority-queue';
const pq = new MinPriorityQueue<number>();
pq.enqueue(x); // insert x, O(log n)
pq.dequeue(); // remove & return the smallest, O(log n)
pq.front(); // peek at the smallest, O(1)
pq.size(); // number of elements, O(1)
pq.isEmpty(); // true if empty, O(1)
Brute force — sort everything O(n log n)
Sort the whole array and index the kth element. Fine once, but wasteful for streams or when k ≪ n; a size-k heap does it in O(n log k) and handles data that doesn't fit in memory.
Template A · kth largest with a size-k min-heap O(n log k)
nums— the input array to search.k— which largest element to find (1 = the largest, 2 = second largest, etc.).min— a min-heap capped at sizek, always holding theklargest values seen so far.x— the current element fromnumsbeing inserted into the heap.- Returns — the smallest value among the
klargest — i.e. the kth largest element overall (the top of the min-heap once it settles at sizek).
int findKthLargest(int[] nums, int k){
PriorityQueue<Integer> min = new PriorityQueue<>(); // keeps k largest
for (int x : nums){
min.offer(x);
if (min.size() > k) min.poll(); // evict the smallest
}
return min.peek(); // kth largest overall
}int findKthLargest(vector<int>& nums, int k){
priority_queue<int, vector<int>, greater<int>> minHeap; // keeps k largest
for (int x : nums){
minHeap.push(x);
if (minHeap.size() > (size_t)k) minHeap.pop(); // evict the smallest
}
return minHeap.top(); // kth largest overall
}def find_kth_largest(nums, k):
heap = [] # keeps k largest (min-heap)
for x in nums:
heapq.heappush(heap, x)
if len(heap) > k:
heapq.heappop(heap) # evict the smallest
return heap[0] # kth largest overallfunction findKthLargest(nums: number[], k: number): number {
const min: number[] = []; // keeps k largest
for (const x of nums) {
min.push(x);
min.sort((a, b) => a - b);
if (min.length > k) min.shift(); // evict the smallest
}
return min[0]; // kth largest overall
}Template B · merge k sorted lists O(N log k)
lists— the array of already-sorted linked lists to merge.pq— a min-heap ordered by node value, holding the current head node of each list still in play.dummy— a placeholder head node used only to make building the result list easier; the real answer starts atdummy.next.tail— a pointer to the last node appended so far in the merged output.n— the smallest node just popped frompq; its successor (if any) is pushed back in to keep that list represented.- Returns — the head of one fully merged, sorted linked list combining all input lists.
ListNode mergeKLists(ListNode[] lists){
PriorityQueue<ListNode> pq = new PriorityQueue<>((a,b)->a.val-b.val);
for (ListNode l : lists) if (l!=null) pq.offer(l);
ListNode dummy = new ListNode(0), tail = dummy;
while (!pq.isEmpty()){
ListNode n = pq.poll();
tail.next = n; tail = n;
if (n.next != null) pq.offer(n.next);
}
return dummy.next;
}ListNode* mergeKLists(vector<ListNode*>& lists){
auto cmp = [](ListNode* a, ListNode* b){ return a->val > b->val; };
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq(cmp);
for (ListNode* l : lists) if (l != nullptr) pq.push(l);
ListNode dummy(0), *tail = &dummy;
while (!pq.empty()){
ListNode* n = pq.top(); pq.pop();
tail->next = n; tail = n;
if (n->next != nullptr) pq.push(n->next);
}
return dummy.next;
}def merge_k_lists(lists):
heap = []
for i, l in enumerate(lists):
if l is not None:
heapq.heappush(heap, (l.val, i, l)) # tie-break with index (nodes aren't comparable)
dummy = ListNode(0)
tail = dummy
counter = len(heap)
while heap:
val, idx, n = heapq.heappop(heap)
tail.next = n
tail = n
if n.next is not None:
counter += 1
heapq.heappush(heap, (n.next.val, counter, n.next))
return dummy.nextfunction mergeKLists(lists: (ListNode | null)[]): ListNode | null {
const pq: ListNode[] = [];
for (const l of lists) if (l !== null) pq.push(l);
pq.sort((a, b) => a.val - b.val);
const dummy = new ListNode(0);
let tail = dummy;
while (pq.length) {
const n = pq.shift()!;
tail.next = n; tail = n;
if (n.next !== null) {
pq.push(n.next);
pq.sort((a, b) => a.val - b.val);
}
}
return dummy.next;
}Template C · two heaps for a running median
lo— a max-heap holding the smaller half of all numbers seen so far (its top is the largest of that small half).hi— a min-heap holding the larger half of all numbers seen so far (its top is the smallest of that large half).x— the new number being inserted byadd.funnel/rebalance step— every new value goes intolofirst, then its max is pushed tohi, then sizes are rebalanced solonever holds more than one extra element overhi.- Returns —
addreturns nothing (it just updates the two heaps);median()returns the median of every number added so far, taken from the top(s) oflo/hi.
PriorityQueue<Integer> lo = new PriorityQueue<>(Collections.reverseOrder()); // max-heap, lower half
PriorityQueue<Integer> hi = new PriorityQueue<>(); // min-heap, upper half
void add(int x){
lo.offer(x); hi.offer(lo.poll()); // funnel through to keep order
if (hi.size() > lo.size()) lo.offer(hi.poll()); // rebalance sizes
}
double median(){ return lo.size()>hi.size() ? lo.peek() : (lo.peek()+hi.peek())/2.0; }priority_queue<int> lo; // max-heap, lower half
priority_queue<int, vector<int>, greater<int>> hi; // min-heap, upper half
void add(int x){
lo.push(x); hi.push(lo.top()); lo.pop(); // funnel through to keep order
if (hi.size() > lo.size()){ lo.push(hi.top()); hi.pop(); } // rebalance sizes
}
double median(){ return lo.size()>hi.size() ? lo.top() : (lo.top()+hi.top())/2.0; }lo = [] # max-heap, lower half (store negated values)
hi = [] # min-heap, upper half
def add(x):
heapq.heappush(lo, -x)
heapq.heappush(hi, -heapq.heappop(lo)) # funnel through to keep order
if len(hi) > len(lo):
heapq.heappush(lo, -heapq.heappop(hi)) # rebalance sizes
def median():
if len(lo) > len(hi):
return -lo[0]
return (-lo[0] + hi[0]) / 2.0const lo: number[] = []; // max-heap, lower half
const hi: number[] = []; // min-heap, upper half
function add(x: number): void {
lo.push(x); lo.sort((a, b) => b - a);
hi.push(lo.shift()!); hi.sort((a, b) => a - b); // funnel through to keep order
if (hi.length > lo.length) {
lo.push(hi.shift()!);
lo.sort((a, b) => b - a); // rebalance sizes
}
}
function median(): number {
return lo.length > hi.length ? lo[0] : (lo[0] + hi[0]) / 2.0;
}Worked example — 2nd largest of [3,2,1,5,6,4], k=2
| Read | Heap (min on top) | Action |
|---|---|---|
| 3 | [3] | size 1 |
| 2 | [2,3] | size 2 |
| 1 | [2,3] | add 1 → [1,2,3], size 3 > 2 → poll 1 |
| 5 | [3,5] | add 5 → poll 2 |
| 6 | [5,6] | add 6 → poll 3 |
| 4 | [5,6] | add 4 → poll 4 |
Heap holds the two largest {5,6}; its min (top) = 5 is the 2nd largest.
🔬 Deeper dive & dry run
Why it works. A size-k min-heap maintains the invariant "the heap always holds the k largest elements seen so far, with the smallest of those k sitting at the top". Every new element either grows the heap (while under capacity) or gets compared against the current k-th largest (the heap's min) and only survives if it beats it — so after processing the whole stream, the heap's minimum is exactly the k-th largest overall.
When to reach for it. "Kth largest/smallest", "top-k frequent", "k closest points", or any streaming/online scenario where you can't sort the whole input up front (or want better than O(n log n)). Signal words: "kth", "top k", "k most/least".
Complexity. O(n log k) time — each of n elements does at most one O(log k) push/pop; O(k) space for the heap (vs. O(n log n)/O(n) for full sort).
Pitfalls & variations. Using a max-heap of the full input instead of a min-heap capped at size k throws away the whole point of the technique (back to O(n log n)). Forgetting to pop back down to size k after a push leaves the heap unbounded. Variants: "kth smallest" flips to a max-heap of size k; "k closest points" compares by distance instead of raw value; quickselect is the O(n) average-case alternative when you don't need a running/streaming answer.
Trace input: nums = [7,10,4,3,20,15], k = 2 — find the 2nd largest element using a min-heap capped at size k.
| Step | Element | Heap before | Action | Heap after | Size |
|---|---|---|---|---|---|
| 1 | 7 | [] | push (under capacity) | [7] | 1 |
| 2 | 10 | [7] | push (now at capacity) | [7,10] | 2 |
| 3 | 4 | [7,10] | push then pop-min(4) | [7,10] | 2 |
| 4 | 3 | [7,10] | push then pop-min(3) | [7,10] | 2 |
| 5 | 20 | [7,10] | push then pop-min(7) | [10,20] | 2 |
| 6 | 15 | [10,20] | push then pop-min(10) | [15,20] | 2 |
Result: heap top (min) = 15 = the 2nd largest (full sorted order: 20,15,10,7,4,3). Elements 4 and 3 never survive a single round in the heap because they're immediately the new minimum — the trace shows the heap self-correcting after every push.
🧰 Quick reference templates
12. Heap / Sorting (kth smallest/largest)
... Sorted/monotonic NO ▸ kth smallest/largest YES
For the kth largest, keep a size-k min-heap: the smallest of the k best stays on top and is evicted when a bigger element arrives. O(n log k), better than a full sort when k ≪ n.
int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int x : nums) {
minHeap.offer(x);
if (minHeap.size() > k) minHeap.poll(); // drop smallest
}
return minHeap.peek(); // kth largest
}int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> minHeap;
for (int x : nums) {
minHeap.push(x);
if ((int)minHeap.size() > k) minHeap.pop(); // drop smallest
}
return minHeap.top(); // kth largest
}def find_kth_largest(nums, k):
min_heap = []
for x in nums:
heapq.heappush(min_heap, x)
if len(min_heap) > k:
heapq.heappop(min_heap) # drop smallest
return min_heap[0] # kth largestfunction findKthLargest(nums: number[], k: number): number {
const minHeap: number[] = [];
const push = (val: number) => {
minHeap.push(val);
let i = minHeap.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (minHeap[p] <= minHeap[i]) break;
[minHeap[p], minHeap[i]] = [minHeap[i], minHeap[p]];
i = p;
}
};
const pop = () => {
const top = minHeap[0];
const last = minHeap.pop()!;
if (minHeap.length > 0) {
minHeap[0] = last;
let i = 0;
while (true) {
const l = 2 * i + 1, r = 2 * i + 2;
let s = i;
if (l < minHeap.length && minHeap[l] < minHeap[s]) s = l;
if (r < minHeap.length && minHeap[r] < minHeap[s]) s = r;
if (s === i) break;
[minHeap[s], minHeap[i]] = [minHeap[i], minHeap[s]];
i = s;
}
}
return top;
};
for (const x of nums) {
push(x);
if (minHeap.length > k) pop(); // drop smallest
}
return minHeap[0]; // kth largest
}[3,2,1,5,6,4], k=2. Heap keeps the 2 largest → {5,6}, top = 5 ⇒ 2nd largest is 5.Linked Lists
🎯 LeetCode practice & worked example
9 · Heap / Priority Queue
Tests efficient access to running min/max/median over a stream or collection — the tell is "kth largest/smallest", "top k", "closest k", "streaming median", or scheduling by priority; reach for a min-heap or max-heap sized to k, or twin heaps for medians.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Kth Largest Element in a Stream | Easy | AmazonGoogle | size-k min-heap, top is answer |
| 2 | Last Stone Weight | Easy | AmazonApple | max-heap, repeated pairwise smash |
| 3 | K Closest Points to Origin ★ | Medium | AmazonMicrosoftFacebook | max-heap of size k by distance |
| 4 | Kth Largest Element in an Array ★ | Medium | AmazonMetaMicrosoftGoogle | size-k min-heap or quickselect partition |
| 5 | Task Scheduler ★ | Medium | AmazonFacebookBloomberg | max-heap by frequency + cooldown queue |
| 6 | Design Twitter | Medium | AmazonTwitter | merge-k-lists style heap over timestamped feeds |
| 7 | Find Median from Data Stream ★ | Hard | AmazonGoogleMicrosoft | two heaps (max-heap low half, min-heap high half) |
| 8 | Reorganize String | Medium | AmazonFacebook | max-heap by frequency, alternate placement |
| 9 | Single-Threaded CPU | Medium | AmazonGoogle | sort by enqueue time + min-heap by (duration, index) |
| 10 | IPO | Hard | AmazonGoogle | sort by capital + max-heap of affordable profits |
Worked example — Kth Largest Element in an Array (LC 215, Medium)
Frequently asked at: Amazon, Meta, Microsoft, Google.
Problem. Given an integer array nums and an integer k, return the kth largest element in the array (counting from the largest, so k=1 is the maximum), without fully sorting the array.
Idea. Maintain a min-heap capped at size k. Push every number; whenever the heap grows past k, pop the smallest. After processing all n numbers, the heap holds exactly the k largest elements seen, and its smallest member (the top of a min-heap) is the kth largest overall.
import java.util.PriorityQueue;
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int n : nums) {
minHeap.offer(n);
if (minHeap.size() > k) {
minHeap.poll();
}
}
return minHeap.peek();
}
}
#include <queue>
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> minHeap;
for (int n : nums) {
minHeap.push(n);
if (minHeap.size() > k) {
minHeap.pop();
}
}
return minHeap.top();
}
};import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
minHeap = []
for n in nums:
heapq.heappush(minHeap, n)
if len(minHeap) > k:
heapq.heappop(minHeap)
return minHeap[0]class Solution {
findKthLargest(nums: number[], k: number): number {
const minHeap: number[] = [];
for (const n of nums) {
minHeap.push(n);
minHeap.sort((a, b) => a - b);
if (minHeap.length > k) {
minHeap.shift();
}
}
return minHeap[0];
}
}Complexity. Time O(n log k) Space O(k) — each of n offers/polls costs O(log k) on a heap bounded to size k; a quickselect partition alternative achieves average O(n) time with O(1) extra space.
Walkthrough. Input: nums = [3,2,1,5,6,4], k = 2.
| Number processed | Heap contents (min-heap) | Action |
|---|---|---|
| 3 | [3] | size ≤ k, keep |
| 2 | [2,3] | size ≤ k, keep |
| 1 | [1,2,3] → pop 1 → [2,3] | size > k, pop smallest |
| 5 | [2,3,5] → pop 2 → [3,5] | size > k, pop smallest |
| 6 | [3,5,6] → pop 3 → [5,6] | size > k, pop smallest |
| 4 | [4,5,6] → pop 4 → [5,6] | size > k, pop smallest |
| Result | heap = [5,6], top = 5 | 5 is the 2nd largest element |
🔬 Deeper dive & dry run
Why it works. A min-heap capped at size k always holds exactly the k largest elements seen so far, with the smallest of those k sitting at the root. Any new element only needs to be compared against that single root: if it's bigger, it displaces the current minimum of the kept set; if not, it can't possibly be in the top k and is discarded. You never re-examine the other k-1 kept elements or anything already discarded.
When to reach for it. "Top-k" / "kth largest-or-smallest" problems, especially streaming or one-pass, where a full sort (O(n log n)) does more work than needed because you only care about k extremal elements, not a total order.
Complexity. O(n log k) time — n pushes/pops, each O(log k) on a heap bounded to size k; O(k) space, which is far better than full sort when k ≪ n.
Pitfalls & variations. Building a max-heap of all n elements instead of a min-heap capped at k works but costs O(n log n) time and O(n) space, missing the whole point. Popping before checking size > k is an off-by-one that leaves k+1 elements. Confusing "kth largest" (1-indexed from the top, e.g. k=1 is the max) with "kth smallest" flips the heap type needed.
Trace input: nums = [3,2,1,5,6,4], k = 2 — find the 2nd largest value.
| Step | num | heap before (min-heap) | action | heap after |
|---|---|---|---|---|
| 0 | — | [] | init | [] |
| 1 | 3 | [] | push 3 (size 1 ≤ k) | [3] |
| 2 | 2 | [3] | push 2 (size 2 = k) | [2,3] |
| 3 | 1 | [2,3] | push 1 → size 3 > k, pop min 1 | [2,3] |
| 4 | 5 | [2,3] | push 5 → size 3 > k, pop min 2 | [3,5] |
| 5 | 6 | [3,5] | push 6 → size 3 > k, pop min 3 | [5,6] |
| 6 | 4 | [5,6] | push 4 → size 3 > k, pop min 4 | [5,6] |
Result: Final heap [5,6]; its root (min) 5 is the 2nd largest element. Notice numbers 1 and 4 get pushed and immediately popped straight back out — they never belonged in the top-k, so the heap never grows past size k.
10 · Two Pointers
Flowchart leaves: linked-list fast/slow array partitioning two-sequence merge find indices O(1) memory
The idea
Two indices move through the data with a rule, replacing a nested loop with a single linear pass and O(1) memory. Three sub-patterns: opposite ends converging (sorted two-sum, container with most water, palindrome), fast/slow on a linked list (cycle, middle, nth-from-end), and one pointer per sequence (merge, count pairs across two sorted arrays).
Brute force — nested loops O(n²)
Check every pair (i,j). The monotonic structure (usually "the array is sorted") means when a sum is too big you never need a larger j — so one pointer only ever moves inward, collapsing the double loop.
Template A · opposite ends — two-sum on a sorted array O(n)
a— the sorted (ascending) array being searchedtarget— the sum the two chosen elements must add up tolo— pointer starting at the left, smallest-value endhi— pointer starting at the right, largest-value endsum— a[lo] + a[hi], compared against target to decide which pointer moves- Returns — the indices [lo, hi] of a pair that sums to target, or [-1, -1] if no pair works
int[] twoSum(int[] a, int target){ // a sorted ascending
int lo = 0, hi = a.length - 1;
while (lo < hi){
int sum = a[lo] + a[hi];
if (sum == target) return new int[]{lo, hi};
else if (sum < target) lo++; // need larger → move left pointer up
else hi--; // need smaller → move right pointer down
}
return new int[]{-1,-1};
}vector<int> twoSum(vector<int>& a, int target){ // a sorted ascending
int lo = 0, hi = a.size() - 1;
while (lo < hi){
int sum = a[lo] + a[hi];
if (sum == target) return {lo, hi};
else if (sum < target) lo++; // need larger → move left pointer up
else hi--; // need smaller → move right pointer down
}
return {-1, -1};
}def two_sum(a, target): # a sorted ascending
lo, hi = 0, len(a) - 1
while lo < hi:
s = a[lo] + a[hi]
if s == target:
return [lo, hi]
elif s < target:
lo += 1 # need larger → move left pointer up
else:
hi -= 1 # need smaller → move right pointer down
return [-1, -1]function twoSum(a: number[], target: number): number[] { // a sorted ascending
let lo = 0, hi = a.length - 1;
while (lo < hi) {
const sum = a[lo] + a[hi];
if (sum === target) return [lo, hi];
else if (sum < target) lo++; // need larger → move left pointer up
else hi--; // need smaller → move right pointer down
}
return [-1, -1];
}Template B · fast/slow — cycle detection & middle
head— the first node of the linked listslow— pointer that advances one node per stepfast— pointer that advances two nodes per step- Returns — true if fast ever laps slow (a cycle exists), false if fast reaches the end first
boolean hasCycle(ListNode head){
ListNode slow = head, fast = head;
while (fast != null && fast.next != null){
slow = slow.next; fast = fast.next.next; // 1x vs 2x speed
if (slow == fast) return true; // they meet inside a loop
}
return false;
}bool hasCycle(ListNode* head){
ListNode *slow = head, *fast = head;
while (fast != nullptr && fast->next != nullptr){
slow = slow->next; fast = fast->next->next; // 1x vs 2x speed
if (slow == fast) return true; // they meet inside a loop
}
return false;
}def has_cycle(head):
slow, fast = head, head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next # 1x vs 2x speed
if slow == fast:
return True # they meet inside a loop
return Falsefunction hasCycle(head: ListNode | null): boolean {
let slow = head, fast = head;
while (fast !== null && fast.next !== null) {
slow = slow!.next; fast = fast.next.next; // 1x vs 2x speed
if (slow === fast) return true; // they meet inside a loop
}
return false;
}Template C · in-place partition — Dutch national flag (sort 0/1/2)
a— array containing only the values 0, 1, and 2, rearranged in placelow— boundary marking where the block of 0s endsmid— index of the element currently being classifiedhigh— boundary marking where the block of 2s begins- Returns — nothing (void) — a ends up reordered as all 0s, then all 1s, then all 2s
void sortColors(int[] a){
int low = 0, mid = 0, high = a.length - 1;
while (mid <= high){
if (a[mid]==0) swap(a, low++, mid++);
else if (a[mid]==1) mid++;
else swap(a, mid, high--); // 2 → send to the back
}
}void sortColors(vector<int>& a){
int low = 0, mid = 0, high = a.size() - 1;
while (mid <= high){
if (a[mid]==0) swap(a[low++], a[mid++]);
else if (a[mid]==1) mid++;
else swap(a[mid], a[high--]); // 2 → send to the back
}
}def sort_colors(a):
low, mid, high = 0, 0, len(a) - 1
while mid <= high:
if a[mid] == 0:
a[low], a[mid] = a[mid], a[low]
low += 1
mid += 1
elif a[mid] == 1:
mid += 1
else:
a[mid], a[high] = a[high], a[mid] # 2 → send to the back
high -= 1function sortColors(a: number[]): void {
let low = 0, mid = 0, high = a.length - 1;
while (mid <= high) {
if (a[mid] === 0) {
[a[low], a[mid]] = [a[mid], a[low]];
low++; mid++;
} else if (a[mid] === 1) {
mid++;
} else {
[a[mid], a[high]] = [a[high], a[mid]]; // 2 → send to the back
high--;
}
}
}Template D · one pointer per sorted array — count pairs ≤ target
a, b— two sorted arrays whose elements are paired uptarget— the maximum sum a pair is allowed to havej— pointer into b that only ever moves left, marking the largest b-index that still keeps a[i] + b[j] within targetcount— running total of qualifying pairs found so far- Returns — the total number of pairs (one value from a, one from b) whose sum is at most target
long countPairs(int[] a, int[] b, int target){ // both sorted
long count = 0; int j = b.length - 1;
for (int i = 0; i < a.length; i++){
while (j >= 0 && a[i] + b[j] > target) j--;
count += (j + 1); // b[0..j] all pair with a[i]
}
return count;
}long long countPairs(vector<int>& a, vector<int>& b, int target){ // both sorted
long long count = 0; int j = b.size() - 1;
for (int i = 0; i < (int)a.size(); i++){
while (j >= 0 && a[i] + b[j] > target) j--;
count += (j + 1); // b[0..j] all pair with a[i]
}
return count;
}def count_pairs(a, b, target): # both sorted
count = 0
j = len(b) - 1
for i in range(len(a)):
while j >= 0 and a[i] + b[j] > target:
j -= 1
count += (j + 1) # b[0..j] all pair with a[i]
return countfunction countPairs(a: number[], b: number[], target: number): number { // both sorted
let count = 0;
let j = b.length - 1;
for (let i = 0; i < a.length; i++) {
while (j >= 0 && a[i] + b[j] > target) j--;
count += (j + 1); // b[0..j] all pair with a[i]
}
return count;
}Worked example — sorted two-sum, target 9 on [2,7,11,15]
| lo | hi | a[lo]+a[hi] | vs 9 | move |
|---|---|---|---|---|
| 0 | 3 | 2+15=17 | > | hi→2 |
| 0 | 2 | 2+11=13 | > | hi→1 |
| 0 | 1 | 2+7=9 | = | return [0,1] |
Three steps instead of the six pairs a double loop would test. Each pointer only moves one direction, so total work is linear.
🔬 Deeper dive & dry run
Why it works. On a sorted array, two pointers l and r maintain the invariant "every pair already skipped past could not possibly sum to target, given the array's order". If nums[l] + nums[r] > target, decreasing r is the only way to shrink the sum (since all values between the old and new r are ≥ the new nums[r], pairing any of them with l would still overshoot); symmetric logic justifies advancing l when the sum is too small. Each move provably eliminates a whole row/column of the pair matrix, never a false eliminate.
When to reach for it. "Sorted array, find a pair/triplet summing to X", "container with most water", "remove duplicates in place", palindrome checks — anything where the array is sorted (or can be) and you're relating a value at one end to a value at the other.
Complexity. O(n) time — l and r together traverse the array at most once; O(1) extra space, no hash map needed (unlike the unsorted two-sum).
Pitfalls & variations. Applying this technique on an unsorted array without sorting first breaks the elimination argument entirely. Using l < r vs. l ≤ r as the loop bound matters when the array has an odd middle element that could pair with itself only if duplicates are allowed. Variants: 3Sum (fix one element, two-pointer the rest), container-with-most-water (move the pointer at the shorter wall, not the smaller value).
Trace input: nums = [-6,-4,-1,0,2,3,5,7,9,11] (sorted), target = 9 — find a pair of indices whose values sum to target.
| Step | l | r | nums[l] | nums[r] | sum | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 9 | -6 | 11 | 5 | 5 < 9 → l++ |
| 2 | 1 | 9 | -4 | 11 | 7 | 7 < 9 → l++ |
| 3 | 2 | 9 | -1 | 11 | 10 | 10 > 9 → r-- |
| 4 | 2 | 8 | -1 | 9 | 8 | 8 < 9 → l++ |
| 5 | 3 | 8 | 0 | 9 | 9 | 9 == 9 → found |
Result: pair found at indices (3,8), values (0,9). The trace shows l advancing three separate times and r retreating once — every move is justified purely by whether the current sum overshot or undershot 9, never by re-checking a skipped pair.
🧰 Quick reference templates
34. Two Pointers — Two Sequences
... Multiple sequences YES ▸ Monotonic condition YES
Two sorted sequences + a monotonic condition ⇒ walk one pointer per sequence. As one pointer advances the other only moves one direction, giving O(n+m).
// count pairs (i,j) with a[i] + b[j] <= target, both arrays sorted ascending
long countPairs(int[] a, int[] b, int target) {
long count = 0;
int j = b.length - 1;
for (int i = 0; i < a.length; i++) {
while (j >= 0 && a[i] + b[j] > target) j--; // shrink from the top
count += (j + 1); // all b[0..j] pair with a[i]
}
return count;
}// count pairs (i,j) with a[i] + b[j] <= target, both arrays sorted ascending
long long countPairs(vector<int>& a, vector<int>& b, int target) {
long long count = 0;
int j = (int)b.size() - 1;
for (int i = 0; i < (int)a.size(); i++) {
while (j >= 0 && a[i] + b[j] > target) j--; // shrink from the top
count += (j + 1); // all b[0..j] pair with a[i]
}
return count;
}# count pairs (i,j) with a[i] + b[j] <= target, both arrays sorted ascending
def count_pairs(a, b, target):
count = 0
j = len(b) - 1
for i in range(len(a)):
while j >= 0 and a[i] + b[j] > target:
j -= 1 # shrink from the top
count += (j + 1) # all b[0..j] pair with a[i]
return count// count pairs (i,j) with a[i] + b[j] <= target, both arrays sorted ascending
function countPairs(a: number[], b: number[], target: number): number {
let count = 0;
let j = b.length - 1;
for (let i = 0; i < a.length; i++) {
while (j >= 0 && a[i] + b[j] > target) j--; // shrink from the top
count += (j + 1); // all b[0..j] pair with a[i]
}
return count;
}a=[1,2,3], b=[1,4,5], target 5. a=1 pairs b[0,1] (2); a=2 pairs b[0] (1); a=3 pairs b[0] (1) ⇒ 4.36. Two Pointers — Find Indices
... Find/enumerate indices YES ▸ Monotonic condition YES
Return the actual indices/pair that satisfies a monotonic sum condition on a sorted array. Move lo up or hi down based on whether the current sum is too small or too big.
int[] twoSumSorted(int[] nums, int target) { // nums sorted ascending
int lo = 0, hi = nums.length - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
if (sum == target) return new int[]{lo, hi};
else if (sum < target) lo++; // need bigger
else hi--; // need smaller
}
return new int[]{-1, -1};
}vector<int> twoSumSorted(vector<int>& nums, int target) { // nums sorted ascending
int lo = 0, hi = nums.size() - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
if (sum == target) return {lo, hi};
else if (sum < target) lo++; // need bigger
else hi--; // need smaller
}
return {-1, -1};
}def two_sum_sorted(nums, target): # nums sorted ascending
lo, hi = 0, len(nums) - 1
while lo < hi:
total = nums[lo] + nums[hi]
if total == target:
return [lo, hi]
elif total < target:
lo += 1 # need bigger
else:
hi -= 1 # need smaller
return [-1, -1]function twoSumSorted(nums: number[], target: number): number[] { // nums sorted ascending
let lo = 0, hi = nums.length - 1;
while (lo < hi) {
const sum = nums[lo] + nums[hi];
if (sum === target) return [lo, hi];
else if (sum < target) lo++; // need bigger
else hi--; // need smaller
}
return [-1, -1];
}[2,7,11,15], target 9. 2+15=17>9→hi--; 2+11=13>9→hi--; 2+7=9 ⇒ indices [0,1].Constant Memory
37. Two Pointers — O(1) Memory
... Find indices NO ▸ Need O(1) memory YES ▸ Monotonic condition YES
Optimize over a range using only two indices and no extra storage (container-with-most-water, valid palindrome). Shrink from the side that can't possibly improve the answer.
int maxArea(int[] height) { // container with most water
int lo = 0, hi = height.length - 1, best = 0;
while (lo < hi) {
best = Math.max(best, Math.min(height[lo], height[hi]) * (hi - lo));
if (height[lo] < height[hi]) lo++; // move the shorter wall inward
else hi--;
}
return best;
}int maxArea(vector<int>& height) { // container with most water
int lo = 0, hi = height.size() - 1, best = 0;
while (lo < hi) {
best = max(best, min(height[lo], height[hi]) * (hi - lo));
if (height[lo] < height[hi]) lo++; // move the shorter wall inward
else hi--;
}
return best;
}def max_area(height): # container with most water
lo, hi = 0, len(height) - 1
best = 0
while lo < hi:
best = max(best, min(height[lo], height[hi]) * (hi - lo))
if height[lo] < height[hi]:
lo += 1 # move the shorter wall inward
else:
hi -= 1
return bestfunction maxArea(height: number[]): number { // container with most water
let lo = 0, hi = height.length - 1, best = 0;
while (lo < hi) {
best = Math.max(best, Math.min(height[lo], height[hi]) * (hi - lo));
if (height[lo] < height[hi]) lo++; // move the shorter wall inward
else hi--;
}
return best;
}[1,8,6,2,5,4,8,3,7]. Best pair is heights 8 & 7, width 7 ⇒ area 49. Only two ints of state.Parsing Symbols
🎯 LeetCode practice & worked example
2 · Two Pointers
Tests whether you can collapse an O(n²) nested-loop search into O(n) by walking two indices toward (or away from) each other over a sorted or partitionable array — the tell is "sorted array" plus "pair/triplet that satisfies a condition".
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Valid Palindrome | Easy | AmazonMicrosoftMeta | two pointers skip non-alphanumeric |
| 2 | Two Sum II - Input Array Is Sorted ★ | Medium | AmazonAdobeMicrosoft | converge pointers on sorted array |
| 3 | 3Sum ★ | Medium | AmazonMetaMicrosoft | sort + fixed anchor + two pointers |
| 4 | Container With Most Water ★ | Medium | AmazonGoogleBloomberg | move shorter wall inward greedily |
| 5 | Trapping Rain Water ★ | Hard | AmazonGoogleApple | two pointers with running left/right max |
| 6 | Remove Duplicates from Sorted Array | Easy | AmazonMicrosoftMeta | in-place write pointer |
| 7 | Sort Colors | Medium | AmazonMetaGoogle | Dutch national flag three pointers |
| 8 | Boats to Save People | Medium | AmazonGoogle | greedy two pointers on sorted weights |
Worked example — 3Sum (LC 15, Medium)
Frequently asked at: Amazon, Meta, Microsoft, Bloomberg
Problem. Given an integer array, return all unique triplets [a, b, c] such that a + b + c == 0.
Idea. Sort the array first. Fix each element nums[i] as the smallest of the triplet, then run the classic two-pointer sweep on the remaining sorted suffix to find pairs summing to -nums[i], skipping duplicate values at every level to keep results unique.
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
if (nums[i] > 0) break;
int lo = i + 1, hi = nums.length - 1;
while (lo < hi) {
int sum = nums[i] + nums[lo] + nums[hi];
if (sum < 0) {
lo++;
} else if (sum > 0) {
hi--;
} else {
res.add(Arrays.asList(nums[i], nums[lo], nums[hi]));
lo++;
hi--;
while (lo < hi && nums[lo] == nums[lo - 1]) lo++;
while (lo < hi && nums[hi] == nums[hi + 1]) hi--;
}
}
}
return res;
}
}class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> res;
for (int i = 0; i < (int)nums.size() - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
if (nums[i] > 0) break;
int lo = i + 1, hi = nums.size() - 1;
while (lo < hi) {
int sum = nums[i] + nums[lo] + nums[hi];
if (sum < 0) {
lo++;
} else if (sum > 0) {
hi--;
} else {
res.push_back({nums[i], nums[lo], nums[hi]});
lo++;
hi--;
while (lo < hi && nums[lo] == nums[lo - 1]) lo++;
while (lo < hi && nums[hi] == nums[hi + 1]) hi--;
}
}
}
return res;
}
};class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
if nums[i] > 0:
break
lo, hi = i + 1, len(nums) - 1
while lo < hi:
total = nums[i] + nums[lo] + nums[hi]
if total < 0:
lo += 1
elif total > 0:
hi -= 1
else:
res.append([nums[i], nums[lo], nums[hi]])
lo += 1
hi -= 1
while lo < hi and nums[lo] == nums[lo - 1]:
lo += 1
while lo < hi and nums[hi] == nums[hi + 1]:
hi -= 1
return resclass Solution {
threeSum(nums: number[]): number[][] {
nums.sort((a, b) => a - b);
const res: number[][] = [];
for (let i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue;
if (nums[i] > 0) break;
let lo = i + 1, hi = nums.length - 1;
while (lo < hi) {
const sum = nums[i] + nums[lo] + nums[hi];
if (sum < 0) {
lo++;
} else if (sum > 0) {
hi--;
} else {
res.push([nums[i], nums[lo], nums[hi]]);
lo++;
hi--;
while (lo < hi && nums[lo] === nums[lo - 1]) lo++;
while (lo < hi && nums[hi] === nums[hi + 1]) hi--;
}
}
}
return res;
}
}Complexity. Time O(n²) Space O(1) extra — O(n log n) sort plus an O(n) two-pointer sweep for each of n anchors.
Walkthrough. Input [-1,0,1,2,-1,-4] sorts to [-4,-1,-1,0,1,2]:
| i (anchor) | lo, hi | sum | action |
|---|---|---|---|
| i=0 (-4) | lo=1(-1), hi=5(2) | -3 | sum<0, lo++ ... no triplet found for -4 |
| i=1 (-1) | lo=2(-1), hi=5(2) | 0 | record [-1,-1,2], then lo++, hi-- |
| i=1 (-1) | lo=3(0), hi=4(1) | 0 | record [-1,0,1], lo/hi cross, stop |
| i=2 (-1) | skipped | - | duplicate of nums[1], continue |
Result: [[-1,-1,2], [-1,0,1]].
🔬 Deeper dive & dry run
Why it works. After sorting, fixing the smallest element at index i and sweeping l/r inward keeps the triplet sum monotonic in each pointer — moving l right only increases the sum, moving r left only decreases it. That monotonicity means no combination is ever re-checked, turning an O(n²) inner search into O(n) per fixed i.
When to reach for it. "Find pairs/triples with a target sum or relationship" on a sorted (or sortable) array, especially when brute force is O(n²) or O(n³) and duplicate results must be deduped — 3Sum, 3Sum Closest, Container With Most Water, pair sum in a sorted array.
Complexity. Time O(n²): O(n log n) sort + O(n) outer loop × O(n) inner two-pointer sweep. Space O(1) extra (or O(log n) for the sort's recursion, depending on implementation).
Pitfalls & variations. Forgetting to skip duplicate values for i, l, and r — three separate dedup checks, easy to miss one and emit duplicate triplets; not sorting first (two pointers require sortedness to guarantee monotonic sum movement); letting i run past n-3, leaving fewer than two elements for l/r.
Trace input: nums = [-1, 0, 1, 2, -1, -4] → sorted [-4, -1, -1, 0, 1, 2] — find all unique triplets summing to 0 (classic LC15 example).
| Step | i | l | r | sum | Action |
|---|---|---|---|---|---|
| 0 | sort | — | — | — | sorted = [-4,-1,-1,0,1,2] |
| 1 | 0 (-4) | 1 | 5 | -4-1+2=-3 | -3<0 → l++ |
| 2 | 0 (-4) | 2 | 5 | -4-1+2=-3 | -3<0 → l++ |
| 3 | 0 (-4) | 3 | 5 | -4+0+2=-2 | -2<0 → l++ |
| 4 | 0 (-4) | 4 | 5 | -4+1+2=-1 | -1<0 → l++ → l==r, done with i=0 |
| 5 | 1 (-1) | 2 | 5 | -1-1+2=0 | match [-1,-1,2] → l++, r-- |
| 6 | 1 (-1) | 3 | 4 | -1+0+1=0 | match [-1,0,1] → l++, r-- → l>r, done with i=1 |
| 7 | 2 (-1) | — | — | — | duplicate of nums[1] → skip i |
| 8 | 3 (0) | 4 | 5 | 0+1+2=3 | 3>0 → r-- → l==r, loop ends (i at n-3) |
Result: [[-1,-1,2], [-1,0,1]] — matches LeetCode's own example exactly. The trace shows the dedup skip at i=2 and the strictly monotonic pointer moves that guarantee every valid triplet is found exactly once.
11 · Sliding Window
Flowchart leaf: maintain a valid window (subarray/substring)
The idea
For "longest/shortest/count of contiguous subarray or substring with property P", keep a window [left, right]. Grow right to include new elements; when the window violates P, shrink from left until valid again. Each index enters and leaves at most once → O(n). Works when extending never fixes a violation (i.e., the property is monotonic in window size).
The universal shape
left— start index of the current windowright— end index of the current window, extended one element at a timeadd()— placeholder step: fold a[right] into the window's tracked stateinvalid()— placeholder check: does the window currently break the required property?remove()— placeholder step: drop a[left] from the window's tracked state while shrinkingbest— largest valid window size seen so far- Returns — (as a pattern) the best valid window size found over the whole scan
int left = 0;
for (int right = 0; right < n; right++){
add(a[right]); // extend window
while (invalid()){ remove(a[left]); left++; } // shrink to restore validity
best = Math.max(best, right - left + 1); // window is valid here
}int left = 0;
for (int right = 0; right < n; right++){
add(a[right]); // extend window
while (invalid()){ remove(a[left]); left++; } // shrink to restore validity
best = max(best, right - left + 1); // window is valid here
}left = 0
for right in range(n):
add(a[right]) # extend window
while invalid():
remove(a[left])
left += 1 # shrink to restore validity
best = max(best, right - left + 1) # window is valid herelet left = 0;
for (let right = 0; right < n; right++) {
add(a[right]); // extend window
while (invalid()) { remove(a[left]); left++; } // shrink to restore validity
best = Math.max(best, right - left + 1); // window is valid here
}Brute force — every subarray O(n²) or O(n³)
Enumerate all O(n²) start/end pairs and re-scan each to test P. The window reuses work between adjacent ranges instead of recomputing from scratch.
Template A · variable window — longest substring without repeats O(n)
s— the input string being scannedlast— array mapping a character to the most recent index where it was seen (-1 if never)left— start of the current repeat-free windowright— end of the current window, the character just examinedc— the character at position rightbest— length of the longest repeat-free window found so far- Returns — the length of the longest substring with no repeated characters
int lengthOfLongestSubstring(String s){
int[] last = new int[128]; Arrays.fill(last, -1);
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++){
char c = s.charAt(right);
if (last[c] >= left) left = last[c] + 1; // jump past the earlier copy
last[c] = right;
best = Math.max(best, right - left + 1);
}
return best;
}int lengthOfLongestSubstring(string s){
vector<int> last(128, -1);
int left = 0, best = 0;
for (int right = 0; right < (int)s.length(); right++){
char c = s[right];
if (last[c] >= left) left = last[c] + 1; // jump past the earlier copy
last[c] = right;
best = max(best, right - left + 1);
}
return best;
}def length_of_longest_substring(s):
last = [-1] * 128
left = best = 0
for right, c in enumerate(s):
if last[ord(c)] >= left:
left = last[ord(c)] + 1 # jump past the earlier copy
last[ord(c)] = right
best = max(best, right - left + 1)
return bestfunction lengthOfLongestSubstring(s: string): number {
const last = new Array(128).fill(-1);
let left = 0, best = 0;
for (let right = 0; right < s.length; right++) {
const c = s.charCodeAt(right);
if (last[c] >= left) left = last[c] + 1; // jump past the earlier copy
last[c] = right;
best = Math.max(best, right - left + 1);
}
return best;
}Template B · fixed window — max sum of size k
a— the input arrayk— the fixed number of elements the window must containsum— running total of the current k-element windowbest— largest window sum seen so far- Returns — the maximum sum of any k consecutive elements
int maxSum(int[] a, int k){
int sum = 0, best;
for (int i = 0; i < k; i++) sum += a[i];
best = sum;
for (int i = k; i < a.length; i++){
sum += a[i] - a[i-k]; // slide: add new, drop oldest
best = Math.max(best, sum);
}
return best;
}int maxSum(vector<int>& a, int k){
int sum = 0, best;
for (int i = 0; i < k; i++) sum += a[i];
best = sum;
for (int i = k; i < (int)a.size(); i++){
sum += a[i] - a[i-k]; // slide: add new, drop oldest
best = max(best, sum);
}
return best;
}def max_sum(a, k):
total = sum(a[:k])
best = total
for i in range(k, len(a)):
total += a[i] - a[i - k] # slide: add new, drop oldest
best = max(best, total)
return bestfunction maxSum(a: number[], k: number): number {
let sum = 0;
for (let i = 0; i < k; i++) sum += a[i];
let best = sum;
for (let i = k; i < a.length; i++) {
sum += a[i] - a[i - k]; // slide: add new, drop oldest
best = Math.max(best, sum);
}
return best;
}Template C · counting windows — "at most K" trick for "exactly K"
a— the input arrayk— the maximum number of distinct values a window may containfreq— map from a value to how many times it currently appears in the windowleft— start of the window, pushed forward whenever there are too many distinct valuesright— end of the window, the element just addedv— the value being dropped from the window as it shrinkscount— running count of valid windows ending at each right- Returns — the number of subarrays with at most k distinct values (subtracting atMost(K-1) from atMost(K) gives exactly K)
// # subarrays with exactly K distinct = atMost(K) - atMost(K-1)
int atMost(int[] a, int k){
Map<Integer,Integer> freq = new HashMap<>();
int left = 0, count = 0;
for (int right = 0; right < a.length; right++){
freq.merge(a[right], 1, Integer::sum);
while (freq.size() > k){
int v = a[left++];
if (freq.merge(v, -1, Integer::sum) == 0) freq.remove(v);
}
count += right - left + 1; // # valid windows ending at right
}
return count;
}// # subarrays with exactly K distinct = atMost(K) - atMost(K-1)
int atMost(vector<int>& a, int k){
unordered_map<int,int> freq;
int left = 0, count = 0;
for (int right = 0; right < (int)a.size(); right++){
freq[a[right]]++;
while ((int)freq.size() > k){
int v = a[left++];
if (--freq[v] == 0) freq.erase(v);
}
count += right - left + 1; // # valid windows ending at right
}
return count;
}# # subarrays with exactly K distinct = at_most(K) - at_most(K-1)
def at_most(a, k):
freq = {}
left = count = 0
for right in range(len(a)):
freq[a[right]] = freq.get(a[right], 0) + 1
while len(freq) > k:
v = a[left]
left += 1
freq[v] -= 1
if freq[v] == 0:
del freq[v]
count += right - left + 1 # # valid windows ending at right
return count// # subarrays with exactly K distinct = atMost(K) - atMost(K-1)
function atMost(a: number[], k: number): number {
const freq = new Map<number, number>();
let left = 0, count = 0;
for (let right = 0; right < a.length; right++) {
freq.set(a[right], (freq.get(a[right]) ?? 0) + 1);
while (freq.size > k) {
const v = a[left++];
const c = freq.get(v)! - 1;
if (c === 0) freq.delete(v); else freq.set(v, c);
}
count += right - left + 1; // # valid windows ending at right
}
return count;
}Worked example — longest no-repeat substring of "abcabcbb"
| right | char | left before → after | window | best |
|---|---|---|---|---|
| 0 | a | 0→0 | a | 1 |
| 1 | b | 0→0 | ab | 2 |
| 2 | c | 0→0 | abc | 3 |
| 3 | a | 0→1 | bca | 3 |
| 4 | b | 1→2 | cab | 3 |
| 5 | c | 2→3 | abc | 3 |
| 6 | b | 3→5 | cb | 3 |
| 7 | b | 5→7 | b | 3 |
Answer 3. left only ever moves right, so both pointers together do ≤ 2n steps.
🔬 Deeper dive & dry run
Why it works. The window [left, right] maintains the invariant "no duplicate characters inside the window." When extending right introduces a duplicate of some character c, the window's left edge only needs to jump to lastSeen[c] + 1 — everything before that still satisfies the invariant, and skipping character-by-character would revisit states already proven invalid. A hash map of last-seen indices turns each shrink into O(1) instead of a linear scan.
When to reach for it. Look for "contiguous subarray/substring" combined with a constraint that can only get worse as the window grows (distinct chars, sum ≤ target, at most k distinct, etc.) — i.e. the constraint is monotonic in window size, so once it breaks, only shrinking from the left can fix it.
Complexity. O(n) time — left and right each move forward at most n times total; O(min(n, Σ)) space for the last-seen map/set, where Σ is the alphabet size.
Pitfalls & variations. Forgetting to move left directly to lastSeen[c] + 1 (instead of one step at a time) turns O(n) into O(n²). Confusing "at most k distinct" (shrink while count > k) with "no repeats" (shrink while duplicate exists) is a common mix-up. Variants: fixed-size window (no left recompute, just slide), and "shrink-while-valid" windows that maximize rather than minimize.
Trace input: s = "abcabcbb" — find the length of the longest substring with no repeated characters.
| right | char | lastSeen before | left | window | len | best |
|---|---|---|---|---|---|---|
| 0 | a | - | 0 | [0,0] | 1 | 1 |
| 1 | b | - | 0 | [0,1] | 2 | 2 |
| 2 | c | - | 0 | [0,2] | 3 | 3 |
| 3 | a | 0 (>=left) | 1 | [1,3] | 3 | 3 |
| 4 | b | 1 (>=left) | 2 | [2,4] | 3 | 3 |
| 5 | c | 2 (>=left) | 3 | [3,5] | 3 | 3 |
| 6 | b | 4 (>=left) | 5 | [5,6] | 2 | 3 |
| 7 | b | 6 (>=left) | 7 | [7,7] | 1 | 3 |
Result: Longest length is 3 (e.g. "abc"). The trace shows the window never shrinks more than one step past the offending duplicate, and left is monotonic non-decreasing throughout — the source of the O(n) bound.
🧰 Quick reference templates
25. Sliding Window
... Subarray/substring YES ▸ Maintain a valid window YES
Grow the right edge; when the window breaks a constraint, shrink from the left. Each index enters and leaves once ⇒ O(n). Great for longest/shortest window with a property.
int lengthOfLongestSubstring(String s) { // longest substring, no repeats
int[] last = new int[128];
Arrays.fill(last, -1);
int start = 0, best = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (last[c] >= start) start = last[c] + 1; // jump past the repeat
last[c] = i;
best = Math.max(best, i - start + 1);
}
return best;
}int lengthOfLongestSubstring(string s) { // longest substring, no repeats
vector<int> last(128, -1);
int start = 0, best = 0;
for (int i = 0; i < (int)s.length(); i++) {
char c = s[i];
if (last[c] >= start) start = last[c] + 1; // jump past the repeat
last[c] = i;
best = max(best, i - start + 1);
}
return best;
}def length_of_longest_substring(s): # longest substring, no repeats
last = [-1] * 128
start = 0
best = 0
for i, c in enumerate(s):
if last[ord(c)] >= start:
start = last[ord(c)] + 1 # jump past the repeat
last[ord(c)] = i
best = max(best, i - start + 1)
return bestfunction lengthOfLongestSubstring(s: string): number { // longest substring, no repeats
const last: number[] = new Array(128).fill(-1);
let start = 0, best = 0;
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (last[c] >= start) start = last[c] + 1; // jump past the repeat
last[c] = i;
best = Math.max(best, i - start + 1);
}
return best;
}"abcabcbb". Window grows to "abc" (3); at second 'a' start jumps; max window length stays 3.🎯 LeetCode practice & worked example
3 · Sliding Window
Tests whether you can maintain a contiguous subarray/substring incrementally — expanding the right edge and shrinking the left edge in response to a constraint — instead of re-scanning from scratch; the tell is "longest/shortest/max substring or subarray satisfying X".
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Best Time to Buy and Sell Stock ★ | Easy | AmazonMetaGoogle | track running min price / max profit |
| 2 | Longest Substring Without Repeating Characters ★ | Medium | AmazonMetaBloomberg | expand/shrink window with a set |
| 3 | Longest Repeating Character Replacement | Medium | AmazonGoogleMicrosoft | window valid while size - maxFreq <= k |
| 4 | Permutation in String | Medium | AmazonMicrosoftUber | fixed-size window char-count match |
| 5 | Minimum Window Substring ★ | Hard | AmazonMetaGoogle | variable window with need/have counters |
| 6 | Sliding Window Maximum ★ | Hard | AmazonGoogleTikTok | monotonic deque of candidate maxima |
| 7 | Max Consecutive Ones III | Medium | GoogleAmazon | window with a flip/zero budget |
| 8 | Fruit Into Baskets | Medium | AmazonGoogle | window with at-most-2-distinct types |
Worked example — Longest Substring Without Repeating Characters (LC 3, Medium)
Frequently asked at: Amazon, Meta, Bloomberg, Adobe
Problem. Given a string s, find the length of the longest substring without repeating characters.
Idea. Keep a window [left, right] and a set of characters currently inside it. Advance right one step at a time; whenever the new character is already in the window, shrink from left until the duplicate is evicted. The window is always duplicate-free, so its length at every step is a candidate answer.
class Solution {
public int lengthOfLongestSubstring(String s) {
Set<Character> window = new HashSet<>();
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
while (window.contains(c)) {
window.remove(s.charAt(left));
left++;
}
window.add(c);
best = Math.max(best, right - left + 1);
}
return best;
}
}class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> window;
int left = 0, best = 0;
for (int right = 0; right < (int)s.length(); right++) {
char c = s[right];
while (window.count(c)) {
window.erase(s[left]);
left++;
}
window.insert(c);
best = max(best, right - left + 1);
}
return best;
}
};class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
window = set()
left = best = 0
for right in range(len(s)):
c = s[right]
while c in window:
window.remove(s[left])
left += 1
window.add(c)
best = max(best, right - left + 1)
return bestclass Solution {
lengthOfLongestSubstring(s: string): number {
const window = new Set<string>();
let left = 0, best = 0;
for (let right = 0; right < s.length; right++) {
const c = s[right];
while (window.has(c)) {
window.delete(s[left]);
left++;
}
window.add(c);
best = Math.max(best, right - left + 1);
}
return best;
}
}Complexity. Time O(n) Space O(min(n, alphabet)) — left and right each advance at most n times total; the set holds at most one entry per distinct character in the window.
Walkthrough. Input "abcabcbb":
| right | char | window before | action | best |
|---|---|---|---|---|
| 0 | a | {} | add a | 1 |
| 1 | b | {a} | add b | 2 |
| 2 | c | {a,b} | add c | 3 |
| 3 | a | {a,b,c} | duplicate: evict a (left=1), then add a | 3 |
| 4 | b | {b,c,a} | duplicate: evict b (left=2), then add b | 3 |
Answer: 3 (substring "abc").
🔬 Deeper dive & dry run
Why it works. The window [left, right] is maintained as an invariant: it never contains a repeated character. When character at right was last seen at an index >= left, that occurrence is inside the current window, so left jumps to one past it — a single O(1) jump rather than shrinking one step at a time. Because left only ever moves forward, the whole scan is O(n).
When to reach for it. Contiguous-subarray/substring problems where the validity of a window can be checked and updated incrementally as one pointer advances (no repeats, sum ≤ k, at most k distinct characters) and the answer is a min/max length or a count of valid windows.
Complexity. O(n) time — both pointers only move rightward over n characters combined; O(min(n, Σ)) space for the last-seen-index map, bounded by the alphabet size Σ.
Pitfalls & variations. Forgetting the lastSeen[c] >= left guard lets left jump backward on a stale index. Off-by-one when computing right - left + 1. The "at most k distinct characters" variant needs a frequency-count shrink loop instead of a single jump — a common source of confusion with this exact problem.
Trace input: s = "abcabcbb" — find the length of the longest substring with no repeated characters.
| Step | i | char | lastSeen[char] | left before | action | left after | window | maxLen |
|---|---|---|---|---|---|---|---|---|
| 0 | — | — | — | — | init | 0 | "" | 0 |
| 1 | 0 | a | none | 0 | record a@0 | 0 | "a" | 1 |
| 2 | 1 | b | none | 0 | record b@1 | 0 | "ab" | 2 |
| 3 | 2 | c | none | 0 | record c@2 | 0 | "abc" | 3 |
| 4 | 3 | a | 0 | 0 | 0 >= left(0) → left=1; record a@3 | 1 | "bca" | 3 |
| 5 | 4 | b | 1 | 1 | 1 >= left(1) → left=2; record b@4 | 2 | "cab" | 3 |
| 6 | 5 | c | 2 | 2 | 2 >= left(2) → left=3; record c@5 | 3 | "abc" | 3 |
| 7 | 6 | b | 4 | 3 | 4 >= left(3) → left=5; record b@6 | 5 | "cb" | 3 |
| 8 | 7 | b | 6 | 5 | 6 >= left(5) → left=7; record b@7 | 7 | "b" | 3 |
Result: maxLen = 3 (substring "abc"). The trace shows left only ever jumping forward past a stale duplicate, never re-scanning already-cleared ground.
12 · Monotonic Stack
Flowchart leaves: nearest greater/smaller (subarray) nearest bounds (max/min)
The idea
To find, for each element, its nearest greater (or smaller) neighbour, keep a stack whose values stay sorted. When a new element breaks the order, pop the violators — the new element is precisely their "next greater/smaller". Each index is pushed and popped once → O(n). Powers next-greater-element, daily temperatures, largest rectangle, trapping rain water, and stock spans.
🧰 API — Deque (Double-Ended Queue) methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
Deque<Integer> dq = new ArrayDeque<>();
dq.offerFirst(x); // add to front, O(1)
dq.offerLast(x); // add to back, O(1)
dq.addFirst(x); // add to front, throws on capacity failure, O(1)
dq.addLast(x); // add to back, throws on capacity failure, O(1)
dq.pollFirst(); // remove & return front, null if empty, O(1)
dq.pollLast(); // remove & return back, null if empty, O(1)
dq.peekFirst(); // look at front, null if empty, O(1)
dq.peekLast(); // look at back, null if empty, O(1)
dq.isEmpty(); // check empty, O(1)
dq.size(); // number of elements, O(1)
for (int v : dq) {} // iterate front-to-back, O(n)
#include <deque>
std::deque<int> dq;
dq.push_front(x); // add to front, O(1)
dq.push_back(x); // add to back, O(1)
dq.pop_front(); // remove front (no return value), O(1)
dq.pop_back(); // remove back (no return value), O(1)
dq.front(); // reference to front element, O(1)
dq.back(); // reference to back element, O(1)
dq.empty(); // check empty, O(1)
dq.size(); // number of elements, O(1)
dq[i]; // random access by index, O(1)
for (int v : dq) {} // iterate, O(n)
from collections import deque
dq = deque()
dq.appendleft(x) # add to front, O(1)
dq.append(x) # add to back, O(1)
dq.popleft() # remove & return front, O(1)
dq.pop() # remove & return back, O(1)
dq[0] # peek front, O(1)
dq[-1] # peek back, O(1)
len(dq) # size, O(1)
not dq # check empty (True if empty)
for v in dq: ... # iterate front-to-back, O(n)
const dq: number[] = [];
dq.unshift(x); // add to front — O(n)! NOT O(1)
dq.push(x); // add to back, O(1)
dq.shift(); // remove & return front — O(n)! NOT O(1)
dq.pop(); // remove & return back, O(1)
dq[0]; // peek front, O(1)
dq[dq.length - 1]; // peek back, O(1)
dq.length; // size, O(1)
for (const v of dq) {} // iterate, O(n)
// JS/TS arrays have no O(1) front operations. For a real O(1)-both-ends
// deque, use a doubly linked list, a circular buffer, or a library
// such as "denque" — plain arrays only give you O(1) at the back.
Brute force — scan forward for each element O(n²)
For every i, walk right until you find a larger value. The stack remembers unresolved candidates so each is settled in O(1) amortized instead of re-scanned.
Template A · next greater element (days until warmer) O(n)
t— array of temperatures, one per dayres— output array; res[i] becomes the number of days until a warmer temperaturest— stack of day indices still waiting for their next-warmer day, kept temperature-decreasing from bottom to topi— the day currently being processedj— an index popped off st once day i turns out to be warmer than it- Returns — for each day, the number of days until a strictly warmer temperature (0 if none exists)
int[] dailyTemperatures(int[] t){
int n = t.length; int[] res = new int[n];
Deque<Integer> st = new ArrayDeque<>(); // indices, temps decreasing down the stack
for (int i = 0; i < n; i++){
while (!st.isEmpty() && t[i] > t[st.peek()]){
int j = st.pop();
res[j] = i - j; // i is j's next-warmer day
}
st.push(i);
}
return res; // unresolved stay 0
}vector<int> dailyTemperatures(vector<int>& t){
int n = t.size(); vector<int> res(n, 0);
deque<int> st; // indices, temps decreasing down the stack
for (int i = 0; i < n; i++){
while (!st.empty() && t[i] > t[st.back()]){
int j = st.back(); st.pop_back();
res[j] = i - j; // i is j's next-warmer day
}
st.push_back(i);
}
return res; // unresolved stay 0
}def daily_temperatures(t):
n = len(t)
res = [0] * n
st = [] # indices, temps decreasing down the stack
for i in range(n):
while st and t[i] > t[st[-1]]:
j = st.pop()
res[j] = i - j # i is j's next-warmer day
st.append(i)
return res # unresolved stay 0function dailyTemperatures(t: number[]): number[] {
const n = t.length;
const res = new Array(n).fill(0);
const st: number[] = []; // indices, temps decreasing down the stack
for (let i = 0; i < n; i++) {
while (st.length && t[i] > t[st[st.length - 1]]) {
const j = st.pop()!;
res[j] = i - j; // i is j's next-warmer day
}
st.push(i);
}
return res; // unresolved stay 0
}Template B · largest rectangle in a histogram O(n)
h— array of bar heights making up the histogramst— stack of bar indices whose rectangle isn't closed yet, kept height-increasing from bottom to topi— the bar currently being processed; the loop runs one extra step past the end as a sentinelcur— height of the current bar, or 0 on the sentinel step so the stack fully emptiesheight— height of the bar popped off st — the shortest bar in the rectangle being closedwidth— how many bars wide that rectangle spansbest— largest rectangle area found so far- Returns — the area of the largest rectangle that fits under the histogram
int largestRectangleArea(int[] h){
int n = h.length, best = 0;
Deque<Integer> st = new ArrayDeque<>();
for (int i = 0; i <= n; i++){
int cur = (i == n) ? 0 : h[i]; // sentinel flushes the stack
while (!st.isEmpty() && h[st.peek()] >= cur){
int height = h[st.pop()];
int width = st.isEmpty() ? i : i - st.peek() - 1;
best = Math.max(best, height * width);
}
st.push(i);
}
return best;
}int largestRectangleArea(vector<int>& h){
int n = h.size(), best = 0;
deque<int> st;
for (int i = 0; i <= n; i++){
int cur = (i == n) ? 0 : h[i]; // sentinel flushes the stack
while (!st.empty() && h[st.back()] >= cur){
int height = h[st.back()]; st.pop_back();
int width = st.empty() ? i : i - st.back() - 1;
best = max(best, height * width);
}
st.push_back(i);
}
return best;
}def largest_rectangle_area(h):
n = len(h)
best = 0
st = []
for i in range(n + 1):
cur = 0 if i == n else h[i] # sentinel flushes the stack
while st and h[st[-1]] >= cur:
height = h[st.pop()]
width = i if not st else i - st[-1] - 1
best = max(best, height * width)
st.append(i)
return bestfunction largestRectangleArea(h: number[]): number {
const n = h.length;
let best = 0;
const st: number[] = [];
for (let i = 0; i <= n; i++) {
const cur = (i === n) ? 0 : h[i]; // sentinel flushes the stack
while (st.length && h[st[st.length - 1]] >= cur) {
const height = h[st.pop()!];
const width = st.length === 0 ? i : i - st[st.length - 1] - 1;
best = Math.max(best, height * width);
}
st.push(i);
}
return best;
}Worked example — next warmer day, [73,74,75,71,69,72,76]
| i (temp) | pops (resolve) | stack (indices) after |
|---|---|---|
| 0 (73) | — | [0] |
| 1 (74) | pop 0 → res[0]=1 | [1] |
| 2 (75) | pop 1 → res[1]=1 | [2] |
| 3 (71) | — | [2,3] |
| 4 (69) | — | [2,3,4] |
| 5 (72) | pop 4 → res[4]=1; pop 3 → res[3]=2 | [2,5] |
| 6 (76) | pop 5 → res[5]=1; pop 2 → res[2]=4 | [6] |
Result [1,1,4,2,1,1,0]. Index 2 (75°) waited 4 days for 76°; index 6 never warms so stays 0.
🔬 Deeper dive & dry run
Why it works. The stack holds indices whose "answer" is still unresolved, kept in strictly decreasing order of value. When a new element arrives that beats the top of the stack, it simultaneously resolves every stack entry it beats (because the stack is sorted), so each index is popped exactly once — that's what keeps the total work linear despite the nested-looking pop loop.
When to reach for it. Signals: "next greater/smaller element", "distance to the next warmer/taller/larger", or "largest rectangle/area under a skyline." Anything where the answer for position i depends on the nearest future (or past) element satisfying an order relation.
Complexity. O(n) time — amortized, since each index is pushed once and popped at most once; O(n) space for the stack.
Pitfalls & variations. Using >= vs > in the comparison changes whether equal values resolve each other — get this backwards and duplicates silently break. Forgetting leftover stack entries never get a "next" (they default to 0/-1) is a frequent off-by-one. Variants: next-smaller (flip comparison), circular arrays (traverse the array twice), and previous-greater (traverse right-to-left).
Trace input: temps = [73,74,75,71,69,72,76,73] — for each day, find how many days until a strictly warmer day.
| i | temp | stack before | pops (idx→ans) | stack after |
|---|---|---|---|---|
| 0 | 73 | [] | - | [0] |
| 1 | 74 | [0] | 0→1 | [1] |
| 2 | 75 | [1] | 1→1 | [2] |
| 3 | 71 | [2] | - | [2,3] |
| 4 | 69 | [2,3] | - | [2,3,4] |
| 5 | 72 | [2,3,4] | 4→1, 3→2 | [2,5] |
| 6 | 76 | [2,5] | 5→1, 2→4 | [6] |
| 7 | 73 | [6] | - | [6,7] |
Result: ans = [1,1,4,2,1,1,0,0]; indices 6 and 7 stay on the stack forever and default to 0. The trace shows day 6 (76°, the max) triggers a double pop, resolving both day 5 and day 2 in one step.
🧰 Quick reference templates
26. Monotonic Stack — Subarray
... Maintain a valid window NO ▸ Need nearest greater/smaller bounds YES
"Next/previous greater or smaller element" ⇒ keep a stack that stays monotonic. When the incoming value breaks the order, pop and resolve those elements. Each index pushed/popped once ⇒ O(n).
int[] dailyTemperatures(int[] t) { // days until a warmer temp
int n = t.length;
int[] res = new int[n];
Deque<Integer> stack = new ArrayDeque<>(); // indices, decreasing temps
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && t[i] > t[stack.peek()]) {
int j = stack.pop();
res[j] = i - j; // i is j's next-warmer day
}
stack.push(i);
}
return res;
}vector<int> dailyTemperatures(vector<int>& t) { // days until a warmer temp
int n = t.size();
vector<int> res(n, 0);
stack<int> st; // indices, decreasing temps
for (int i = 0; i < n; i++) {
while (!st.empty() && t[i] > t[st.top()]) {
int j = st.top(); st.pop();
res[j] = i - j; // i is j's next-warmer day
}
st.push(i);
}
return res;
}def daily_temperatures(t): # days until a warmer temp
n = len(t)
res = [0] * n
stack = [] # indices, decreasing temps
for i in range(n):
while stack and t[i] > t[stack[-1]]:
j = stack.pop()
res[j] = i - j # i is j's next-warmer day
stack.append(i)
return resfunction dailyTemperatures(t: number[]): number[] { // days until a warmer temp
const n = t.length;
const res: number[] = new Array(n).fill(0);
const stack: number[] = []; // indices, decreasing temps
for (let i = 0; i < n; i++) {
while (stack.length && t[i] > t[stack[stack.length - 1]]) {
const j = stack.pop()!;
res[j] = i - j; // i is j's next-warmer day
}
stack.push(i);
}
return res;
}[73,74,75,71,69,72,76,73] ⇒ [1,1,4,2,1,1,0,0]. Index 2 waits 4 days for 76.29. Monotonic Stack — Max/Min
... Sorted/monotonic answer NO ▸ Need nearest greater/smaller bounds YES
Optimizing an area/span bounded by nearest smaller elements (largest rectangle in a histogram, trapping rain water). The stack tracks candidate left boundaries; pop resolves a rectangle.
int largestRectangleArea(int[] heights) {
int n = heights.length, best = 0;
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i]; // sentinel flush at end
while (!stack.isEmpty() && heights[stack.peek()] >= h) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
best = Math.max(best, height * width);
}
stack.push(i);
}
return best;
}int largestRectangleArea(vector<int>& heights) {
int n = heights.size(), best = 0;
stack<int> st;
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i]; // sentinel flush at end
while (!st.empty() && heights[st.top()] >= h) {
int height = heights[st.top()]; st.pop();
int width = st.empty() ? i : i - st.top() - 1;
best = max(best, height * width);
}
st.push(i);
}
return best;
}def largest_rectangle_area(heights):
n = len(heights)
best = 0
stack = []
for i in range(n + 1):
h = 0 if i == n else heights[i] # sentinel flush at end
while stack and heights[stack[-1]] >= h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
best = max(best, height * width)
stack.append(i)
return bestfunction largestRectangleArea(heights: number[]): number {
const n = heights.length;
let best = 0;
const stack: number[] = [];
for (let i = 0; i <= n; i++) {
const h = (i === n) ? 0 : heights[i]; // sentinel flush at end
while (stack.length && heights[stack[stack.length - 1]] >= h) {
const height = heights[stack.pop()!];
const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
best = Math.max(best, height * width);
}
stack.push(i);
}
return best;
}[2,1,5,6,2,3]. Bars 5 & 6 form the best rectangle: height 5 × width 2 = 10.13 · Prefix Sums & Difference Arrays
Flowchart leaf: sum / additive problem
The idea
Precompute cumulative totals so any range sum is a single subtraction: sum(l..r) = P[r+1] - P[l]. Combine with a hash map of seen prefix sums to count subarrays with a target sum in one pass. The dual, a difference array, applies many range-updates in O(1) each and reconstructs the final array at the end.
Brute force — re-sum each range O(n) per query / O(n²) total
Adding up a[l..r] from scratch for every query. Prefix sums pay O(n) once, then answer each range in O(1).
Template A · count subarrays summing to k (prefix + hashmap) O(n)
nums— the input arrayk— the target sum a subarray must add up toseen— map from a prefix-sum value to how many times it has occurred, seeded with sum 0 occurring once for the empty prefixsum— running prefix sum through the current elementx— the element currently being folded into sumcount— running total of subarrays found so far that sum to k- Returns — the number of contiguous subarrays whose elements sum exactly to k
int subarraySum(int[] nums, int k){
Map<Integer,Integer> seen = new HashMap<>();
seen.put(0, 1); // empty prefix
int sum = 0, count = 0;
for (int x : nums){
sum += x;
count += seen.getOrDefault(sum - k, 0); // a prior prefix closes a k-sum here
seen.merge(sum, 1, Integer::sum);
}
return count;
}int subarraySum(vector<int>& nums, int k){
unordered_map<int,int> seen;
seen[0] = 1; // empty prefix
int sum = 0, count = 0;
for (int x : nums){
sum += x;
count += seen.count(sum - k) ? seen[sum - k] : 0; // a prior prefix closes a k-sum here
seen[sum]++;
}
return count;
}def subarray_sum(nums, k):
seen = {0: 1} # empty prefix
total = count = 0
for x in nums:
total += x
count += seen.get(total - k, 0) # a prior prefix closes a k-sum here
seen[total] = seen.get(total, 0) + 1
return countfunction subarraySum(nums: number[], k: number): number {
const seen = new Map<number, number>();
seen.set(0, 1); // empty prefix
let sum = 0, count = 0;
for (const x of nums) {
sum += x;
count += seen.get(sum - k) ?? 0; // a prior prefix closes a k-sum here
seen.set(sum, (seen.get(sum) ?? 0) + 1);
}
return count;
}Template B · 2D prefix sum (sub-matrix sum in O(1))
a— the input 2D matrixm, n— number of rows and columns in aP— prefix-sum grid, one row/column larger than a, where P[i+1][j+1] is the sum of every cell from (0,0) to (i,j)r1, c1, r2, c2— the top-left and bottom-right corners of the rectangle being queriedsum— the total of every cell inside that rectangle, computed from P by inclusion-exclusion- Returns — the sum of any rectangular sub-matrix in O(1) time after the one-time O(m·n) build
// P[i+1][j+1] = sum of the rectangle from (0,0) to (i,j)
int[][] P = new int[m+1][n+1];
for (int i=0;i<m;i++)
for (int j=0;j<n;j++)
P[i+1][j+1] = a[i][j] + P[i][j+1] + P[i+1][j] - P[i][j];
// sum of rows [r1,r2], cols [c1,c2]:
int sum = P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1];// P[i+1][j+1] = sum of the rectangle from (0,0) to (i,j)
vector<vector<int>> P(m+1, vector<int>(n+1, 0));
for (int i=0;i<m;i++)
for (int j=0;j<n;j++)
P[i+1][j+1] = a[i][j] + P[i][j+1] + P[i+1][j] - P[i][j];
// sum of rows [r1,r2], cols [c1,c2]:
int sum = P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1];# P[i+1][j+1] = sum of the rectangle from (0,0) to (i,j)
P = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m):
for j in range(n):
P[i+1][j+1] = a[i][j] + P[i][j+1] + P[i+1][j] - P[i][j]
# sum of rows [r1,r2], cols [c1,c2]:
total = P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1]// P[i+1][j+1] = sum of the rectangle from (0,0) to (i,j)
const P: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 0; i < m; i++)
for (let j = 0; j < n; j++)
P[i+1][j+1] = a[i][j] + P[i][j+1] + P[i+1][j] - P[i][j];
// sum of rows [r1,r2], cols [c1,c2]:
const sum = P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1];Template C · difference array (range add in O(1))
diff— helper array where a bump at diff[l] and an opposite bump at diff[r+1] represent adding val across the whole range [l, r]l, r, val— the inclusive range and the amount to add to it in one O(1) updaterun— running total used while replaying diff back into the final array- Returns — build() reconstructs the fully-updated array by taking a running sum over diff; addRange() itself changes no return value
int[] diff = new int[n+1];
void addRange(int l, int r, int val){ diff[l] += val; diff[r+1] -= val; }
int[] build(){ int[] a=new int[n]; int run=0;
for (int i=0;i<n;i++){ run += diff[i]; a[i]=run; } return a; }std::vector<int> diff(n+1, 0);
void addRange(int l, int r, int val){ diff[l] += val; diff[r+1] -= val; }
std::vector<int> build(){ std::vector<int> a(n); int run=0;
for (int i=0;i<n;i++){ run += diff[i]; a[i]=run; } return a; }diff = [0] * (n + 1)
def add_range(l, r, val):
diff[l] += val
diff[r + 1] -= val
def build():
a = [0] * n
run = 0
for i in range(n):
run += diff[i]
a[i] = run
return aconst diff: number[] = new Array(n + 1).fill(0);
function addRange(l: number, r: number, val: number): void { diff[l] += val; diff[r + 1] -= val; }
function build(): number[] {
const a: number[] = new Array(n).fill(0);
let run = 0;
for (let i = 0; i < n; i++) { run += diff[i]; a[i] = run; }
return a;
}Worked example — subarrays summing to 2 in [1,1,1]
| x | running sum | need sum−k = sum−2 | seen count of it | count | map after |
|---|---|---|---|---|---|
| — | 0 | — | — | 0 | {0:1} |
| 1 | 1 | -1 | 0 | 0 | {0:1,1:1} |
| 1 | 2 | 0 | 1 | 1 | {0:1,1:1,2:1} |
| 1 | 3 | 1 | 1 | 2 | {…,3:1} |
Answer 2 — the subarrays [1,1] at indices (0,1) and (1,2). The map turns "find an earlier prefix" into an O(1) lookup.
🔬 Deeper dive & dry run
Why it works. Any subarray sum nums[i..j] equals prefix[j] - prefix[i-1]. Fixing j, the count of valid starting points is exactly the number of earlier prefix sums equal to prefix[j] - k. Storing prefix-sum frequencies in a hash map turns "count subarrays ending here with sum k" into an O(1) lookup instead of rescanning from every start.
When to reach for it. "Number of subarrays with sum/XOR equal to k", "subarray sum divisible by k", or anything phrased as a range-sum query repeated over many ranges — especially when negatives are allowed, ruling out sliding window.
Complexity. O(n) time — one pass accumulating the running sum and one map lookup/update per index; O(n) space for the frequency map.
Pitfalls & variations. Must seed the map with {0: 1} to count subarrays starting at index 0. Look up prefix - k before inserting the current prefix, or a subarray can incorrectly "match itself" as a zero-length span. Variants: difference arrays for O(1) range-update/O(n) rebuild, and 2D prefix sums for submatrix queries.
Trace input: nums = [1,1,1,0,1,1], k = 2 — count subarrays summing to exactly 2.
| j | num | prefix | lookup key (prefix-k) | count found | ans | freq map after |
|---|---|---|---|---|---|---|
| - | - | 0 | - | - | 0 | {0:1} |
| 0 | 1 | 1 | -1 | 0 | 0 | {0:1,1:1} |
| 1 | 1 | 2 | 0 | 1 | 1 | {0:1,1:1,2:1} |
| 2 | 1 | 3 | 1 | 1 | 2 | {0:1,1:1,2:1,3:1} |
| 3 | 0 | 3 | 1 | 1 | 3 | {0:1,1:1,2:1,3:2} |
| 4 | 1 | 4 | 2 | 1 | 4 | {0:1,1:1,2:1,3:2,4:1} |
| 5 | 1 | 5 | 3 | 2 | 6 | {0:1,1:1,2:1,3:2,4:1,5:1} |
Result: 6 subarrays sum to 2: (0,1), (1,2), (1,3), (2,4), (3,5), (4,5). The double count at j=5 (prefix 3 seen twice) shows how one lookup can resolve multiple matches at once.
🧰 Quick reference templates
24. Prefix Sums
... Small constraints NO ▸ Sum/additive problem YES
Precompute running totals so any range sum is O(1). Combined with a hash map of seen prefix sums, you can count subarrays with a given sum in one pass.
int subarraySum(int[] nums, int k) { // # subarrays summing to k
Map<Integer, Integer> prefixCount = new HashMap<>();
prefixCount.put(0, 1); // empty prefix
int sum = 0, count = 0;
for (int x : nums) {
sum += x;
count += prefixCount.getOrDefault(sum - k, 0); // seen a prefix that closes a k-sum
prefixCount.merge(sum, 1, Integer::sum);
}
return count;
}int subarraySum(vector<int>& nums, int k) { // # subarrays summing to k
unordered_map<int, int> prefixCount;
prefixCount[0] = 1; // empty prefix
int sum = 0, count = 0;
for (int x : nums) {
sum += x;
auto it = prefixCount.find(sum - k);
if (it != prefixCount.end()) count += it->second; // seen a prefix that closes a k-sum
prefixCount[sum]++;
}
return count;
}def subarray_sum(nums, k): # # subarrays summing to k
prefix_count = {0: 1} # empty prefix
total = 0
count = 0
for x in nums:
total += x
count += prefix_count.get(total - k, 0) # seen a prefix that closes a k-sum
prefix_count[total] = prefix_count.get(total, 0) + 1
return countfunction subarraySum(nums: number[], k: number): number { // # subarrays summing to k
const prefixCount = new Map<number, number>();
prefixCount.set(0, 1); // empty prefix
let sum = 0, count = 0;
for (const x of nums) {
sum += x;
count += prefixCount.get(sum - k) ?? 0; // seen a prefix that closes a k-sum
prefixCount.set(sum, (prefixCount.get(sum) ?? 0) + 1);
}
return count;
}[1,1,1], k=2. Prefixes 0,1,2,3. At sum=2 we've seen sum-k=0 once; at sum=3 we've seen 1 once ⇒ 2 subarrays.14 · Dynamic Programming I — 1D, Knapsack, Counting
Flowchart leaves: DP (small constraints) DP (subarray) DP (max/min) DP (counting) DP (parsing)
The idea
DP applies when a problem has overlapping subproblems (the same sub-question recurs) and optimal substructure (the best whole is built from best parts). You define a state, a transition (recurrence), and base cases, then either memoize top-down or fill a table bottom-up. The recipe that never fails:
- State: what does
dp[i]mean, in one precise sentence? - Transition: how is
dp[i]built from smaller states? - Base case: the smallest states you can answer directly.
- Order: fill so every state's dependencies are ready first.
- Answer: which state (or aggregate) is the result?
Brute force — naive recursion exponential
c— array of available coin denominationsamt— amount still needed to reach exactly 0coin— one denomination being tried as the next coin to usesub— result of recursing on the smaller remaining amount (amt - coin), or -1 if that's impossiblebest— fewest coins found so far among all branches tried at this call- Returns — the minimum number of coins needed to make amt, or -1 if it can't be made
int coins(int[] c, int amt){ // recompute same amounts repeatedly
if (amt == 0) return 0;
if (amt < 0) return -1;
int best = Integer.MAX_VALUE;
for (int coin : c){
int sub = coins(c, amt - coin);
if (sub >= 0) best = Math.min(best, sub + 1);
}
return best == Integer.MAX_VALUE ? -1 : best;
}int coins(std::vector<int>& c, int amt){ // recompute same amounts repeatedly
if (amt == 0) return 0;
if (amt < 0) return -1;
int best = INT_MAX;
for (int coin : c){
int sub = coins(c, amt - coin);
if (sub >= 0) best = std::min(best, sub + 1);
}
return best == INT_MAX ? -1 : best;
}def coins(c, amt): # recompute same amounts repeatedly
if amt == 0:
return 0
if amt < 0:
return -1
best = float('inf')
for coin in c:
sub = coins(c, amt - coin)
if sub >= 0:
best = min(best, sub + 1)
return -1 if best == float('inf') else bestfunction coins(c: number[], amt: number): number { // recompute same amounts repeatedly
if (amt === 0) return 0;
if (amt < 0) return -1;
let best = Number.MAX_SAFE_INTEGER;
for (const coin of c) {
const sub = coins(c, amt - coin);
if (sub >= 0) best = Math.min(best, sub + 1);
}
return best === Number.MAX_SAFE_INTEGER ? -1 : best;
}This recomputes coins(amt) for the same amt down countless branches — the overlap DP eliminates.
Template A · 1D min — coin change (fewest coins) O(amount·coins)
coins— array of available coin denominationsamount— the target amount to makedp— dp[a] holds the fewest coins needed to make amount a, started at a large "infinity" sentinela— the amount currently being solved, built up from 1 to amountc— one coin denomination tried as the last coin used to reach a- Returns — the minimum number of coins needed to make amount, or -1 if impossible
int coinChange(int[] coins, int amount){
int[] dp = new int[amount+1];
Arrays.fill(dp, amount+1); // "infinity"
dp[0] = 0; // base: 0 coins make 0
for (int a = 1; a <= amount; a++)
for (int c : coins)
if (c <= a) dp[a] = Math.min(dp[a], dp[a-c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}int coinChange(std::vector<int>& coins, int amount){
std::vector<int> dp(amount+1, amount+1); // "infinity"
dp[0] = 0; // base: 0 coins make 0
for (int a = 1; a <= amount; a++)
for (int c : coins)
if (c <= a) dp[a] = std::min(dp[a], dp[a-c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}def coin_change(coins, amount):
dp = [amount + 1] * (amount + 1) # "infinity"
dp[0] = 0 # base: 0 coins make 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return -1 if dp[amount] > amount else dp[amount]function coinChange(coins: number[], amount: number): number {
const dp: number[] = new Array(amount + 1).fill(amount + 1); // "infinity"
dp[0] = 0; // base: 0 coins make 0
for (let a = 1; a <= amount; a++)
for (const c of coins)
if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}Template B · 1D counting — number of ways (combinations)
amount— the target amount to makecoins— array of available coin denominationsdp— dp[a] holds the number of distinct combinations of coins that sum to ac— the coin denomination in the outer loop — iterating coins first keeps combinations order-independenta— the amount being updated in the inner loop- Returns — the total number of distinct ways to make amount using any number of each coin
int change(int amount, int[] coins){
int[] dp = new int[amount+1]; dp[0] = 1;
for (int c : coins) // coin outer ⇒ order-independent combos
for (int a = c; a <= amount; a++)
dp[a] += dp[a-c];
return dp[amount];
}int change(int amount, std::vector<int>& coins){
std::vector<int> dp(amount+1, 0); dp[0] = 1;
for (int c : coins) // coin outer ⇒ order-independent combos
for (int a = c; a <= amount; a++)
dp[a] += dp[a-c];
return dp[amount];
}def change(amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1
for c in coins: # coin outer ⇒ order-independent combos
for a in range(c, amount + 1):
dp[a] += dp[a - c]
return dp[amount]function change(amount: number, coins: number[]): number {
const dp: number[] = new Array(amount + 1).fill(0);
dp[0] = 1;
for (const c of coins) // coin outer ⇒ order-independent combos
for (let a = c; a <= amount; a++)
dp[a] += dp[a - c];
return dp[amount];
}Template C · 0/1 knapsack (each item once — iterate capacity downward)
w— array of item weightsv— array of item values, parallel to wW— total knapsack capacitydp— dp[c] holds the best total value achievable using capacity c so fari— index of the item currently being consideredc— capacity being updated; iterating it downward ensures each item is only used once- Returns — the maximum total value obtainable without exceeding capacity W
int knapsack(int[] w, int[] v, int W){
int[] dp = new int[W+1];
for (int i = 0; i < w.length; i++)
for (int c = W; c >= w[i]; c--) // DOWNWARD ⇒ item used at most once
dp[c] = Math.max(dp[c], dp[c-w[i]] + v[i]);
return dp[W];
}int knapsack(std::vector<int>& w, std::vector<int>& v, int W){
std::vector<int> dp(W+1, 0);
for (size_t i = 0; i < w.size(); i++)
for (int c = W; c >= w[i]; c--) // DOWNWARD ⇒ item used at most once
dp[c] = std::max(dp[c], dp[c-w[i]] + v[i]);
return dp[W];
}def knapsack(w, v, W):
dp = [0] * (W + 1)
for i in range(len(w)):
for c in range(W, w[i] - 1, -1): # DOWNWARD ⇒ item used at most once
dp[c] = max(dp[c], dp[c - w[i]] + v[i])
return dp[W]function knapsack(w: number[], v: number[], W: number): number {
const dp: number[] = new Array(W + 1).fill(0);
for (let i = 0; i < w.length; i++)
for (let c = W; c >= w[i]; c--) // DOWNWARD ⇒ item used at most once
dp[c] = Math.max(dp[c], dp[c - w[i]] + v[i]);
return dp[W];
}Template D · Kadane — max subarray (running-best DP)
a— the input array of numbers, which may include negativescur— the best sum of a subarray that must end at the current indexbest— the best subarray sum found anywhere in the scan so far- Returns — the maximum sum achievable by any contiguous subarray
int maxSubArray(int[] a){
int best = a[0], cur = a[0];
for (int i = 1; i < a.length; i++){
cur = Math.max(a[i], cur + a[i]); // extend previous or restart at a[i]
best = Math.max(best, cur);
}
return best;
}int maxSubArray(std::vector<int>& a){
int best = a[0], cur = a[0];
for (size_t i = 1; i < a.size(); i++){
cur = std::max(a[i], cur + a[i]); // extend previous or restart at a[i]
best = std::max(best, cur);
}
return best;
}def max_sub_array(a):
best = cur = a[0]
for i in range(1, len(a)):
cur = max(a[i], cur + a[i]) # extend previous or restart at a[i]
best = max(best, cur)
return bestfunction maxSubArray(a: number[]): number {
let best = a[0], cur = a[0];
for (let i = 1; i < a.length; i++) {
cur = Math.max(a[i], cur + a[i]); // extend previous or restart at a[i]
best = Math.max(best, cur);
}
return best;
}Template E · parsing DP — decode ways (rolling O(1) memory)
s— the string of digits to decode, e.g. "12" could be "AB" or "L"prev2— number of ways to decode the string up to two characters backprev1— number of ways to decode the string up to one character backcur— number of ways to decode up through the current character, combining a valid 1-digit take from prev1 and a valid 2-digit take (10-26) from prev2two— the two-digit number formed by the previous and current character, checked for validity (10-26)- Returns — the total number of ways to decode the whole string
int numDecodings(String s){
if (s.charAt(0)=='0') return 0;
int prev2 = 1, prev1 = 1;
for (int i = 1; i < s.length(); i++){
int cur = 0;
if (s.charAt(i) != '0') cur += prev1; // take 1 digit
int two = (s.charAt(i-1)-'0')*10 + (s.charAt(i)-'0');
if (two >= 10 && two <= 26) cur += prev2; // take 2 digits
prev2 = prev1; prev1 = cur;
}
return prev1;
}int numDecodings(std::string s){
if (s[0]=='0') return 0;
int prev2 = 1, prev1 = 1;
for (size_t i = 1; i < s.size(); i++){
int cur = 0;
if (s[i] != '0') cur += prev1; // take 1 digit
int two = (s[i-1]-'0')*10 + (s[i]-'0');
if (two >= 10 && two <= 26) cur += prev2; // take 2 digits
prev2 = prev1; prev1 = cur;
}
return prev1;
}def num_decodings(s):
if s[0] == '0':
return 0
prev2, prev1 = 1, 1
for i in range(1, len(s)):
cur = 0
if s[i] != '0':
cur += prev1 # take 1 digit
two = int(s[i-1]) * 10 + int(s[i])
if 10 <= two <= 26:
cur += prev2 # take 2 digits
prev2, prev1 = prev1, cur
return prev1function numDecodings(s: string): number {
if (s[0] === '0') return 0;
let prev2 = 1, prev1 = 1;
for (let i = 1; i < s.length; i++) {
let cur = 0;
if (s[i] !== '0') cur += prev1; // take 1 digit
const two = (s.charCodeAt(i - 1) - 48) * 10 + (s.charCodeAt(i) - 48);
if (two >= 10 && two <= 26) cur += prev2; // take 2 digits
prev2 = prev1; prev1 = cur;
}
return prev1;
}Worked example — coin change min, coins {1,2,5}, amount 11
| a | candidates dp[a-c]+1 | dp[a] |
|---|---|---|
| 0 | base | 0 |
| 1 | dp[0]+1 | 1 |
| 2 | dp[1]+1=2, dp[0]+1=1 | 1 |
| 3 | dp[2]+1=2, dp[1]+1=2 | 2 |
| 5 | dp[4]+1=3, dp[3]+1=3, dp[0]+1=1 | 1 |
| 6 | dp[5]+1=2, dp[4]+1=3, dp[1]+1=2 | 2 |
| 10 | dp[5]+1=2 … | 2 |
| 11 | dp[10]+1=3, dp[9]+1=4, dp[6]+1=3 | 3 |
Answer 3 coins (5+5+1). Each dp[a] is computed once and reused — that reuse is the entire speedup over the recursion above.
🔬 Deeper dive & dry run
Why it works. dp[i] = fewest coins to make amount i, and it obeys the recurrence dp[i] = 1 + min(dp[i-c]) over every coin c ≤ i. This is valid because an optimal solution for i must end in some last coin c, and removing that coin leaves an optimal solution for the smaller amount i-c — the classic optimal-substructure argument that lets us build answers for larger amounts strictly from already-finalized smaller ones.
When to reach for it. "Fewest/most ways to reach a target using unlimited/limited copies of given items" — coin change, combination/permutation counting, unbounded knapsack. The target decomposes into a smaller instance of the same problem via one item choice.
Complexity. O(amount × coins) time — one pass over coins for each amount from 1 to target; O(amount) space for the 1D dp array.
Pitfalls & variations. Initializing dp with 0 instead of infinity/sentinel for unreachable amounts silently produces wrong minimums. Iterating amount in the outer loop vs. coin in the outer loop changes counting problems from combinations to permutations (order matters) — mixing this up is the most common bug. Variants: 0/1 knapsack (loop items outer, amount inner, descending) vs. unbounded knapsack (amount inner, ascending, as here).
Trace input: coins = [1,3,4], amount = 6 — fewest coins that sum exactly to 6.
| i | candidates (dp[i-c]+1) | dp[i] |
|---|---|---|
| 0 | base case | 0 |
| 1 | dp[0]+1=1 | 1 |
| 2 | dp[1]+1=2 | 2 |
| 3 | dp[2]+1=3, dp[0]+1=1 | 1 |
| 4 | dp[3]+1=2, dp[1]+1=2, dp[0]+1=1 | 1 |
| 5 | dp[4]+1=2, dp[2]+1=3, dp[1]+1=2 | 2 |
| 6 | dp[5]+1=3, dp[3]+1=2, dp[2]+1=3 | 2 |
Result: dp[6] = 2 (e.g. 3+3). The table shows dp[3] and dp[4] hitting 1 coin directly, which is what lets dp[6] and dp[5] beat the naive all-ones count.
🧰 Quick reference templates
27. Dynamic Programming — Subarray
... Need bounds NO
Best subarray with no window/stack structure ⇒ DP where each element either extends the running best or starts fresh (Kadane), or fills a 2-D table (palindromic substrings).
int maxSubArray(int[] nums) { // max contiguous sum (Kadane)
int best = nums[0], cur = nums[0];
for (int i = 1; i < nums.length; i++) {
cur = Math.max(nums[i], cur + nums[i]); // extend or restart
best = Math.max(best, cur);
}
return best;
}int maxSubArray(vector<int>& nums) { // max contiguous sum (Kadane)
int best = nums[0], cur = nums[0];
for (int i = 1; i < (int)nums.size(); i++) {
cur = max(nums[i], cur + nums[i]); // extend or restart
best = max(best, cur);
}
return best;
}def max_sub_array(nums): # max contiguous sum (Kadane)
best = cur = nums[0]
for i in range(1, len(nums)):
cur = max(nums[i], cur + nums[i]) # extend or restart
best = max(best, cur)
return bestfunction maxSubArray(nums: number[]): number { // max contiguous sum (Kadane)
let best = nums[0], cur = nums[0];
for (let i = 1; i < nums.length; i++) {
cur = Math.max(nums[i], cur + nums[i]); // extend or restart
best = Math.max(best, cur);
}
return best;
}[-2,1,-3,4,-1,2,1,-5,4]. Best window [4,-1,2,1] ⇒ sum 6.Max / Min Optimization
30. Dynamic Programming — Max/Min
... Need bounds NO ▸ Split into subproblems YES
Optimal value built from optimal sub-answers (LIS, min path sum, house robber, edit distance). Below is longest increasing subsequence in O(n log n) via patience sorting.
int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length]; // tails[k] = smallest tail of an LIS of length k+1
int size = 0;
for (int x : nums) {
int lo = 0, hi = size;
while (lo < hi) { // lower-bound of x
int mid = (lo + hi) / 2;
if (tails[mid] < x) lo = mid + 1; else hi = mid;
}
tails[lo] = x;
if (lo == size) size++;
}
return size;
}
// O(n^2) variant: dp[i]=1+max(dp[j]) for j<i with nums[j]<nums[i].int lengthOfLIS(vector<int>& nums) {
vector<int> tails(nums.size()); // tails[k] = smallest tail of an LIS of length k+1
int size = 0;
for (int x : nums) {
int lo = 0, hi = size;
while (lo < hi) { // lower-bound of x
int mid = (lo + hi) / 2;
if (tails[mid] < x) lo = mid + 1; else hi = mid;
}
tails[lo] = x;
if (lo == size) size++;
}
return size;
}
// O(n^2) variant: dp[i]=1+max(dp[j]) for j<i with nums[j]<nums[i].def length_of_lis(nums):
tails = [0] * len(nums) # tails[k] = smallest tail of an LIS of length k+1
size = 0
for x in nums:
lo, hi = 0, size
while lo < hi: # lower-bound of x
mid = (lo + hi) // 2
if tails[mid] < x:
lo = mid + 1
else:
hi = mid
tails[lo] = x
if lo == size:
size += 1
return size
# O(n^2) variant: dp[i]=1+max(dp[j]) for j<i with nums[j]<nums[i].function lengthOfLIS(nums: number[]): number {
const tails: number[] = new Array(nums.length).fill(0); // tails[k] = smallest tail of an LIS of length k+1
let size = 0;
for (const x of nums) {
let lo = 0, hi = size;
while (lo < hi) { // lower-bound of x
const mid = Math.floor((lo + hi) / 2);
if (tails[mid] < x) lo = mid + 1; else hi = mid;
}
tails[lo] = x;
if (lo === size) size++;
}
return size;
}
// O(n^2) variant: dp[i]=1+max(dp[j]) for j<i with nums[j]<nums[i].[10,9,2,5,3,7,101,18]. tails evolves to length 4 (e.g., 2,3,7,18).33. Dynamic Programming — Counting
... Count number of ways YES ▸ Brute force enough NO
Count ways by summing sub-counts (climbing stairs, unique paths, coin combinations). Order of loops decides combinations vs. permutations.
int change(int amount, int[] coins) { // # combinations that make amount
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int c : coins) // coin-outer ⇒ combinations (order-free)
for (int a = c; a <= amount; a++)
dp[a] += dp[a - c];
return dp[amount];
}int change(int amount, vector<int>& coins) { // # combinations that make amount
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for (int c : coins) // coin-outer ⇒ combinations (order-free)
for (int a = c; a <= amount; a++)
dp[a] += dp[a - c];
return dp[amount];
}def change(amount, coins): # # combinations that make amount
dp = [0] * (amount + 1)
dp[0] = 1
for c in coins: # coin-outer ⇒ combinations (order-free)
for a in range(c, amount + 1):
dp[a] += dp[a - c]
return dp[amount]function change(amount: number, coins: number[]): number { // # combinations that make amount
const dp: number[] = new Array(amount + 1).fill(0);
dp[0] = 1;
for (const c of coins) // coin-outer ⇒ combinations (order-free)
for (let a = c; a <= amount; a++)
dp[a] += dp[a - c];
return dp[amount];
}amount=5, coins {1,2,5} ⇒ 4 ways: 5, 2+2+1, 2+1+1+1, 1×5.Multiple Sequences
38. Dynamic Programming — Parsing
... Need to parse symbols YES ▸ Optimize/count valid spans YES
Counting valid interpretations / longest valid span of a symbol string (decode ways, longest valid parentheses) is DP over positions, where each char extends one or two prior states.
int numDecodings(String s) { // "12" → "AB" or "L" ⇒ 2 ways
int n = s.length();
if (s.charAt(0) == '0') return 0;
int prev2 = 1, prev1 = 1; // dp[i-2], dp[i-1]
for (int i = 1; i < n; i++) {
int cur = 0;
if (s.charAt(i) != '0') cur += prev1; // single digit
int two = (s.charAt(i-1) - '0') * 10 + (s.charAt(i) - '0');
if (two >= 10 && two <= 26) cur += prev2; // valid pair
prev2 = prev1; prev1 = cur;
}
return prev1;
}int numDecodings(string s) { // "12" → "AB" or "L" ⇒ 2 ways
int n = s.length();
if (s[0] == '0') return 0;
int prev2 = 1, prev1 = 1; // dp[i-2], dp[i-1]
for (int i = 1; i < n; i++) {
int cur = 0;
if (s[i] != '0') cur += prev1; // single digit
int two = (s[i-1] - '0') * 10 + (s[i] - '0');
if (two >= 10 && two <= 26) cur += prev2; // valid pair
prev2 = prev1; prev1 = cur;
}
return prev1;
}def num_decodings(s): # "12" → "AB" or "L" ⇒ 2 ways
n = len(s)
if s[0] == '0':
return 0
prev2 = prev1 = 1 # dp[i-2], dp[i-1]
for i in range(1, n):
cur = 0
if s[i] != '0':
cur += prev1 # single digit
two = int(s[i-1]) * 10 + int(s[i])
if 10 <= two <= 26:
cur += prev2 # valid pair
prev2, prev1 = prev1, cur
return prev1function numDecodings(s: string): number { // "12" → "AB" or "L" ⇒ 2 ways
const n = s.length;
if (s[0] === '0') return 0;
let prev2 = 1, prev1 = 1; // dp[i-2], dp[i-1]
for (let i = 1; i < n; i++) {
let cur = 0;
if (s[i] !== '0') cur += prev1; // single digit
const two = (s.charCodeAt(i - 1) - 48) * 10 + (s.charCodeAt(i) - 48);
if (two >= 10 && two <= 26) cur += prev2; // valid pair
prev2 = prev1; prev1 = cur;
}
return prev1;
}"226" ⇒ "BZ", "VF", "BBF" = 3 decodings. O(1) memory rolling DP.🎯 LeetCode practice & worked example
13 · 1-D Dynamic Programming
Tests whether you can define a state that depends on a fixed number of previous states along a single index — the tell is "count the ways" or "min/max cost" over a sequence where brute force is exponential.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Climbing Stairs ★ | Easy | AmazonAdobeApple | 1-D dp / fib |
| 2 | Min Cost Climbing Stairs | Easy | AmazonGoogle | 1-D dp / min path cost |
| 3 | House Robber ★ | Medium | AmazonGoogleMicrosoft | include/exclude dp |
| 4 | House Robber II | Medium | AmazonUber | circular array, two linear passes |
| 5 | Longest Palindromic Substring ★ | Medium | AmazonMicrosoftMeta | expand-around-center / interval dp |
| 6 | Palindromic Substrings | Medium | AmazonGoogle | expand around center, count |
| 7 | Decode Ways | Medium | FacebookAmazon | 1-D dp over string parse |
| 8 | Coin Change ★ | Medium | AmazonGoogleUber | unbounded knapsack |
| 9 | Maximum Product Subarray | Medium | LinkedInAmazon | running max/min product |
| 10 | Word Break ★ | Medium | AmazonGoogleMeta | dp over valid cut points |
| 11 | Longest Increasing Subsequence ★ | Medium | AmazonMicrosoftGoogle | dp O(n²) or patience/binary search O(n log n) |
| 12 | Partition Equal Subset Sum | Medium | AmazonMeta | subset-sum dp / bitset |
Worked example — Coin Change (LC 322, Medium)
Frequently asked at: Amazon, Google, Uber, Bloomberg.
Problem. Given an array coins of distinct denominations and an integer amount, return the fewest number of coins needed to make up amount. If it cannot be made, return -1. Each coin can be used unlimited times.
Idea. Let dp[a] = minimum coins to form amount a, with dp[0] = 0. For every amount a from 1 up to the target, try every coin c ≤ a and take dp[a] = min(dp[a], dp[a - c] + 1). This is unbounded knapsack: each coin denomination can be reused, so the inner loop simply scans all coins for every amount rather than tracking how many times a coin was used.
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // sentinel > any real answer
dp[0] = 0;
for (int a = 1; a <= amount; a++) {
for (int c : coins) {
if (c <= a) {
dp[a] = Math.min(dp[a], dp[a - c] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1); // sentinel > any real answer
dp[0] = 0;
for (int a = 1; a <= amount; a++) {
for (int c : coins) {
if (c <= a) {
dp[a] = min(dp[a], dp[a - c] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [amount + 1] * (amount + 1) # sentinel > any real answer
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return -1 if dp[amount] > amount else dp[amount]class Solution {
coinChange(coins: number[], amount: number): number {
const dp = new Array(amount + 1).fill(amount + 1); // sentinel > any real answer
dp[0] = 0;
for (let a = 1; a <= amount; a++) {
for (const c of coins) {
if (c <= a) {
dp[a] = Math.min(dp[a], dp[a - c] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}Complexity. Time O(amount × coins.length) Space O(amount) — one dp array, nested loop over every amount and every coin.
Walkthrough. coins = [1, 2, 5], amount = 11. Filling dp[0..11] left to right:
| a | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| dp[a] | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |
Each cell picks the best of "add a 1", "add a 2", or "add a 5" to a smaller already-solved amount — e.g. dp[10] = dp[5] + 1 = 2 (two 5s), and dp[11] = dp[10] + 1 = 3 (5 + 5 + 1). Final answer dp[11] = 3.
🔬 Deeper dive & dry run
Why it works. dp[i] = fewest coins to make amount i. The recurrence dp[i] = 1 + min(dp[i-c]) over every coin c ≤ i works because an optimal solution for i must end by placing some last coin c, and whatever remains (i-c) must itself be solved optimally — this is the DP optimal-substructure argument. Building bottom-up from dp[0]=0 guarantees every sub-answer is final before it's reused.
When to reach for it. Unbounded/unlimited supply of items, you need a min/max count or count-of-ways over a single integer axis (amount, length, steps), and brute force recursion on that axis has overlapping subproblems (visible as an exponential recursion tree with repeated arguments).
Complexity. Time O(amount × coins) — one inner loop over coins for each of the amount outer states. Space O(amount) for the 1-D dp array (coins array is O(1) extra).
Pitfalls & variations. Forgetting to initialize unreachable states to infinity (not 0) causes silently wrong mins; iterating amount in the outer loop and coins in the inner loop counts combinations correctly for min-coins, but if you instead want number of ways (LC 518) swapping loop order changes permutations vs combinations; off-by-one on whether dp[amount] stays infinite (no solution) must be checked and mapped to -1.
Trace input: coins = [1,3,4], amount = 6 — find the minimum number of coins that sum to 6.
| i | via coin 1 → dp[i-1]+1 | via coin 3 → dp[i-3]+1 | via coin 4 → dp[i-4]+1 | dp[i] | Note |
|---|---|---|---|---|---|
| 0 | — | — | — | 0 | base case, 0 coins for amount 0 |
| 1 | dp[0]+1=1 | n/a (1<3) | n/a (1<4) | 1 | only coin 1 fits |
| 2 | dp[1]+1=2 | n/a | n/a | 2 | two 1's |
| 3 | dp[2]+1=3 | dp[0]+1=1 | n/a | 1 | coin 3 alone beats three 1's |
| 4 | dp[3]+1=2 | dp[1]+1=2 | dp[0]+1=1 | 1 | coin 4 alone wins |
| 5 | dp[4]+1=2 | dp[2]+1=3 | dp[1]+1=2 | 2 | 1+4 or 1+1+3, tie at 2 coins |
| 6 | dp[5]+1=3 | dp[3]+1=2 | dp[2]+1=3 | 2 | coin 3 path wins: 3+3 |
Result: dp[6] = 2 (using 3+3). The trace shows the minimum is recomputed fresh at each amount from three already-finalized smaller answers — no amount is ever revisited once computed.
15 · Dynamic Programming II — Grids & Two Sequences
Flowchart leaves: DP (two sequences) string segmentation DP LIS (max/min)
The idea
When two strings/arrays interact, the state is a pair of prefix lengths dp[i][j] = answer for a[0..i) vs b[0..j). Grid-path problems use dp[i][j] over cell coordinates. The transition compares the current pair of characters/cells and pulls from the neighbours (diagonal / up / left). Most 2D DPs roll down to a 1D array over one dimension.
Brute force — recurse on both indices O(2^(m+n))
Branch on "match / skip in a / skip in b" without a table; the same (i,j) pair is revisited exponentially. The 2D table stores each pair once → O(m·n).
Template A · Longest Common Subsequence O(m·n)
a,b— the two input strings being compared.m,n— lengths ofaandb.dp[i][j]— length of the longest common subsequence using the firstichars ofaand firstjchars ofb.i,j— 1-indexed scan positions intoaandb(so the actual chars area[i-1],b[j-1]).- Returns — the length of the longest common subsequence shared by
aandb.
int lcs(String a, String b){
int m = a.length(), n = b.length();
int[][] dp = new int[m+1][n+1];
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = (a.charAt(i-1)==b.charAt(j-1))
? dp[i-1][j-1] + 1 // chars match
: Math.max(dp[i-1][j], dp[i][j-1]); // drop one side
return dp[m][n];
}int lcs(const std::string& a, const std::string& b){
int m = a.size(), n = b.size();
std::vector<std::vector<int>> dp(m+1, std::vector<int>(n+1, 0));
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = (a[i-1]==b[j-1])
? dp[i-1][j-1] + 1 // chars match
: std::max(dp[i-1][j], dp[i][j-1]); // drop one side
return dp[m][n];
}def lcs(a, b):
m, n = len(a), len(b)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = (dp[i-1][j-1] + 1 if a[i-1] == b[j-1] # chars match
else max(dp[i-1][j], dp[i][j-1])) # drop one side
return dp[m][n]function lcs(a: string, b: string): number {
const m = a.length, n = b.length;
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++)
for (let j = 1; j <= n; j++)
dp[i][j] = (a[i - 1] === b[j - 1])
? dp[i - 1][j - 1] + 1 // chars match
: Math.max(dp[i - 1][j], dp[i][j - 1]); // drop one side
return dp[m][n];
}Template B · Edit distance (Levenshtein)
a,b— the source string and the target string to transform it into.m,n— lengths ofaandb.dp[i][j]— minimum edits (insert/delete/replace) to turn the firstichars ofainto the firstjchars ofb.dp[i][0],dp[0][j]— base rows/columns: turning a prefix into empty (all deletes) or empty into a prefix (all inserts).- Returns — the minimum number of single-character edits to turn
aintob.
int editDistance(String a, String b){
int m=a.length(), n=b.length();
int[][] dp = new int[m+1][n+1];
for (int i=0;i<=m;i++) dp[i][0]=i; // delete all
for (int j=0;j<=n;j++) dp[0][j]=j; // insert all
for (int i=1;i<=m;i++)
for (int j=1;j<=n;j++)
dp[i][j] = (a.charAt(i-1)==b.charAt(j-1))
? dp[i-1][j-1]
: 1 + Math.min(dp[i-1][j-1], // replace
Math.min(dp[i-1][j], dp[i][j-1]));// delete / insert
return dp[m][n];
}int editDistance(const std::string& a, const std::string& b){
int m=a.size(), n=b.size();
std::vector<std::vector<int>> dp(m+1, std::vector<int>(n+1, 0));
for (int i=0;i<=m;i++) dp[i][0]=i; // delete all
for (int j=0;j<=n;j++) dp[0][j]=j; // insert all
for (int i=1;i<=m;i++)
for (int j=1;j<=n;j++)
dp[i][j] = (a[i-1]==b[j-1])
? dp[i-1][j-1]
: 1 + std::min(dp[i-1][j-1], // replace
std::min(dp[i-1][j], dp[i][j-1]));// delete / insert
return dp[m][n];
}def edit_distance(a, b):
m, n = len(a), len(b)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i # delete all
for j in range(n + 1):
dp[0][j] = j # insert all
for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = (dp[i-1][j-1] if a[i-1] == b[j-1]
else 1 + min(dp[i-1][j-1], # replace
dp[i-1][j], dp[i][j-1])) # delete / insert
return dp[m][n]function editDistance(a: string, b: string): number {
const m = a.length, n = b.length;
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = i; // delete all
for (let j = 0; j <= n; j++) dp[0][j] = j; // insert all
for (let i = 1; i <= m; i++)
for (let j = 1; j <= n; j++)
dp[i][j] = (a[i - 1] === b[j - 1])
? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j - 1], // replace
Math.min(dp[i - 1][j], dp[i][j - 1])); // delete / insert
return dp[m][n];
}Template C · count distinct subsequences (rolling 1D)
s— the source string you pick characters from.t— the target string you are trying to spell out as a subsequence.n— length oft.dp[j]— number of ways to form the firstjcharacters oftusing the part ofsscanned so far.dp[0]— base case: there is exactly one way to form an empty target (choose nothing).- Returns — the count of distinct ways the characters of
s, in order, can spell outt.
int numDistinct(String s, String t){
int n = t.length();
long[] dp = new long[n+1]; dp[0] = 1;
for (int i = 1; i <= s.length(); i++)
for (int j = n; j >= 1; j--) // reverse ⇒ 1D safe
if (s.charAt(i-1) == t.charAt(j-1)) dp[j] += dp[j-1];
return (int) dp[n];
}int numDistinct(const std::string& s, const std::string& t){
int n = t.size();
std::vector<long long> dp(n+1, 0); dp[0] = 1;
for (int i = 1; i <= (int)s.size(); i++)
for (int j = n; j >= 1; j--) // reverse ⇒ 1D safe
if (s[i-1] == t[j-1]) dp[j] += dp[j-1];
return (int) dp[n];
}def num_distinct(s, t):
n = len(t)
dp = [0] * (n + 1)
dp[0] = 1
for i in range(1, len(s) + 1):
for j in range(n, 0, -1): # reverse ⇒ 1D safe
if s[i-1] == t[j-1]:
dp[j] += dp[j-1]
return dp[n]function numDistinct(s: string, t: string): number {
const n = t.length;
const dp: number[] = new Array(n + 1).fill(0);
dp[0] = 1;
for (let i = 1; i <= s.length; i++)
for (let j = n; j >= 1; j--) // reverse ⇒ 1D safe
if (s[i - 1] === t[j - 1]) dp[j] += dp[j - 1];
return dp[n];
}Template D · word break (segmentation DP)
s— the string to try to split into dictionary words.dict/words— the set of valid words allowed as pieces.dp[i]— true if the prefixs[0..i)can be fully split into dictionary words.j— a candidate split point tried for eachi, checking whethers[j..i)is itself a dictionary word.- Returns — whether the entire string
scan be segmented into dictionary words.
boolean wordBreak(String s, List<String> dict){
Set<String> words = new HashSet<>(dict);
boolean[] dp = new boolean[s.length()+1]; dp[0] = true;
for (int i = 1; i <= s.length(); i++)
for (int j = 0; j < i; j++)
if (dp[j] && words.contains(s.substring(j, i))){ dp[i] = true; break; }
return dp[s.length()];
}bool wordBreak(const std::string& s, std::vector<std::string>& dict){
std::unordered_set<std::string> words(dict.begin(), dict.end());
std::vector<bool> dp(s.size()+1, false); dp[0] = true;
for (size_t i = 1; i <= s.size(); i++)
for (size_t j = 0; j < i; j++)
if (dp[j] && words.count(s.substr(j, i-j))){ dp[i] = true; break; }
return dp[s.size()];
}def word_break(s, dict_):
words = set(dict_)
dp = [False] * (len(s) + 1)
dp[0] = True
for i in range(1, len(s) + 1):
for j in range(i):
if dp[j] and s[j:i] in words:
dp[i] = True
break
return dp[len(s)]function wordBreak(s: string, dict: string[]): boolean {
const words = new Set(dict);
const dp: boolean[] = new Array(s.length + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= s.length; i++)
for (let j = 0; j < i; j++)
if (dp[j] && words.has(s.substring(j, i))) { dp[i] = true; break; }
return dp[s.length];
}Template E · Longest Increasing Subsequence O(n log n)
nums— the input array to find the longest increasing subsequence in.tails—tails[k]holds the smallest possible tail value of an increasing subsequence of lengthk+1found so far.size— length of the longest increasing subsequence found so far.x— the current number fromnumsbeing placed intotails.lo,hi,mid— binary-search bounds used to find wherexbelongs intails.- Returns — the length of the longest strictly increasing subsequence in
nums.
int lengthOfLIS(int[] nums){
int[] tails = new int[nums.length]; int size = 0;
for (int x : nums){
int lo = 0, hi = size;
while (lo < hi){ int mid=(lo+hi)/2; if (tails[mid]<x) lo=mid+1; else hi=mid; }
tails[lo] = x; // patience sorting
if (lo == size) size++;
}
return size;
}int lengthOfLIS(std::vector<int>& nums){
std::vector<int> tails(nums.size()); int size = 0;
for (int x : nums){
int lo = 0, hi = size;
while (lo < hi){ int mid=(lo+hi)/2; if (tails[mid]<x) lo=mid+1; else hi=mid; }
tails[lo] = x; // patience sorting
if (lo == size) size++;
}
return size;
}def length_of_lis(nums):
tails = [0] * len(nums)
size = 0
for x in nums:
lo, hi = 0, size
while lo < hi:
mid = (lo + hi) // 2
if tails[mid] < x:
lo = mid + 1
else:
hi = mid
tails[lo] = x # patience sorting
if lo == size:
size += 1
return sizefunction lengthOfLIS(nums: number[]): number {
const tails: number[] = new Array(nums.length).fill(0);
let size = 0;
for (const x of nums) {
let lo = 0, hi = size;
while (lo < hi) { const mid = (lo + hi) >> 1; if (tails[mid] < x) lo = mid + 1; else hi = mid; }
tails[lo] = x; // patience sorting
if (lo === size) size++;
}
return size;
}Worked example — LCS of "ABCBDAB" and "BDCAB"
Filling dp[i][j] (rows = A's prefix, cols = B's prefix); on a match take the diagonal +1, else the max of up/left:
"" B D C A B
"" 0 0 0 0 0 0
A 0 0 0 0 1 1
B 0 1 1 1 1 2
C 0 1 1 2 2 2
B 0 1 1 2 2 3
D 0 1 2 2 2 3
A 0 1 2 2 3 3
B 0 1 2 2 3 4 "" B D C A B
"" 0 0 0 0 0 0
A 0 0 0 0 1 1
B 0 1 1 1 1 2
C 0 1 1 2 2 2
B 0 1 1 2 2 3
D 0 1 2 2 2 3
A 0 1 2 2 3 3
B 0 1 2 2 3 4 "" B D C A B
"" 0 0 0 0 0 0
A 0 0 0 0 1 1
B 0 1 1 1 1 2
C 0 1 1 2 2 2
B 0 1 1 2 2 3
D 0 1 2 2 2 3
A 0 1 2 2 3 3
B 0 1 2 2 3 4 "" B D C A B
"" 0 0 0 0 0 0
A 0 0 0 0 1 1
B 0 1 1 1 1 2
C 0 1 1 2 2 2
B 0 1 1 2 2 3
D 0 1 2 2 2 3
A 0 1 2 2 3 3
B 0 1 2 2 3 4Bottom-right = 4 — e.g. the common subsequence "BCAB". Every cell reads only its three neighbours, so the whole grid is O(m·n).
🔬 Deeper dive & dry run
Why it works. dp[i][j] = LCS length of the first i chars of s1 and first j chars of s2. If the current characters match, they must both belong to the LCS, so dp[i][j] = dp[i-1][j-1] + 1; if they don't match, the LCS of the prefixes can't use both characters, so it's the better of dropping one: max(dp[i-1][j], dp[i][j-1]). Every cell only needs the row above and the current row, which is why row-by-row filling works.
When to reach for it. Two sequences, asking for the longest common/similar structure preserving relative order (not contiguity) — LCS, edit distance, shortest common supersequence, longest palindromic subsequence (LCS of s with reverse(s)).
Complexity. O(m·n) time and O(m·n) space for the full table; space reducible to O(min(m,n)) by keeping only two rows if reconstruction isn't needed.
Pitfalls & variations. Off-by-one on the 1-indexed dp table vs. 0-indexed strings is the single most common bug. Confusing LCS (subsequence, order preserved, gaps allowed) with longest common substring (contiguous) leads to the wrong recurrence entirely (substring resets to 0 on mismatch). Variants: edit distance replaces the match branch with insert/delete/replace costs.
Trace input: s1 = "ABCBDAB", s2 = "BDCABA" — length of the longest common subsequence.
| i | s1[i] | dp row (j=1..6, vs B,D,C,A,B,A) |
|---|---|---|
| 0 | - | 0,0,0,0,0,0 |
| 1 | A | 0,0,0,1,1,1 |
| 2 | B | 1,1,1,1,2,2 |
| 3 | C | 1,1,2,2,2,2 |
| 4 | B | 1,1,2,2,3,3 |
| 5 | D | 1,2,2,2,3,3 |
| 6 | A | 1,2,2,3,3,4 |
| 7 | B | 1,2,2,3,4,4 |
Result: dp[7][6] = 4 (one valid LCS is "BCBA"). Watch row 6→7: the match at s1[7]=B against s2[5]=B lifts dp[7][5] from 3 to 4 by pulling from dp[6][4]=3, showing the diagonal-plus-one rule in action.
🧰 Quick reference templates
35. Dynamic Programming — Two Sequences
... Multiple sequences YES ▸ Monotonic NO ▸ Split into subproblems YES
Two strings/arrays with a 2-D relationship (LCS, edit distance, distinct subsequences). State is a pair of prefix lengths. Rolling one dimension keeps memory O(min).
int numDistinct(String s, String t) { // # subsequences of s equal to t
int m = s.length(), n = t.length();
long[] dp = new long[n + 1];
dp[0] = 1; // empty t matches once
for (int i = 1; i <= m; i++)
for (int j = n; j >= 1; j--) // reverse ⇒ 1-D rolling is safe
if (s.charAt(i - 1) == t.charAt(j - 1)) dp[j] += dp[j - 1];
return (int) dp[n];
}int numDistinct(string s, string t) { // # subsequences of s equal to t
int m = s.length(), n = t.length();
vector<long long> dp(n + 1, 0);
dp[0] = 1; // empty t matches once
for (int i = 1; i <= m; i++)
for (int j = n; j >= 1; j--) // reverse ⇒ 1-D rolling is safe
if (s[i - 1] == t[j - 1]) dp[j] += dp[j - 1];
return (int) dp[n];
}def num_distinct(s, t): # # subsequences of s equal to t
m, n = len(s), len(t)
dp = [0] * (n + 1)
dp[0] = 1 # empty t matches once
for i in range(1, m + 1):
for j in range(n, 0, -1): # reverse ⇒ 1-D rolling is safe
if s[i - 1] == t[j - 1]:
dp[j] += dp[j - 1]
return dp[n]function numDistinct(s: string, t: string): number { // # subsequences of s equal to t
const m = s.length, n = t.length;
const dp: number[] = new Array(n + 1).fill(0);
dp[0] = 1; // empty t matches once
for (let i = 1; i <= m; i++)
for (let j = n; j >= 1; j--) // reverse ⇒ 1-D rolling is safe
if (s[i - 1] === t[j - 1]) dp[j] += dp[j - 1];
return dp[n];
}s="rabbbit", t="rabbit" ⇒ 3 distinct subsequences of s spell t.Find / Enumerate Indices
🎯 LeetCode practice & worked example
14 · 2-D Dynamic Programming
Tests state design over two indices — two strings, a grid, or an index plus a running budget — where the recurrence compares or combines a handful of neighboring cells; the tell is two sequences or a grid with an optimal-substructure relationship.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Unique Paths ★ | Medium | GoogleAmazonBloomberg | grid dp, combinatorics-equivalent |
| 2 | Longest Common Subsequence ★ | Medium | AmazonGoogleMicrosoft | 2-D string dp |
| 3 | Best Time to Buy and Sell Stock with Cooldown | Medium | AmazonGoogle | state-machine dp (hold/sold/rest) |
| 4 | Coin Change II | Medium | AmazonGoogle | unbounded knapsack, count combinations |
| 5 | Target Sum | Medium | FacebookAmazon | subset sum with +/- signs |
| 6 | Interleaving String | Medium | FacebookMicrosoft | 2-D string matching dp |
| 7 | Longest Increasing Path in a Matrix | Hard | GoogleAmazon | dfs + memoization on grid |
| 8 | Distinct Subsequences | Hard | AmazonGoogle | 2-D string counting dp |
| 9 | Edit Distance ★ | Medium | GoogleAmazonMicrosoft | 2-D string transform dp |
| 10 | Burst Balloons | Hard | GoogleAmazon | interval dp, last-burst-in-range |
| 11 | Regular Expression Matching ★ | Hard | GoogleFacebookAmazon | 2-D pattern-match dp |
Worked example — Longest Common Subsequence (LC 1143, Medium)
Frequently asked at: Amazon, Google, Microsoft, Bloomberg.
Problem. Given two strings text1 and text2, return the length of their longest common subsequence (characters in relative order, not necessarily contiguous). If there is none, return 0.
Idea. Let dp[i][j] = LCS length of text1[0..i) and text2[0..j). If the last characters match, extend the diagonal: dp[i][j] = dp[i-1][j-1] + 1. Otherwise the match must skip one character from either string: dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
}
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
};class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]class Solution {
longestCommonSubsequence(text1: string, text2: string): number {
const m = text1.length, n = text2.length;
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
}Complexity. Time O(m × n) Space O(m × n) — one cell per pair of prefixes; reducible to O(min(m,n)) with two rolling rows.
Walkthrough. text1 = "abcde", text2 = "ace". Filled grid (rows = text1 prefix length, cols = text2 prefix length):
| i \ j | "" | a | c | e |
|---|---|---|---|---|
| "" | 0 | 0 | 0 | 0 |
| a | 0 | 1 | 1 | 1 |
| b | 0 | 1 | 1 | 1 |
| c | 0 | 1 | 2 | 2 |
| d | 0 | 1 | 2 | 2 |
| e | 0 | 1 | 2 | 3 |
The diagonal bumps happen at matching pairs ('a','a'), ('c','c'), ('e','e'), giving dp[5][3] = 3 for the subsequence "ace".
🔬 Deeper dive & dry run
Why it works. dp[i][j] = LCS length of text1[0..i) and text2[0..j). If the last characters match, they must both belong to the LCS, so dp[i][j] = dp[i-1][j-1] + 1; if they don't match, the LCS either drops the last char of text1 or of text2, so dp[i][j] = max(dp[i-1][j], dp[i][j-1]). Every cell only depends on cells above/left/diagonal, so filling row-by-row (or column-by-column) is a valid topological order.
When to reach for it. Two sequences, you need a length/count that depends on aligning or comparing pairs of elements from each, and the "keep or skip one side" choice is visible in the problem (LCS, edit distance, distinct subsequences, interleaving strings).
Complexity. Time O(m × n) for the full grid. Space O(m × n) naively, reducible to O(min(m,n)) by keeping only the previous row since each cell needs just the row above and the current row so far.
Pitfalls & variations. Off-by-one indexing between the 1-indexed dp grid and 0-indexed strings is the #1 bug; confusing LCS (can skip, order preserved) with substring problems (must be contiguous, recurrence resets to 0 on mismatch) produces a completely different table shape; reconstructing the actual subsequence (not just its length) requires walking back through the grid from dp[m][n].
Trace input: text1 = "abcde", text2 = "ace" — find the length of their longest common subsequence, filling the dp grid one row of text1 at a time.
| i | text1[i-1] | dp row [j=0..3] (vs "","a","c","e") | Note |
|---|---|---|---|
| 0 | — | [0, 0, 0, 0] | base row: empty prefix of text1 matches nothing |
| 1 | a | [0, 1, 1, 1] | 'a'=='a' at j=1 → dp[0][0]+1=1; then max-carries right |
| 2 | b | [0, 1, 1, 1] | 'b' matches nothing new → row copies previous row exactly |
| 3 | c | [0, 1, 2, 2] | 'c'=='c' at j=2 → diagonal dp[2][1]+1 = 1+1 = 2 |
| 4 | d | [0, 1, 2, 2] | 'd' matches nothing new → row copies previous row |
| 5 | e | [0, 1, 2, 3] | 'e'=='e' at j=3 → diagonal dp[4][2]+1 = 2+1 = 3 |
Result: dp[5][3] = 3, the LCS is "ace". The trace shows two distinct row behaviors: a "match" jumps the value up from the diagonal, while a "no match" row is just the running max carried forward unchanged.
16 · Bitmask DP
Flowchart leaf: subset state (small n)
The idea
When the state is "which subset of ≤ ~20 items have I used", encode that subset as the bits of an integer. dp[mask][i] then means "best having visited exactly the set mask, currently at item i." This turns factorial search (n! orderings) into O(2ⁿ · n²) — the standard weapon for TSP, assignment, and "visit all" problems.
Bit operations you need
mask— an integer whose bits each represent whether one item is currently in the subset.i— the index (bit position) of the item being tested, added, or removed.1 << i— a mask with only bitiset, used to check/add/remove that one item.mask & -mask— isolates the lowest set bit, i.e. the lowest-numbered item currently in the set.- Returns — these are standalone expressions, not a function — each line evaluates to a new mask, a boolean, or a count of set bits.
mask & (1 << i) // is item i in the set?
mask | (1 << i) // add item i
mask & ~(1 << i) // remove item i
Integer.bitCount(mask) // how many items used
mask & -mask // lowest set bitmask & (1 << i) // is item i in the set?
mask | (1 << i) // add item i
mask & ~(1 << i) // remove item i
__builtin_popcount(mask) // how many items used
mask & -mask // lowest set bitmask & (1 << i) # is item i in the set?
mask | (1 << i) # add item i
mask & ~(1 << i) # remove item i
bin(mask).count('1') # how many items used
mask & -mask # lowest set bitmask & (1 << i) // is item i in the set?
mask | (1 << i) // add item i
mask & ~(1 << i) // remove item i
mask.toString(2).replace(/0/g, '').length // how many items used
mask & -mask // lowest set bitBrute force — try all orderings O(n!)
Permute all cities/tasks and evaluate each route. 12! ≈ 479 million already; bitmask DP does the same 12-node TSP in ~600k operations.
Template · shortest Hamiltonian path (TSP core) O(2ⁿ·n²)
dist— the distance/cost matrix between every pair of nodes.n— number of nodes (cities/tasks) to visit.FULL— the bitmask with allnbits set, meaning "every node has been visited".dp[mask][i]— cheapest cost of a path that has visited exactly the nodes inmaskand currently ends at nodei.mask— bitmask of nodes visited so far on a given path.j— a candidate next node to move to (must not already be set inmask).nm— the new mask after adding nodejtomask.ans— running minimum total cost across all possible ending nodes once every node is visited.- Returns — the minimum cost of a path that visits every node exactly once (the shortest Hamiltonian path).
int tsp(int[][] dist){
int n = dist.length, FULL = (1 << n) - 1;
int[][] dp = new int[1 << n][n];
for (int[] row : dp) Arrays.fill(row, Integer.MAX_VALUE / 2);
for (int i = 0; i < n; i++) dp[1 << i][i] = 0; // start at each node
for (int mask = 1; mask <= FULL; mask++)
for (int i = 0; i < n; i++){
if ((mask & (1 << i)) == 0) continue; // i must be in mask
for (int j = 0; j < n; j++){
if ((mask & (1 << j)) != 0) continue; // j not yet visited
int nm = mask | (1 << j);
dp[nm][j] = Math.min(dp[nm][j], dp[mask][i] + dist[i][j]);
}
}
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) ans = Math.min(ans, dp[FULL][i]);
return ans;
}int tsp(std::vector<std::vector<int>>& dist){
int n = dist.size(), FULL = (1 << n) - 1;
std::vector<std::vector<int>> dp(1 << n, std::vector<int>(n, INT_MAX / 2));
for (int i = 0; i < n; i++) dp[1 << i][i] = 0; // start at each node
for (int mask = 1; mask <= FULL; mask++)
for (int i = 0; i < n; i++){
if ((mask & (1 << i)) == 0) continue; // i must be in mask
for (int j = 0; j < n; j++){
if ((mask & (1 << j)) != 0) continue; // j not yet visited
int nm = mask | (1 << j);
dp[nm][j] = std::min(dp[nm][j], dp[mask][i] + dist[i][j]);
}
}
int ans = INT_MAX;
for (int i = 0; i < n; i++) ans = std::min(ans, dp[FULL][i]);
return ans;
}def tsp(dist):
n = len(dist)
FULL = (1 << n) - 1
INF = float('inf')
dp = [[INF] * n for _ in range(1 << n)]
for i in range(n):
dp[1 << i][i] = 0 # start at each node
for mask in range(1, FULL + 1):
for i in range(n):
if not (mask & (1 << i)):
continue # i must be in mask
for j in range(n):
if mask & (1 << j):
continue # j not yet visited
nm = mask | (1 << j)
dp[nm][j] = min(dp[nm][j], dp[mask][i] + dist[i][j])
return min(dp[FULL][i] for i in range(n))function tsp(dist: number[][]): number {
const n = dist.length, FULL = (1 << n) - 1;
const dp: number[][] = Array.from({ length: 1 << n }, () => new Array(n).fill(Infinity));
for (let i = 0; i < n; i++) dp[1 << i][i] = 0; // start at each node
for (let mask = 1; mask <= FULL; mask++)
for (let i = 0; i < n; i++) {
if ((mask & (1 << i)) === 0) continue; // i must be in mask
for (let j = 0; j < n; j++) {
if ((mask & (1 << j)) !== 0) continue; // j not yet visited
const nm = mask | (1 << j);
dp[nm][j] = Math.min(dp[nm][j], dp[mask][i] + dist[i][j]);
}
}
let ans = Infinity;
for (let i = 0; i < n; i++) ans = Math.min(ans, dp[FULL][i]);
return ans;
}Worked example — 3 nodes, how the masks fill
Distances symmetric: 0-1=1, 1-2=2, 0-2=4. Masks are 3-bit sets of visited nodes.
| mask (set) | meaning | key values |
|---|---|---|
| 001,010,100 | single start node | dp=0 at that node |
| 011 = {0,1} | visited 0&1 | dp[011][1]=1 (0→1) |
| 111 = {0,1,2} | all visited | dp[111][2]=dp[011][1]+dist(1,2)=1+2=3 |
Best path visiting all three ends at node 2 with cost 3 (0→1→2). Each subset is solved once and feeds larger subsets — the exponential factor is 2ⁿ, not n!.
🔬 Deeper dive & dry run
Why it works. A bitmask compresses "which subset of nodes has been visited" into a single integer, so dp[mask][j] = min cost of a path that has visited exactly the nodes in mask and currently sits at j. The recurrence extends smaller masks by one bit: dp[mask][j] = min over prev in mask (prev≠j) of dp[mask without j][prev] + dist[prev][j]. Because a mask with k bits set only depends on masks with k-1 bits, processing masks in increasing numeric order (or by popcount) guarantees dependencies are ready.
When to reach for it. n is small (≤ ~20) and the state is "a subset of items/nodes visited/used" — TSP, set-cover-style counting, assigning n tasks to n slots, or DP over subsets of a small graph.
Complexity. O(2^n · n²) time (each of 2^n masks, times n choices of current node, times n choices of previous node); O(2^n · n) space for the dp table.
Pitfalls & variations. Forgetting to add the return-to-start edge when the problem wants a cycle (Hamiltonian tour) vs. an open path is the classic omission. Bit-index confusion — using node id directly as the bit position only works if nodes are 0-indexed and the start node is excluded from the mask. Variants: profile/broken-profile DP for grid tiling uses the same bitmask-over-a-frontier idea.
Trace input: 4 cities {0=start,1,2,3}, dist(0,1)=10, dist(0,2)=15, dist(0,3)=20, dist(1,2)=35, dist(1,3)=25, dist(2,3)=30 — min-cost tour visiting all cities once and returning to 0 (TSP over the 3 non-start nodes).
| mask (bits=1,2,4) | dp[mask][end] | via |
|---|---|---|
| {1} | dp[1][1]=10 | 0→1 |
| {2} | dp[2][2]=15 | 0→2 |
| {4} | dp[4][3]=20 | 0→3 |
| {1,2} | dp[3][1]=50, dp[3][2]=45 | 2→1, 1→2 |
| {1,4} | dp[5][1]=45, dp[5][3]=35 | 3→1, 1→3 |
| {2,4} | dp[6][2]=50, dp[6][3]=45 | 3→2, 2→3 |
| {1,2,4} | dp[7][1]=70, dp[7][2]=65, dp[7][3]=75 | best of predecessor masks + edge |
| close tour | min(70+10, 65+15, 75+20) = 80 | best: 0→1→3→2→0 |
Result: Optimal tour cost is 80 (0→1→3→2→0 or its reverse). dp[7][2]=65 is built from dp[5][3]=35 + dist(3,2)=30, which itself came from dp[1][1]=10 + dist(1,3)=25 — a chain showing each mask strictly reuses the previous popcount's answers.
🧰 Quick reference templates
22. Bitmask DP
... Brute force NO ▸ Subset state YES
When the state is "which subset of ≤ ~20 items is done", encode it as an integer bitmask. dp[mask][i] = best having visited mask, currently at i. Classic for TSP / assignment.
int tsp(int[][] dist) { // shortest Hamiltonian path
int n = dist.length, FULL = (1 << n) - 1;
int[][] dp = new int[1 << n][n];
for (int[] row : dp) Arrays.fill(row, Integer.MAX_VALUE / 2);
for (int i = 0; i < n; i++) dp[1 << i][i] = 0;
for (int mask = 1; mask <= FULL; mask++)
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) == 0) continue;
for (int j = 0; j < n; j++) {
if ((mask & (1 << j)) != 0) continue;
int nm = mask | (1 << j);
dp[nm][j] = Math.min(dp[nm][j], dp[mask][i] + dist[i][j]);
}
}
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) ans = Math.min(ans, dp[FULL][i]);
return ans;
}int tsp(vector<vector<int>>& dist) { // shortest Hamiltonian path
int n = dist.size(), FULL = (1 << n) - 1;
vector<vector<int>> dp(1 << n, vector<int>(n, INT_MAX / 2));
for (int i = 0; i < n; i++) dp[1 << i][i] = 0;
for (int mask = 1; mask <= FULL; mask++)
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) == 0) continue;
for (int j = 0; j < n; j++) {
if ((mask & (1 << j)) != 0) continue;
int nm = mask | (1 << j);
dp[nm][j] = min(dp[nm][j], dp[mask][i] + dist[i][j]);
}
}
int ans = INT_MAX;
for (int i = 0; i < n; i++) ans = min(ans, dp[FULL][i]);
return ans;
}def tsp(dist): # shortest Hamiltonian path
n = len(dist)
FULL = (1 << n) - 1
INF = float('inf')
dp = [[INF] * n for _ in range(1 << n)]
for i in range(n):
dp[1 << i][i] = 0
for mask in range(1, FULL + 1):
for i in range(n):
if not (mask & (1 << i)):
continue
for j in range(n):
if mask & (1 << j):
continue
nm = mask | (1 << j)
dp[nm][j] = min(dp[nm][j], dp[mask][i] + dist[i][j])
return min(dp[FULL][i] for i in range(n))function tsp(dist: number[][]): number { // shortest Hamiltonian path
const n = dist.length, FULL = (1 << n) - 1;
const dp: number[][] = Array.from({ length: 1 << n }, () => new Array(n).fill(Infinity));
for (let i = 0; i < n; i++) dp[1 << i][i] = 0;
for (let mask = 1; mask <= FULL; mask++)
for (let i = 0; i < n; i++) {
if ((mask & (1 << i)) === 0) continue;
for (let j = 0; j < n; j++) {
if ((mask & (1 << j)) !== 0) continue;
const nm = mask | (1 << j);
dp[nm][j] = Math.min(dp[nm][j], dp[mask][i] + dist[i][j]);
}
}
let ans = Infinity;
for (let i = 0; i < n; i++) ans = Math.min(ans, dp[FULL][i]);
return ans;
}dp[1111][*] holds shortest route touching all four; answer = min over ending city. States = 2ⁿ·n.23. Dynamic Programming (small constraints)
... Brute force NO ▸ Subset state NO
Overlapping subproblems but no subset state ⇒ classic 1-D/2-D DP (knapsack, coin change, edit distance). Define the state, the transition, and the base case.
int coinChange(int[] coins, int amount) { // fewest coins to make amount
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // "infinity"
dp[0] = 0;
for (int a = 1; a <= amount; a++)
for (int c : coins)
if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}int coinChange(vector<int>& coins, int amount) { // fewest coins to make amount
vector<int> dp(amount + 1, amount + 1); // "infinity"
dp[0] = 0;
for (int a = 1; a <= amount; a++)
for (int c : coins)
if (c <= a) dp[a] = min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}def coin_change(coins, amount): # fewest coins to make amount
dp = [amount + 1] * (amount + 1) # "infinity"
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return -1 if dp[amount] > amount else dp[amount]function coinChange(coins: number[], amount: number): number { // fewest coins to make amount
const dp: number[] = new Array(amount + 1).fill(amount + 1); // "infinity"
dp[0] = 0;
for (let a = 1; a <= amount; a++)
for (const c of coins)
if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}coins={1,2,5}, amount 11. dp[11]=dp[6]+1=(dp[1]+1)+1... ⇒ 5+5+1 = 3 coins.Sums & Subarrays
17 · Greedy
Flowchart leaf: greedy solution (max/min)
The idea
Make the locally-best choice at each step and never reconsider. Greedy is only correct when a local optimum provably leads to a global optimum — proven via an exchange argument (any optimal solution can be transformed into the greedy one without getting worse) or a matroid structure. When greedy fails, you usually need DP.
Brute force — DP / try all splits O(n²) or worse
Interval scheduling, jump game, and Huffman all have DP formulations, but when the greedy choice is provably safe you skip the table entirely for O(n log n) or O(n).
Template A · interval scheduling (max non-overlapping) — sort by end O(n log n)
iv— the list of intervals/meetings, each given as[start, end].count— number of intervals selected so far.end— end time of the most recently selected interval (when the resource becomes free again).m— the interval currently being considered, in sorted order.- Returns — the maximum number of non-overlapping intervals that can be scheduled.
int maxMeetings(int[][] iv){
Arrays.sort(iv, (a,b) -> a[1] - b[1]); // earliest finish first
int count = 0, end = Integer.MIN_VALUE;
for (int[] m : iv)
if (m[0] >= end){ count++; end = m[1]; } // take it if it starts after last end
return count;
}int maxMeetings(std::vector<std::vector<int>>& iv){
std::sort(iv.begin(), iv.end(), [](const std::vector<int>& a, const std::vector<int>& b){
return a[1] < b[1]; // earliest finish first
});
int count = 0, end = INT_MIN;
for (auto& m : iv)
if (m[0] >= end){ count++; end = m[1]; } // take it if it starts after last end
return count;
}def max_meetings(iv):
iv.sort(key=lambda m: m[1]) # earliest finish first
count, end = 0, float('-inf')
for m in iv:
if m[0] >= end: # take it if it starts after last end
count += 1
end = m[1]
return countfunction maxMeetings(iv: number[][]): number {
iv.sort((a, b) => a[1] - b[1]); // earliest finish first
let count = 0, end = -Infinity;
for (const m of iv)
if (m[0] >= end) { count++; end = m[1]; } // take it if it starts after last end
return count;
}Template B · jump game II (fewest jumps) — BFS-like frontier
a— array wherea[i]is the farthest number of steps you can jump from indexi.jumps— number of jumps taken so far.curEnd— the farthest index reachable using the jumps taken so far — the boundary of the current "layer".farthest— the farthest index reachable from any position seen while scanning through the current layer.i— the index currently being scanned.- Returns — the minimum number of jumps needed to reach the last index of
a.
int jump(int[] a){
int jumps = 0, curEnd = 0, farthest = 0;
for (int i = 0; i < a.length - 1; i++){
farthest = Math.max(farthest, i + a[i]); // best reach from this layer
if (i == curEnd){ jumps++; curEnd = farthest; } // must jump; extend layer
}
return jumps;
}int jump(std::vector<int>& a){
int jumps = 0, curEnd = 0, farthest = 0;
for (int i = 0; i < (int)a.size() - 1; i++){
farthest = std::max(farthest, i + a[i]); // best reach from this layer
if (i == curEnd){ jumps++; curEnd = farthest; } // must jump; extend layer
}
return jumps;
}def jump(a):
jumps = cur_end = farthest = 0
for i in range(len(a) - 1):
farthest = max(farthest, i + a[i]) # best reach from this layer
if i == cur_end: # must jump; extend layer
jumps += 1
cur_end = farthest
return jumpsfunction jump(a: number[]): number {
let jumps = 0, curEnd = 0, farthest = 0;
for (let i = 0; i < a.length - 1; i++) {
farthest = Math.max(farthest, i + a[i]); // best reach from this layer
if (i === curEnd) { jumps++; curEnd = farthest; } // must jump; extend layer
}
return jumps;
}Worked example — interval scheduling
Meetings [1,3],[2,4],[3,5],[0,6],[5,7]. Sort by end → [1,3],[2,4],[3,5],[0,6],[5,7]:
| Meeting | start ≥ end? | take? | end after | count |
|---|---|---|---|---|
| [1,3] | 1 ≥ −∞ | yes | 3 | 1 |
| [2,4] | 2 ≥ 3? | no | 3 | 1 |
| [3,5] | 3 ≥ 3? | yes | 5 | 2 |
| [0,6] | 0 ≥ 5? | no | 5 | 2 |
| [5,7] | 5 ≥ 5? | yes | 7 | 3 |
3 meetings. Why earliest-end is safe: the meeting that frees the room soonest leaves the most room for the rest — swapping in any other first choice can't beat it (exchange argument).
🔬 Deeper dive & dry run
Why it works. Sorting by finish time and always keeping the interval that frees up the earliest is optimal because of an exchange argument: for any optimal solution, swapping its first-finishing interval for the earliest-finishing candidate never makes things worse and can only leave more room for later picks. The greedy choice never needs to be reconsidered — that's what makes it greedy rather than requiring backtracking.
When to reach for it. "Maximum number of non-overlapping intervals", "minimum number of rooms/platforms", or any problem where a locally-best choice (by some sort key) provably can't be beaten by saving the resource for later — provable via exchange argument or matroid structure.
Complexity. O(n log n) time dominated by the sort; O(1) extra space beyond the sort (O(n) if the sort isn't in-place).
Pitfalls & variations. Sorting by start time instead of finish time is the most common wrong greedy — it isn't optimal for maximizing count. Using > instead of >= when comparing start to lastEnd changes whether touching endpoints count as overlapping. Variants: weighted interval scheduling needs DP instead of greedy (greedy fails once profit varies), and minimum platforms/rooms sorts start and end events together instead.
Trace input: intervals sorted by end: A(1,3), B(2,5), C(4,6), D(6,7), E(5,8), F(7,9), G(8,10) — max count of non-overlapping intervals.
| interval | lastEnd before | start >= lastEnd? | decision | lastEnd after |
|---|---|---|---|---|
| A(1,3) | -∞ | yes | take | 3 |
| B(2,5) | 3 | no (2<3) | skip | 3 |
| C(4,6) | 3 | yes | take | 6 |
| D(6,7) | 6 | yes | take | 7 |
| E(5,8) | 7 | no (5<7) | skip | 7 |
| F(7,9) | 7 | yes | take | 9 |
| G(8,10) | 9 | no (8<9) | skip | 9 |
Result: 4 intervals selected: A, C, D, F. Note B and E are skipped even though each individually looks fine — they only conflict with the greedy pick, which is exactly the trade the exchange argument guarantees won't cost us the optimum.
🧰 Quick reference templates
31. Greedy Algorithms
... Split into subproblems NO ▸ Greedy solution YES
When a locally optimal choice is provably globally optimal (interval scheduling, jump game, Huffman), commit to the best step each iteration. No revisiting.
int jump(int[] nums) { // min jumps to reach the end
int jumps = 0, curEnd = 0, farthest = 0;
for (int i = 0; i < nums.length - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i == curEnd) { // must jump now
jumps++;
curEnd = farthest; // greedily to the farthest reachable
}
}
return jumps;
}int jump(vector<int>& nums) { // min jumps to reach the end
int jumps = 0, curEnd = 0, farthest = 0;
for (int i = 0; i < (int)nums.size() - 1; i++) {
farthest = max(farthest, i + nums[i]);
if (i == curEnd) { // must jump now
jumps++;
curEnd = farthest; // greedily to the farthest reachable
}
}
return jumps;
}def jump(nums): # min jumps to reach the end
jumps = 0
cur_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == cur_end: # must jump now
jumps += 1
cur_end = farthest # greedily to the farthest reachable
return jumpsfunction jump(nums: number[]): number { // min jumps to reach the end
let jumps = 0, curEnd = 0, farthest = 0;
for (let i = 0; i < nums.length - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i === curEnd) { // must jump now
jumps++;
curEnd = farthest; // greedily to the farthest reachable
}
}
return jumps;
}[2,3,1,1,4]. From 0 reach index 2; best next reach is index 4 ⇒ 2 jumps.Counting
🎯 LeetCode practice & worked example
15 · Greedy
Tests whether a locally optimal choice at each step provably leads to a globally optimal solution — the tell is an exchange argument or "process in sorted order and commit, never revisit" phrasing.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Maximum Subarray ★ | Medium | AmazonMicrosoftLinkedIn | Kadane's running max |
| 2 | Jump Game ★ | Medium | AmazonGoogleFacebook | track reachable frontier |
| 3 | Jump Game II | Medium | AmazonGoogle | min jumps via greedy level expansion |
| 4 | Gas Station | Medium | AmazonGoogle | prefix-sum reset on deficit |
| 5 | Hand of Straights | Medium | Amazon | counting map + greedy run grouping |
| 6 | Merge Triplets to Form Target Triplet | Medium | Amazon | component-wise greedy filter |
| 7 | Partition Labels | Medium | AmazonGoogle | last-index greedy window growth |
| 8 | Valid Parenthesis String | Medium | FacebookAmazon | track range of possible open-counts |
Worked example — Maximum Subarray, Kadane's Algorithm (LC 53, Medium)
Frequently asked at: Amazon, Microsoft, LinkedIn, Bloomberg.
Problem. Given an integer array nums, find the contiguous subarray with the largest sum and return that sum.
Idea. Greedily track curSum, the best sum of a subarray ending at the current index. At each step, either extend the previous run or restart from the current element — whichever is larger: curSum = max(nums[i], curSum + nums[i]). A negative running sum never helps a future subarray, so it's always correct to drop it and restart. Track the best curSum ever seen.
class Solution {
public int maxSubArray(int[] nums) {
int curSum = nums[0];
int best = nums[0];
for (int i = 1; i < nums.length; i++) {
curSum = Math.max(nums[i], curSum + nums[i]);
best = Math.max(best, curSum);
}
return best;
}
}
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int curSum = nums[0];
int best = nums[0];
for (int i = 1; i < (int)nums.size(); i++) {
curSum = max(nums[i], curSum + nums[i]);
best = max(best, curSum);
}
return best;
}
};class Solution:
def maxSubArray(self, nums: List[int]) -> int:
curSum = nums[0]
best = nums[0]
for i in range(1, len(nums)):
curSum = max(nums[i], curSum + nums[i])
best = max(best, curSum)
return bestclass Solution {
maxSubArray(nums: number[]): number {
let curSum = nums[0];
let best = nums[0];
for (let i = 1; i < nums.length; i++) {
curSum = Math.max(nums[i], curSum + nums[i]);
best = Math.max(best, curSum);
}
return best;
}
}Complexity. Time O(n) Space O(1) — single left-to-right pass, two scalars.
Walkthrough. nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| nums[i] | -2 | 1 | -3 | 4 | -1 | 2 | 1 | -5 | 4 |
| curSum | -2 | 1 | -2 | 4 | 3 | 5 | 6 | 1 | 5 |
| best | -2 | 1 | 1 | 4 | 4 | 5 | 6 | 6 | 6 |
The running sum resets at index 2 (dropping the -2 prefix) and peaks at 6 by index 6, covering the subarray [4, -1, 2, 1]. Answer: 6.
🔬 Deeper dive & dry run
Why it works. Kadane's greedily tracks cur, the best sum of a subarray ending at the current index. The greedy claim is: if extending the running sum with the next element ever drops below just starting fresh at that element (cur + x < x, i.e. cur < 0), a negative prefix can never help any future subarray, so it's always safe to discard it and restart. A running best then just remembers the max cur seen across all positions.
When to reach for it. "Best/worst contiguous run" over an array of signed numbers where you want a single pass, O(1) extra state, and the answer only depends on a local running aggregate (sum, product, or a similar reducible quantity) rather than global structure.
Complexity. Time O(n), one pass, one comparison and one addition per element. Space O(1), just two scalars.
Pitfalls & variations. Forgetting to initialize best to the first element (not 0) breaks all-negative arrays; the "reset to 0" version (cur = max(0, cur+x)) silently produces the wrong answer when every number is negative because it allows an empty subarray; the circular-array variant (LC 918) and the "return indices, not just sum" variant require tracking extra state beyond plain Kadane's.
Trace input: nums = [-2,1,-3,4,-1,2,1,-5,4] — find the maximum sum of any contiguous subarray.
| i | nums[i] | cur + nums[i] | cur = max(nums[i], cur+nums[i]) | best | Note |
|---|---|---|---|---|---|
| 0 | -2 | — | -2 | -2 | seed both with first element |
| 1 | 1 | -2+1=-1 | 1 | 1 | restart beats extending negative run |
| 2 | -3 | 1-3=-2 | -2 | 1 | extending still beats starting fresh (-2>-3) |
| 3 | 4 | -2+4=2 | 4 | 4 | restart at 4, best updates |
| 4 | -1 | 4-1=3 | 3 | 4 | keep extending, best unchanged |
| 5 | 2 | 3+2=5 | 5 | 5 | new best |
| 6 | 1 | 5+1=6 | 6 | 6 | new best |
| 7 | -5 | 6-5=1 | 1 | 6 | run absorbs the dip but drops below best |
| 8 | 4 | 1+4=5 | 5 | 6 | best stays 6 |
Result: best = 6, from subarray [4,-1,2,1]. The trace shows cur silently absorbing a small dip (index 4, -1) because staying attached still beat restarting, while best only updates when a new record is actually hit.
18 · Trie & String Matching
Flowchart leaves: Trie / string matching / rolling hash Trie / DP (segmentation)
The idea
A trie (prefix tree) stores strings by shared prefixes: each node is one character, each root-to-node path is a prefix. Insert/search/startsWith are O(word length), independent of how many words are stored — ideal for autocomplete, prefix queries, and matching many patterns at once. For single-pattern search inside text, KMP (O(n+m)) and rolling hash (Rabin-Karp) avoid re-scanning.
🧰 API — Trie (Prefix Tree) methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
// Create
class TrieNode {
TrieNode[] children = new TrieNode[26]; // or Map<Character, TrieNode> for larger alphabets
boolean isEnd;
}
Trie trie = new Trie(); // root node, no words yet
trie.insert(String word); // add word to the trie, O(L), L = word length
trie.search(String word); // true iff the exact word was inserted, O(L)
trie.startsWith(String prefix); // true iff some inserted word has this prefix, O(L)
trie.delete(String word); // (optional) remove word, unlink now-unused nodes, O(L)
// Create
struct TrieNode {
TrieNode* children[26] = {}; // or unordered_map<char, TrieNode*>
bool isEnd = false;
};
Trie trie; // root node, no words yet
trie.insert(const string& word); // add word, O(L), L = word length
trie.search(const string& word); // exact word exists?, O(L)
trie.startsWith(const string& prefix); // some word has this prefix?, O(L)
trie.erase(const string& word); // (optional) remove word, O(L)
# Create
class TrieNode:
def __init__(self):
self.children = {} # char -> TrieNode
self.is_end = False
trie = Trie() # root node, no words yet
trie.insert(word: str) -> None # add word, O(L), L = len(word)
trie.search(word: str) -> bool # exact word exists?, O(L)
trie.starts_with(prefix: str) -> bool # some word has this prefix?, O(L)
trie.delete(word: str) -> None # (optional) remove word, O(L)
// Create
class TrieNode {
children: Map<string, TrieNode> = new Map(); // or a fixed-size array for a-z only
isEnd = false;
}
class Trie {
private root = new TrieNode(); // root node, no words yet
insert(word: string): void {} // add word, O(L)
search(word: string): boolean { return false; } // exact word exists?, O(L)
startsWith(prefix: string): boolean { return false; } // some word has this prefix?, O(L)
delete(word: string): void {} // (optional) remove word, O(L)
}
Brute force — compare against every stored word O(words · len)
Checking a prefix by looping over all stored strings. The trie shares common prefixes so a lookup only walks the query's own characters.
Template A · Trie O(L) per op
next— array of 26 child pointers (one per letter) forming the tree's edges out of this node.end— flag marking that a complete inserted word ends exactly at this node.w/word— the word being inserted.p/prefix— the prefix being tested bystartsWith.node— the trie node currently being visited while walking down character by character.walk(s)— helper that follows the path spelled bysand returns the node reached, or null if the path breaks partway.- Returns —
insertbuilds the tree (no value);search/startsWithreturn true/false;walkreturns the node at the end of the path or null.
class Trie {
private final Trie[] next = new Trie[26];
private boolean end;
void insert(String w){
Trie node = this;
for (char c : w.toCharArray()){
int i = c - 'a';
if (node.next[i] == null) node.next[i] = new Trie();
node = node.next[i];
}
node.end = true;
}
boolean search(String w){ Trie n = walk(w); return n != null && n.end; }
boolean startsWith(String p){ return walk(p) != null; }
private Trie walk(String s){
Trie node = this;
for (char c : s.toCharArray()){
node = node.next[c - 'a'];
if (node == null) return null;
}
return node;
}
}class Trie {
private:
Trie* next[26] = {nullptr};
bool end = false;
Trie* walk(const std::string& s){
Trie* node = this;
for (char c : s){
node = node->next[c - 'a'];
if (node == nullptr) return nullptr;
}
return node;
}
public:
void insert(const std::string& w){
Trie* node = this;
for (char c : w){
int i = c - 'a';
if (node->next[i] == nullptr) node->next[i] = new Trie();
node = node->next[i];
}
node->end = true;
}
bool search(const std::string& w){ Trie* n = walk(w); return n != nullptr && n->end; }
bool startsWith(const std::string& p){ return walk(p) != nullptr; }
};class Trie:
def __init__(self):
self.next = [None] * 26
self.end = False
def insert(self, w):
node = self
for c in w:
i = ord(c) - ord('a')
if node.next[i] is None:
node.next[i] = Trie()
node = node.next[i]
node.end = True
def search(self, w):
n = self._walk(w)
return n is not None and n.end
def starts_with(self, p):
return self._walk(p) is not None
def _walk(self, s):
node = self
for c in s:
node = node.next[ord(c) - ord('a')]
if node is None:
return None
return nodeclass Trie {
private next: (Trie | null)[] = new Array(26).fill(null);
private end = false;
insert(w: string): void {
let node: Trie = this;
for (const c of w) {
const i = c.charCodeAt(0) - 'a'.charCodeAt(0);
if (node.next[i] === null) node.next[i] = new Trie();
node = node.next[i]!;
}
node.end = true;
}
search(w: string): boolean { const n = this.walk(w); return n !== null && n.end; }
startsWith(p: string): boolean { return this.walk(p) !== null; }
private walk(s: string): Trie | null {
let node: Trie | null = this;
for (const c of s) {
node = node.next[c.charCodeAt(0) - 'a'.charCodeAt(0)];
if (node === null) return null;
}
return node;
}
}Template B · KMP substring search O(n+m)
text— the string being searched in.pat— the pattern being searched for.m— length ofpat.lps— "longest proper prefix that is also a suffix" table:lps[k]says how much of the pattern's start already matches its own end up to positionk, so a mismatch can skip re-comparing characters already known to match.i— position scanning throughtext(or throughpatwhile buildinglps).j/len— position inpatcurrently being matched (or matched against itself while buildinglps).- Returns — the index in
textwherepatfirst occurs, or -1 if it never occurs.
int strStr(String text, String pat){
int m = pat.length(); int[] lps = new int[m]; // longest proper prefix=suffix
for (int i=1,len=0; i<m; ){
if (pat.charAt(i)==pat.charAt(len)) lps[i++] = ++len;
else if (len>0) len = lps[len-1];
else lps[i++] = 0;
}
for (int i=0,j=0; i<text.length(); ){
if (text.charAt(i)==pat.charAt(j)){ i++; j++; if (j==m) return i-m; }
else if (j>0) j = lps[j-1]; // reuse matched prefix, no re-scan
else i++;
}
return -1;
}int strStr(const std::string& text, const std::string& pat){
int m = pat.size(); std::vector<int> lps(m, 0); // longest proper prefix=suffix
for (int i=1,len=0; i<m; ){
if (pat[i]==pat[len]) lps[i++] = ++len;
else if (len>0) len = lps[len-1];
else lps[i++] = 0;
}
for (int i=0,j=0; i<(int)text.size(); ){
if (text[i]==pat[j]){ i++; j++; if (j==m) return i-m; }
else if (j>0) j = lps[j-1]; // reuse matched prefix, no re-scan
else i++;
}
return -1;
}def str_str(text, pat):
m = len(pat)
lps = [0] * m # longest proper prefix=suffix
i, length = 1, 0
while i < m:
if pat[i] == pat[length]:
length += 1
lps[i] = length
i += 1
elif length > 0:
length = lps[length - 1]
else:
lps[i] = 0
i += 1
i = j = 0
while i < len(text):
if text[i] == pat[j]:
i += 1
j += 1
if j == m:
return i - m
elif j > 0:
j = lps[j - 1] # reuse matched prefix, no re-scan
else:
i += 1
return -1function strStr(text: string, pat: string): number {
const m = pat.length; const lps: number[] = new Array(m).fill(0); // longest proper prefix=suffix
for (let i = 1, len = 0; i < m; ) {
if (pat[i] === pat[len]) lps[i++] = ++len;
else if (len > 0) len = lps[len - 1];
else lps[i++] = 0;
}
for (let i = 0, j = 0; i < text.length; ) {
if (text[i] === pat[j]) { i++; j++; if (j === m) return i - m; }
else if (j > 0) j = lps[j - 1]; // reuse matched prefix, no re-scan
else i++;
}
return -1;
}Worked example — trie holding {"app","apple"}
root ─a─ p ─p*─ l ─e* (* = word end)root ─a─ p ─p*─ l ─e* (* = word end)root ─a─ p ─p*─ l ─e* (* = word end)root ─a─ p ─p*─ l ─e* (* = word end)| Query | walk | result |
|---|---|---|
| search("app") | reach node after 2nd p, end=true | true |
| search("ap") | reach node, end=false | false |
| startsWith("appl") | path exists to l | true |
| search("apples") | no child 's' after e | false |
"app" and "apple" share the "app" path — stored once, so 5 nodes hold both words.
🔬 Deeper dive & dry run
Why it works. Each trie node represents a shared prefix; children branch on the next character, so any two words sharing a prefix share the same path from the root and only diverge where the strings actually differ. An isEnd flag per node distinguishes "this prefix exists in the tree" from "this prefix is itself a complete stored word" — the same node can be both a mid-path prefix for one word and a terminator for another.
When to reach for it. "Search / autocomplete / prefix match over a dictionary of strings", counting words with a given prefix, or word search on a grid with a dictionary — anywhere repeated prefix comparisons against a fixed word set would otherwise be redone from scratch.
Complexity. O(L) time per insert/search where L is the word length, independent of dictionary size; O(total characters × alphabet branching) space in the worst case (shared prefixes reduce this in practice).
Pitfalls & variations. Confusing "search" (must end exactly on a node with isEnd=true) with "startsWith" (just needs the path to exist) is the classic bug. Not reusing existing nodes on insert (always creating fresh nodes) breaks the whole point of prefix sharing. Variants: compressed/radix tries collapse single-child chains; suffix tries/Aho-Corasick extend this to multi-pattern matching.
Trace input: insert "app", insert "apple", then search("app") and search("appl") — build a trie over {"app","apple"} and test membership.
| op | char | node path | action | isEnd after |
|---|---|---|---|---|
| insert "app" | a | root→a | create | false |
| insert "app" | p | a→p | create | false |
| insert "app" | p | ap→p | create | true |
| insert "apple" | a | root→a | reuse | false |
| insert "apple" | p | a→p | reuse | false |
| insert "apple" | p | ap→p | reuse (already isEnd) | true |
| insert "apple" | l | app→l | create | false |
| insert "apple" | e | appl→e | create | true |
| search "app" | a,p,p | root→a→p→p | path exists, check isEnd | true → found |
| search "appl" | a,p,p,l | root→a→p→p→l | path exists, check isEnd | false → not found |
Result: "app" is found (isEnd=true at that node); "appl" exists only as a prefix (isEnd=false), so search reports false even though the path is walkable — the distinction the trie exists to make cheap.
🧰 Quick reference templates
19. Trie / String Matching / Rolling Hash
... Split/match with dictionary YES ▸ Prefix or pattern matching YES
Prefix queries and multi-word matching want a Trie (prefix tree): shared prefixes share nodes, so startsWith and autocomplete are O(word length). (KMP / rolling hash cover single-pattern search.)
class Trie {
private final Trie[] children = new Trie[26];
private boolean end;
void insert(String w) {
Trie node = this;
for (char c : w.toCharArray()) {
int i = c - 'a';
if (node.children[i] == null) node.children[i] = new Trie();
node = node.children[i];
}
node.end = true;
}
boolean search(String w) { Trie n = walk(w); return n != null && n.end; }
boolean startsWith(String p) { return walk(p) != null; }
private Trie walk(String s) {
Trie node = this;
for (char c : s.toCharArray()) {
node = node.children[c - 'a'];
if (node == null) return null;
}
return node;
}
}class Trie {
public:
Trie* children[26] = {};
bool end = false;
void insert(const string& w) {
Trie* node = this;
for (char c : w) {
int i = c - 'a';
if (node->children[i] == nullptr) node->children[i] = new Trie();
node = node->children[i];
}
node->end = true;
}
bool search(const string& w) { Trie* n = walk(w); return n != nullptr && n->end; }
bool startsWith(const string& p) { return walk(p) != nullptr; }
private:
Trie* walk(const string& s) {
Trie* node = this;
for (char c : s) {
node = node->children[c - 'a'];
if (node == nullptr) return nullptr;
}
return node;
}
};class Trie:
def __init__(self):
self.children = [None] * 26
self.end = False
def insert(self, w):
node = self
for c in w:
i = ord(c) - ord('a')
if node.children[i] is None:
node.children[i] = Trie()
node = node.children[i]
node.end = True
def search(self, w):
n = self._walk(w)
return n is not None and n.end
def starts_with(self, p):
return self._walk(p) is not None
def _walk(self, s):
node = self
for c in s:
node = node.children[ord(c) - ord('a')]
if node is None:
return None
return nodeclass Trie {
private children: (Trie | null)[] = new Array(26).fill(null);
private end = false;
insert(w: string): void {
let node: Trie = this;
for (const c of w) {
const i = c.charCodeAt(0) - 97;
if (node.children[i] === null) node.children[i] = new Trie();
node = node.children[i]!;
}
node.end = true;
}
search(w: string): boolean { const n = this.walk(w); return n !== null && n.end; }
startsWith(p: string): boolean { return this.walk(p) !== null; }
private walk(s: string): Trie | null {
let node: Trie | null = this;
for (const c of s) {
node = node.children[c.charCodeAt(0) - 97];
if (node === null) return null;
}
return node;
}
}search("app") = false (no end flag), startsWith("app") = true, search("apple") = true.20. Trie / DP / Memoization — Segmentation
... Split/match with dictionary YES ▸ Prefix/pattern matching NO
"Can this string be cut into dictionary words / how many ways?" is DP over prefixes: dp[i] = is s[0..i) segmentable. Try every split point that lands on a dictionary word.
boolean wordBreak(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true; // empty prefix
for (int i = 1; i <= n; i++)
for (int j = 0; j < i; j++)
if (dp[j] && dict.contains(s.substring(j, i))) { dp[i] = true; break; }
return dp[n];
}bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> dict(wordDict.begin(), wordDict.end());
int n = s.size();
vector<bool> dp(n + 1, false);
dp[0] = true; // empty prefix
for (int i = 1; i <= n; i++)
for (int j = 0; j < i; j++)
if (dp[j] && dict.count(s.substr(j, i - j))) { dp[i] = true; break; }
return dp[n];
}def word_break(s, word_dict):
dict_set = set(word_dict)
n = len(s)
dp = [False] * (n + 1)
dp[0] = True # empty prefix
for i in range(1, n + 1):
for j in range(i):
if dp[j] and s[j:i] in dict_set:
dp[i] = True
break
return dp[n]function wordBreak(s: string, wordDict: string[]): boolean {
const dict = new Set(wordDict);
const n = s.length;
const dp = new Array(n + 1).fill(false);
dp[0] = true; // empty prefix
for (let i = 1; i <= n; i++)
for (let j = 0; j < i; j++)
if (dp[j] && dict.has(s.slice(j, i))) { dp[i] = true; break; }
return dp[n];
}s="leetcode", dict={leet,code}. dp[4]=true ("leet"), then dp[8]=true ("code") ⇒ true.Small Constraints: Brute Force / DP / Bitmask
🎯 LeetCode practice & worked example
8 · Tries
Tests prefix-shared storage and retrieval over strings — the tell is repeated work on "starts with", autocomplete, dictionary lookups, or word-search over a fixed word list; reach for a 26-ary (or map-based) trie node with an end-of-word flag.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Implement Trie (Prefix Tree) ★ | Medium | AmazonGoogleMicrosoft | array-of-children trie node |
| 2 | Design Add and Search Words Data Structure ★ | Medium | AmazonGoogleUber | trie DFS with wildcard '.' branching |
| 3 | Word Search II ★ | Hard | AmazonGoogleMicrosoft | trie + board backtracking with pruning |
| 4 | Longest Word in Dictionary | Medium | AmazonGoogle | trie build + DFS on complete-prefix chains |
| 5 | Replace Words | Medium | AmazonBloomberg | shortest-prefix-root lookup per word |
| 6 | Maximum XOR of Two Numbers in an Array | Medium | GoogleAmazon | binary-bit trie, greedy opposite-bit descent |
Worked example — Implement Trie (Prefix Tree) (LC 208, Medium)
Frequently asked at: Amazon, Google, Microsoft.
Problem. Implement a trie with insert(word), search(word) (exact match), and startsWith(prefix) (prefix match), all operating on lowercase English letters.
Idea. Each trie node holds a fixed-size array of 26 child pointers (one per letter) plus a boolean marking "a word ends here". Insert and search both just walk character by character, either creating or following child links; startsWith is identical to search minus the end-of-word check.
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for (char c : word.toCharArray()) {
int i = c - 'a';
if (node.children[i] == null) node.children[i] = new Trie();
node = node.children[i];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = find(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
return find(prefix) != null;
}
private Trie find(String s) {
Trie node = this;
for (char c : s.toCharArray()) {
int i = c - 'a';
if (node.children[i] == null) return null;
node = node.children[i];
}
return node;
}
}
class Trie {
private:
Trie* children[26];
bool isEnd;
public:
Trie() {
for (int i = 0; i < 26; i++) children[i] = nullptr;
isEnd = false;
}
void insert(string word) {
Trie* node = this;
for (char c : word) {
int i = c - 'a';
if (node->children[i] == nullptr) node->children[i] = new Trie();
node = node->children[i];
}
node->isEnd = true;
}
bool search(string word) {
Trie* node = find(word);
return node != nullptr && node->isEnd;
}
bool startsWith(string prefix) {
return find(prefix) != nullptr;
}
private:
Trie* find(string s) {
Trie* node = this;
for (char c : s) {
int i = c - 'a';
if (node->children[i] == nullptr) return nullptr;
node = node->children[i];
}
return node;
}
};class Trie:
def __init__(self):
self.children = [None] * 26
self.isEnd = False
def insert(self, word: str) -> None:
node = self
for c in word:
i = ord(c) - ord('a')
if node.children[i] is None:
node.children[i] = Trie()
node = node.children[i]
node.isEnd = True
def search(self, word: str) -> bool:
node = self._find(word)
return node is not None and node.isEnd
def startsWith(self, prefix: str) -> bool:
return self._find(prefix) is not None
def _find(self, s: str):
node = self
for c in s:
i = ord(c) - ord('a')
if node.children[i] is None:
return None
node = node.children[i]
return nodeclass Trie {
private children: (Trie | null)[];
private isEnd: boolean;
constructor() {
this.children = new Array(26).fill(null);
this.isEnd = false;
}
insert(word: string): void {
let node: Trie = this;
for (const c of word) {
const i = c.charCodeAt(0) - 'a'.charCodeAt(0);
if (node.children[i] === null) node.children[i] = new Trie();
node = node.children[i]!;
}
node.isEnd = true;
}
search(word: string): boolean {
const node = this.find(word);
return node !== null && node.isEnd;
}
startsWith(prefix: string): boolean {
return this.find(prefix) !== null;
}
private find(s: string): Trie | null {
let node: Trie = this;
for (const c of s) {
const i = c.charCodeAt(0) - 'a'.charCodeAt(0);
if (node.children[i] === null) return null;
node = node.children[i]!;
}
return node;
}
}Complexity. Time O(L) Space O(N·L) — each call costs the length L of the word/prefix; total space is proportional to the number of inserted words N times their average length (bounded by shared prefixes).
Walkthrough. Operations: insert("apple"), search("app"), startsWith("app"), insert("app"), search("app").
| Call | What happens | Returns |
|---|---|---|
| insert("apple") | creates chain a-p-p-l-e, marks 'e' node isEnd=true | — |
| search("app") | walks a-p-p, reaches a real node but isEnd is false there | false |
| startsWith("app") | walks a-p-p, node exists regardless of isEnd | true |
| insert("app") | walks existing a-p-p node, now marks its isEnd=true | — |
| search("app") | walks a-p-p, isEnd is now true | true |
🔬 Deeper dive & dry run
Why it works. Each trie node is a map from character to child node plus an isEnd flag; words sharing a prefix share the same chain of nodes for that prefix. Insert/search/startsWith all just walk one character at a time from the root, so the cost depends only on the query's length, never on how many other words are stored.
When to reach for it. Repeated prefix queries over a set of strings — autocomplete, prefix counting, word-search boards, longest common prefix at scale — anywhere you'd otherwise repeatedly rescan prefixes of many strings.
Complexity. O(L) time per insert/search/startsWith, where L is the query length, independent of how many words are stored; O(total characters inserted) space in the worst case of no shared prefixes.
Pitfalls & variations. Conflating startsWith (path must exist) with search (path must exist AND the end node's isEnd must be true) is the classic bug. Forgetting to set isEnd makes every inserted word also satisfy prefix-only search. Using a fixed 26-slot array per node wastes memory for large or sparse alphabets versus a hashmap.
Trace input: insert("cat"); insert("car"); search("car"); search("ca"); startsWith("ca"); search("cats") — build the trie and answer each query.
| Step | Operation | Path walked | Node changes | Return |
|---|---|---|---|---|
| 0 | insert("cat") | root→c→a→t (all new) | create c, a, t; mark t.isEnd=true | — |
| 1 | insert("car") | root→c→a (reused)→r (new) | create r branching off shared "ca"; mark r.isEnd=true | — |
| 2 | search("car") | root→c→a→r | r exists and r.isEnd=true | true |
| 3 | search("ca") | root→c→a | a exists but a.isEnd=false | false |
| 4 | startsWith("ca") | root→c→a | a exists (isEnd irrelevant) | true |
| 5 | search("cats") | root→c→a→t, then need child "s" | t has no child "s" | false |
Result: "cat" and "car" share the "ca" branch. The trace shows search("ca") failing (isEnd false) while startsWith("ca") succeeds on the identical path — exactly the distinction that motivates a separate end-of-word flag.
19 · Interval Sweep
Flowchart leaf: merge / insert / scan intervals
The idea
Most interval problems collapse once you sort by start (or end) and sweep once, maintaining the "current" interval: merge overlaps, flush and restart on a gap, or insert in order. For "max concurrent" problems, a sweep line over sorted +1/−1 events counts overlaps in O(n log n).
Brute force — compare all pairs O(n²)
Testing every pair for overlap. Sorting imposes an order so a single left-to-right pass sees all overlaps in sequence.
Template A · merge overlapping intervals O(n log n)
iv— the input list of intervals, each[start, end].cur— the interval currently being extended as later intervals are folded into it.out— the list of finalized, non-overlapping merged intervals.i— index of the next interval being considered, after sorting by start.- Returns — the list of intervals after merging every pair that overlapped.
int[][] merge(int[][] iv){
Arrays.sort(iv, (a,b) -> a[0] - b[0]);
List<int[]> out = new ArrayList<>();
int[] cur = iv[0];
for (int i = 1; i < iv.length; i++){
if (iv[i][0] <= cur[1]) cur[1] = Math.max(cur[1], iv[i][1]); // overlap → extend
else { out.add(cur); cur = iv[i]; } // gap → flush
}
out.add(cur);
return out.toArray(new int[0][]);
}std::vector<std::vector<int>> merge(std::vector<std::vector<int>>& iv){
std::sort(iv.begin(), iv.end(), [](const std::vector<int>& a, const std::vector<int>& b){
return a[0] < b[0];
});
std::vector<std::vector<int>> out;
std::vector<int> cur = iv[0];
for (size_t i = 1; i < iv.size(); i++){
if (iv[i][0] <= cur[1]) cur[1] = std::max(cur[1], iv[i][1]); // overlap → extend
else { out.push_back(cur); cur = iv[i]; } // gap → flush
}
out.push_back(cur);
return out;
}def merge(iv):
iv.sort(key=lambda x: x[0])
out = []
cur = iv[0]
for i in range(1, len(iv)):
if iv[i][0] <= cur[1]:
cur[1] = max(cur[1], iv[i][1]) # overlap → extend
else:
out.append(cur) # gap → flush
cur = iv[i]
out.append(cur)
return outfunction merge(iv: number[][]): number[][] {
iv.sort((a, b) => a[0] - b[0]);
const out: number[][] = [];
let cur = iv[0];
for (let i = 1; i < iv.length; i++) {
if (iv[i][0] <= cur[1]) cur[1] = Math.max(cur[1], iv[i][1]); // overlap → extend
else { out.push(cur); cur = iv[i]; } // gap → flush
}
out.push(cur);
return out;
}Template B · sweep line — max concurrent intervals
iv— the input list of intervals.starts,ends— all interval start times and all end times, each sorted independently (not paired by original interval).i— pointer intostarts: the next interval about to begin.j— pointer intoends: the next interval about to finish.cur— number of intervals currently overlapping at the sweep position.best— the highest valuecurhas reached so far.- Returns — the maximum number of intervals that are ever active at the same time.
int maxOverlap(int[][] iv){
int n = iv.length; int[] starts = new int[n], ends = new int[n];
for (int i=0;i<n;i++){ starts[i]=iv[i][0]; ends[i]=iv[i][1]; }
Arrays.sort(starts); Arrays.sort(ends);
int i=0,j=0,cur=0,best=0;
while (i<n){
if (starts[i] < ends[j]){ cur++; best=Math.max(best,cur); i++; }
else { cur--; j++; }
}
return best;
}int maxOverlap(std::vector<std::vector<int>>& iv){
int n = iv.size(); std::vector<int> starts(n), ends(n);
for (int i=0;i<n;i++){ starts[i]=iv[i][0]; ends[i]=iv[i][1]; }
std::sort(starts.begin(), starts.end()); std::sort(ends.begin(), ends.end());
int i=0,j=0,cur=0,best=0;
while (i<n){
if (starts[i] < ends[j]){ cur++; best=std::max(best,cur); i++; }
else { cur--; j++; }
}
return best;
}def max_overlap(iv):
n = len(iv)
starts = sorted(x[0] for x in iv)
ends = sorted(x[1] for x in iv)
i = j = cur = best = 0
while i < n:
if starts[i] < ends[j]:
cur += 1
best = max(best, cur)
i += 1
else:
cur -= 1
j += 1
return bestfunction maxOverlap(iv: number[][]): number {
const n = iv.length;
const starts = iv.map(x => x[0]).sort((a, b) => a - b);
const ends = iv.map(x => x[1]).sort((a, b) => a - b);
let i = 0, j = 0, cur = 0, best = 0;
while (i < n) {
if (starts[i] < ends[j]) { cur++; best = Math.max(best, cur); i++; }
else { cur--; j++; }
}
return best;
}Worked example — merge [[1,3],[2,6],[8,10],[15,18]]
| Interval | starts ≤ cur.end? | action | cur / output |
|---|---|---|---|
| [1,3] | seed | — | cur=[1,3] |
| [2,6] | 2 ≤ 3 yes | extend end to 6 | cur=[1,6] |
| [8,10] | 8 ≤ 6 no | flush [1,6], start | cur=[8,10] |
| [15,18] | 15 ≤ 10 no | flush [8,10], start | cur=[15,18] |
Output [[1,6],[8,10],[15,18]]. Sorting guarantees any interval that overlaps cur comes right after it.
🔬 Deeper dive & dry run
Why it works. After sorting by start time, any interval that overlaps the currently-open merged interval must have a start ≤ the current merged end (since it's sorted, later intervals can only start later or equal). So a single left-to-right sweep with one "current merged interval" suffices — the moment a new interval's start exceeds the current end, every future interval (sorted) is also guaranteed disjoint from it, so it can be closed off for good.
When to reach for it. "Merge overlapping intervals", "insert an interval into a sorted list", meeting-room/free-time problems, or any question about intervals where the answer only depends on order along one axis — sort, then sweep once.
Complexity. O(n log n) time for the sort (the sweep itself is O(n)); O(n) space for the output (O(log n) extra if sorting in place).
Pitfalls & variations. Comparing with < instead of <= at the boundary decides whether touching intervals like [1,3] and [3,5] merge — get the problem's convention right. Forgetting to sort first (or sorting by end instead of start) breaks the single-pass guarantee. Variants: sweep-line with separate start/end events (for counting overlaps at any point) instead of merging whole intervals.
Trace input: intervals = [[1,3],[2,6],[5,7],[8,10],[9,12],[15,18]] (already sorted by start) — merge all overlapping intervals.
| current | last merged | overlap? (start <= last.end) | merged list after |
|---|---|---|---|
| [1,3] | none | - | [[1,3]] |
| [2,6] | [1,3] | yes (2<=3) | [[1,6]] |
| [5,7] | [1,6] | yes (5<=6) | [[1,7]] |
| [8,10] | [1,7] | no (8>7) | [[1,7],[8,10]] |
| [9,12] | [8,10] | yes (9<=10) | [[1,7],[8,12]] |
| [15,18] | [8,12] | no (15>12) | [[1,7],[8,12],[15,18]] |
Result: Merged output is [[1,7],[8,12],[15,18]]. The merged end keeps extending (3→6→7) across two consecutive overlaps before the sweep hits a genuine gap at [8,10], showing merges can chain across more than one input interval.
🧰 Quick reference templates
17. Sorting + Interval Scan
... Fast lookup NO ▸ Merge/insert/scan intervals YES
Most interval problems collapse after sorting by start (or end). Sweep once and either merge with the current interval, flush it and start a new one, or insert in place. The canonical "sort first, scan once."
int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
List<int[]> res = new ArrayList<>();
int[] cur = intervals[0];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] <= cur[1]) { // overlap
cur[1] = Math.max(cur[1], intervals[i][1]); // extend
} else {
res.add(cur); // flush
cur = intervals[i];
}
}
res.add(cur);
return res.toArray(new int[0][]);
}vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) { return a[0] < b[0]; });
vector<vector<int>> res;
vector<int> cur = intervals[0];
for (int i = 1; i < (int)intervals.size(); i++) {
if (intervals[i][0] <= cur[1]) { // overlap
cur[1] = max(cur[1], intervals[i][1]); // extend
} else {
res.push_back(cur); // flush
cur = intervals[i];
}
}
res.push_back(cur);
return res;
}def merge(intervals):
intervals.sort(key=lambda a: a[0])
res = []
cur = intervals[0]
for i in range(1, len(intervals)):
if intervals[i][0] <= cur[1]: # overlap
cur[1] = max(cur[1], intervals[i][1]) # extend
else:
res.append(cur) # flush
cur = intervals[i]
res.append(cur)
return resfunction merge(intervals: number[][]): number[][] {
intervals.sort((a, b) => a[0] - b[0]);
const res: number[][] = [];
let cur = intervals[0];
for (let i = 1; i < intervals.length; i++) {
if (intervals[i][0] <= cur[1]) { // overlap
cur[1] = Math.max(cur[1], intervals[i][1]); // extend
} else {
res.push(cur); // flush
cur = intervals[i];
}
}
res.push(cur);
return res;
}[[1,3],[2,6],[8,10],[15,18]] sorted. [1,3] & [2,6] overlap → [1,6]; [8,10], [15,18] separate ⇒ [[1,6],[8,10],[15,18]].🎯 LeetCode practice & worked example
16 · Intervals
Tests handling of overlapping ranges — the tell is "sort by start or end, then sweep once" to merge, count conflicts, or schedule resources.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Insert Interval ★ | Medium | GoogleFacebookLinkedIn | merge while inserting into sorted list |
| 2 | Merge Intervals ★ | Medium | FacebookAmazonGoogle | sort by start, sweep and merge |
| 3 | Non-overlapping Intervals | Medium | AmazonFacebook | sort by end, greedy keep earliest finish |
| 4 | Meeting Rooms | Easy | FacebookGoogleAmazon | sort + adjacent overlap check |
| 5 | Meeting Rooms II ★ | Medium | FacebookGoogleAmazon | min-heap of active end times |
| 6 | Minimum Interval to Include Each Query | Hard | offline sort + min-heap sweep |
Worked example — Merge Intervals (LC 56, Medium)
Frequently asked at: Facebook, Amazon, Google, Bloomberg.
Problem. Given an array of intervals where intervals[i] = [start_i, end_i], merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the input ranges.
Idea. Sort intervals by start time. Then sweep once: if the current interval's start is beyond the end of the last merged interval, it starts a new group; otherwise it overlaps, so extend the last merged interval's end to the max of the two. Sorting is what makes a single linear sweep sufficient — the greedy step (extend vs. start new) is always correct because intervals are processed in start order.
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> merged = new ArrayList<>();
for (int[] iv : intervals) {
if (merged.isEmpty() || iv[0] > merged.get(merged.size() - 1)[1]) {
merged.add(iv);
} else {
int[] last = merged.get(merged.size() - 1);
last[1] = Math.max(last[1], iv[1]);
}
}
return merged.toArray(new int[merged.size()][]);
}
}
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
});
vector<vector<int>> merged;
for (auto& iv : intervals) {
if (merged.empty() || iv[0] > merged.back()[1]) {
merged.push_back(iv);
} else {
merged.back()[1] = max(merged.back()[1], iv[1]);
}
}
return merged;
}
};class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda a: a[0])
merged = []
for iv in intervals:
if not merged or iv[0] > merged[-1][1]:
merged.append(iv)
else:
merged[-1][1] = max(merged[-1][1], iv[1])
return mergedclass Solution {
merge(intervals: number[][]): number[][] {
intervals.sort((a, b) => a[0] - b[0]);
const merged: number[][] = [];
for (const iv of intervals) {
if (merged.length === 0 || iv[0] > merged[merged.length - 1][1]) {
merged.push(iv);
} else {
merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], iv[1]);
}
}
return merged;
}
}Complexity. Time O(n log n) Space O(n) — sorting dominates; the sweep itself is linear.
Walkthrough. intervals = [[1,3],[2,6],[8,10],[15,18]] (already sorted by start):
| step | current interval | action | merged list |
|---|---|---|---|
| 1 | [1,3] | list empty → add | [[1,3]] |
| 2 | [2,6] | 2 ≤ 3 → extend last end to 6 | [[1,6]] |
| 3 | [8,10] | 8 > 6 → add new group | [[1,6],[8,10]] |
| 4 | [15,18] | 15 > 10 → add new group | [[1,6],[8,10],[15,18]] |
Result: [[1,6],[8,10],[15,18]].
🔬 Deeper dive & dry run
Why it works. Sorting by start turns "do any two intervals overlap" into a purely local question: once sorted, interval k can only possibly overlap the last merged interval, never an earlier one, because every interval before it already has a smaller-or-equal start. That collapses an all-pairs overlap check into one linear left-to-right sweep that only ever needs to compare against the single most recent merged interval.
When to reach for it. Problems about ranges/intervals on a line (meeting rooms, insert interval, merge, calendar booking) where the question depends on overlap or ordering, not on the intervals' original positions — a strong signal is that sorting is "free" to do first without losing information the problem needs.
Complexity. Time O(n log n) dominated by the sort; the merge sweep itself is O(n). Space O(n) for the output (or O(log n)–O(n) extra for the sort depending on implementation).
Pitfalls & variations. Using strict < instead of ≤ when checking overlap mishandles touching intervals like [1,3] and [3,5] (are they "overlapping" or just adjacent? read the problem); forgetting to sort first (or sorting by end instead of start) breaks the local-comparison invariant; "insert interval" (LC 57) is the same idea but avoids a full re-sort by exploiting that the list is already sorted.
Trace input: intervals = [[1,3],[2,6],[8,10],[15,18]] (already sorted by start) — merge all overlapping intervals.
| Step | Current interval | Last merged | Overlap? (cur.start ≤ last.end) | Action | merged |
|---|---|---|---|---|---|
| 0 | [1,3] | none | — | push as first merged interval | [[1,3]] |
| 1 | [2,6] | [1,3] | 2 ≤ 3 → yes | merge: extend end to max(3,6)=6 | [[1,6]] |
| 2 | [8,10] | [1,6] | 8 ≤ 6 → no | no overlap, push new interval | [[1,6],[8,10]] |
| 3 | [15,18] | [8,10] | 15 ≤ 10 → no | no overlap, push new interval | [[1,6],[8,10],[15,18]] |
Result: [[1,6],[8,10],[15,18]]. The trace shows the algorithm only ever needs one comparison per step (current vs. last-merged) — [2,6] silently absorbs into [1,3] while [8,10] and [15,18] each start a fresh group.
20 · Linked List Manipulation
Flowchart leaf: linked list manipulation
The idea
Pointer surgery: reverse, reorder, merge, remove. Three habits prevent 90% of bugs — save next before you overwrite a link, use a dummy head when the head might change, and draw the pointers before coding. Fast/slow pointers (see §10) find the middle and detect cycles.
🧰 API — Linked List methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
// --- Built-in doubly linked list ---
LinkedList<Integer> list = new LinkedList<>();
list.addFirst(x); // insert at head, O(1)
list.addLast(x); // insert at tail, O(1)
list.removeFirst(); // remove & return head, O(1)
list.removeLast(); // remove & return tail, O(1)
list.peekFirst(); // look at head, O(1)
list.peekLast(); // look at tail, O(1)
list.get(i); // indexed access, O(n)
list.size(); // number of elements, O(1)
for (int v : list) {} // iterate, O(n)
// --- Manual singly-linked node (the form interviews actually use) ---
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
ListNode head = new ListNode(1); // create a node, O(1)
ListNode cur = head;
while (cur != null) { cur = cur.next; } // traverse, O(n)
// insert newNode right after node p
newNode.next = p.next; // O(1)
p.next = newNode; // O(1)
// delete the node right after p
p.next = p.next.next; // O(1)
// reverse a singly linked list, O(n)
ListNode prev = null, curr = head;
while (curr != null) {
ListNode next = curr.next; // save next before overwriting
curr.next = prev; // reverse the pointer
prev = curr; // advance prev
curr = next; // advance curr
}
head = prev;
// --- Built-in doubly linked list ---
#include <list>
std::list<int> lst;
lst.push_front(x); // insert at head, O(1)
lst.push_back(x); // insert at tail, O(1)
lst.pop_front(); // remove head (no return value), O(1)
lst.pop_back(); // remove tail (no return value), O(1)
lst.front(); // reference to head, O(1)
lst.back(); // reference to tail, O(1)
lst.insert(it, x); // insert before iterator it, O(1)
lst.erase(it); // erase element at iterator it, O(1)
lst.size(); // number of elements, O(1)
for (int v : lst) {} // iterate, O(n)
// --- Manual singly-linked node (the form interviews actually use) ---
struct ListNode {
int val;
ListNode* next;
ListNode(int v) : val(v), next(nullptr) {}
};
ListNode* head = new ListNode(1); // create a node, O(1)
ListNode* cur = head;
while (cur != nullptr) { cur = cur->next; } // traverse, O(n)
// insert newNode right after node p
newNode->next = p->next; // O(1)
p->next = newNode; // O(1)
// delete the node right after p
ListNode* tmp = p->next;
p->next = p->next->next; // O(1)
delete tmp;
// reverse a singly linked list, O(n)
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* next = curr->next; // save next before overwriting
curr->next = prev; // reverse the pointer
prev = curr; // advance prev
curr = next; // advance curr
}
head = prev;
# Python has no built-in linked-list type — interviews use a manual node.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
head = ListNode(1) # create a node, O(1)
cur = head
while cur: # traverse, O(n)
cur = cur.next
# insert new_node right after node p
new_node.next = p.next # O(1)
p.next = new_node # O(1)
# delete the node right after p
p.next = p.next.next # O(1)
# reverse a singly linked list, O(n)
prev, curr = None, head
while curr:
nxt = curr.next # save next before overwriting
curr.next = prev # reverse the pointer
prev = curr # advance prev
curr = nxt # advance curr
head = prev
# --- Built-in alternative for O(1) end operations ---
# collections.deque is doubly-linked-list-backed; use it when you
# need O(1) operations at both ends but don't need raw node pointers.
from collections import deque
dq = deque([1, 2, 3])
dq.appendleft(0) # O(1)
dq.pop() # O(1)
// TS/JS has no built-in linked-list type — interviews use a manual node class.
class ListNode<T> {
val: T;
next: ListNode<T> | null = null;
constructor(val: T) { this.val = val; }
}
let head: ListNode<number> | null = new ListNode(1); // create a node, O(1)
let cur: ListNode<number> | null = head;
while (cur !== null) { cur = cur.next; } // traverse, O(n)
// insert newNode right after node p
newNode.next = p.next; // O(1)
p.next = newNode; // O(1)
// delete the node right after p
p.next = p.next!.next; // O(1) (non-null assertion since p.next is known to exist)
// reverse a singly linked list, O(n)
let prev: ListNode<number> | null = null;
let curr: ListNode<number> | null = head;
while (curr !== null) {
const next: ListNode<number> | null = curr.next; // save next before overwriting
curr.next = prev; // reverse the pointer
prev = curr; // advance prev
curr = next; // advance curr
}
head = prev;
// Note: prefer a plain array unless you specifically need O(1)
// insert/delete at an arbitrary already-known node.
Brute force — copy to an array O(n) extra space
Dumping nodes into a list, manipulating, and rebuilding works but wastes O(n) memory; in-place pointer rewiring is O(1) space and is what interviewers want.
Template A · reverse a list O(n), O(1) space
head— the first node of the linked list.prev— the node that will becomecur's new "next"; starts as null since the new tail should point to nothing.cur— the node currently having its pointer flipped.next— a saved copy ofcur's original next node, taken beforecur.nextis overwritten so the rest of the list isn't lost.- Returns — the new head of the reversed list (the original list's last node).
ListNode reverse(ListNode head){
ListNode prev = null, cur = head;
while (cur != null){
ListNode next = cur.next; // save
cur.next = prev; // flip
prev = cur; cur = next; // advance
}
return prev;
}ListNode* reverse(ListNode* head){
ListNode* prev = nullptr, *cur = head;
while (cur != nullptr){
ListNode* next = cur->next; // save
cur->next = prev; // flip
prev = cur; cur = next; // advance
}
return prev;
}def reverse(head):
prev, cur = None, head
while cur is not None:
nxt = cur.next # save
cur.next = prev # flip
prev, cur = cur, nxt # advance
return prevfunction reverse(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null, cur = head;
while (cur !== null) {
const next = cur.next; // save
cur.next = prev; // flip
prev = cur; cur = next; // advance
}
return prev;
}Template B · reverse first k nodes (building block for reverse-in-groups)
head— the first node of the segment to reverse.k— how many nodes from the front to reverse.prev,cur,next— same roles as in plain reversal: the node built so far, the node being flipped, and its saved original successor.- Returns — the new head of the reversed
k-node segment; the originalhead(now the segment's tail) is relinked to whatever followed the segment.
ListNode reverseK(ListNode head, int k){
ListNode prev = null, cur = head;
while (k-- > 0 && cur != null){
ListNode next = cur.next;
cur.next = prev; prev = cur; cur = next;
}
head.next = cur; // old head now points to the remainder
return prev; // new head of this segment
}ListNode* reverseK(ListNode* head, int k){
ListNode* prev = nullptr, *cur = head;
while (k-- > 0 && cur != nullptr){
ListNode* next = cur->next;
cur->next = prev; prev = cur; cur = next;
}
head->next = cur; // old head now points to the remainder
return prev; // new head of this segment
}def reverse_k(head, k):
prev, cur = None, head
while k > 0 and cur is not None:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
k -= 1
head.next = cur # old head now points to the remainder
return prev # new head of this segmentfunction reverseK(head: ListNode, k: number): ListNode | null {
let prev: ListNode | null = null, cur: ListNode | null = head;
while (k-- > 0 && cur !== null) {
const next = cur.next;
cur.next = prev; prev = cur; cur = next;
}
head.next = cur; // old head now points to the remainder
return prev; // new head of this segment
}Template C · remove nth from end (dummy + gap pointers)
head— the first node of the list.n— how many nodes counted from the end should be removed (1 = the last node).dummy— a placeholder node placed beforeheadso the removal logic works even when the target is the very first real node.fast— pointer advancedn+1steps ahead ofslowto open a fixed-size gap.slow— pointer that ends up just before the node to remove oncefastreaches the end of the list.- Returns — the head of the list with the nth-from-the-end node removed.
ListNode removeNthFromEnd(ListNode head, int n){
ListNode dummy = new ListNode(0); dummy.next = head;
ListNode fast = dummy, slow = dummy;
for (int i = 0; i <= n; i++) fast = fast.next; // open a gap of n+1
while (fast != null){ fast = fast.next; slow = slow.next; }
slow.next = slow.next.next; // skip target
return dummy.next;
}ListNode* removeNthFromEnd(ListNode* head, int n){
ListNode* dummy = new ListNode(0); dummy->next = head;
ListNode* fast = dummy, *slow = dummy;
for (int i = 0; i <= n; i++) fast = fast->next; // open a gap of n+1
while (fast != nullptr){ fast = fast->next; slow = slow->next; }
slow->next = slow->next->next; // skip target
return dummy->next;
}def remove_nth_from_end(head, n):
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
for i in range(n + 1):
fast = fast.next # open a gap of n+1
while fast is not None:
fast = fast.next
slow = slow.next
slow.next = slow.next.next # skip target
return dummy.nextfunction removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
const dummy = new ListNode(0); dummy.next = head;
let fast: ListNode | null = dummy, slow: ListNode | null = dummy;
for (let i = 0; i <= n; i++) fast = fast!.next; // open a gap of n+1
while (fast !== null) { fast = fast.next; slow = slow!.next; }
slow!.next = slow!.next!.next; // skip target
return dummy.next;
}Worked example — reverse 1→2→3
| step | prev | cur | list so far |
|---|---|---|---|
| init | null | 1 | 1→2→3 |
| 1 | 1 | 2 | 1→null |
| 2 | 2 | 3 | 2→1→null |
| 3 | 3 | null | 3→2→1→null |
Return prev = 3. Saving next first is what keeps the tail reachable after each flip.
🔬 Deeper dive & dry run
Why it works. The invariant held before each step is "everything from the original head up to (but not including) curr has already been reversed and points backward, prev is the new head of that reversed segment, and curr still points forward into the untouched remainder." Each iteration peels one node off the untouched remainder, flips its pointer to point at prev, then advances both prev and curr by one — so the reversed-prefix invariant is preserved and grows by exactly one node per step.
When to reach for it. Any "reverse/rearrange a linked list in place" task — full reversal, reverse in groups of k, reverse a sublist [left,right], or detect/remove a cycle (Floyd's) — the shared tool is a small fixed set of pointers (prev/curr/next, or slow/fast) advanced with no extra data structure.
Complexity. O(n) time — one pass, one pointer reassignment per node; O(1) space — no recursion stack, no auxiliary list (recursive reversal trades this for O(n) call-stack space).
Pitfalls & variations. Saving next after (instead of before) reassigning curr.next loses the rest of the list irrecoverably. Returning curr instead of prev as the new head after the loop ends (curr is null at that point) is the classic off-by-one. Variants: reversing only a sublist requires stitching the reversed segment back between two saved boundary pointers; reversing in groups of k requires recursion or an explicit stack of segment boundaries.
Trace input: 1 → 2 → 3 → 4 → 5 → 6 — reverse the linked list in place, iteratively.
| step | prev | curr | next (saved) | action |
|---|---|---|---|---|
| 0 (init) | null | 1 | - | start |
| 1 | 1 | 2 | 2 | 1.next = null |
| 2 | 2 | 3 | 3 | 2.next = 1 |
| 3 | 3 | 4 | 4 | 3.next = 2 |
| 4 | 4 | 5 | 5 | 4.next = 3 |
| 5 | 5 | 6 | 6 | 5.next = 4 |
| 6 | 6 | null | null | 6.next = 5; loop ends (curr null) |
Result: New head is prev = 6, giving 6 → 5 → 4 → 3 → 2 → 1. Each row's "action" shows the reversed prefix growing by exactly one link, and the loop terminates precisely when curr falls off the end.
🧰 Quick reference templates
13. Two Pointers — Linked List
... Linked list YES ▸ Fast/slow or fixed-gap pointers YES
Two pointers moving at different speeds (or a fixed gap apart) solve cycle detection, finding the middle, and "nth from end" in one pass with O(1) memory.
boolean hasCycle(ListNode head) { // Floyd's tortoise & hare
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true; // pointers met inside a loop
}
return false;
}
ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; }
return slow; // fast at end ⇒ slow at middle
}bool hasCycle(ListNode* head) { // Floyd's tortoise & hare
ListNode *slow = head, *fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true; // pointers met inside a loop
}
return false;
}
ListNode* middleNode(ListNode* head) {
ListNode *slow = head, *fast = head;
while (fast != nullptr && fast->next != nullptr) { slow = slow->next; fast = fast->next->next; }
return slow; // fast at end ⇒ slow at middle
}def has_cycle(head): # Floyd's tortoise & hare
slow, fast = head, head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True # pointers met inside a loop
return False
def middle_node(head):
slow, fast = head, head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
return slow # fast at end ⇒ slow at middlefunction hasCycle(head: ListNode | null): boolean { // Floyd's tortoise & hare
let slow = head, fast = head;
while (fast !== null && fast.next !== null) {
slow = slow!.next;
fast = fast.next.next;
if (slow === fast) return true; // pointers met inside a loop
}
return false;
}
function middleNode(head: ListNode | null): ListNode | null {
let slow = head, fast = head;
while (fast !== null && fast.next !== null) { slow = slow!.next; fast = fast.next.next; }
return slow; // fast at end ⇒ slow at middle
}1→2→3→4→5. fast reaches end after slow takes 2 steps ⇒ middle = node 3. If 5→2 (cycle), slow/fast collide.14. Heap / Divide & Conquer — Merge k Sorted Lists
... Fast/slow NO ▸ Merge many sorted lists YES
Merging k lists: a min-heap of the current head of each list always yields the global minimum next. O(N log k) where N is total nodes. (Pairwise divide-and-conquer merging is the same complexity.)
ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
for (ListNode l : lists) if (l != null) pq.offer(l);
ListNode dummy = new ListNode(0), tail = dummy;
while (!pq.isEmpty()) {
ListNode node = pq.poll();
tail.next = node; tail = node;
if (node.next != null) pq.offer(node.next);
}
return dummy.next;
}ListNode* mergeKLists(vector<ListNode*>& lists) {
auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; };
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq(cmp);
for (ListNode* l : lists) if (l != nullptr) pq.push(l);
ListNode* dummy = new ListNode(0); ListNode* tail = dummy;
while (!pq.empty()) {
ListNode* node = pq.top(); pq.pop();
tail->next = node; tail = node;
if (node->next != nullptr) pq.push(node->next);
}
return dummy->next;
}import heapq
def merge_k_lists(lists):
pq = []
for i, l in enumerate(lists):
if l is not None:
heapq.heappush(pq, (l.val, i, l)) # index breaks val ties (nodes aren't comparable)
dummy = ListNode(0)
tail = dummy
while pq:
val, i, node = heapq.heappop(pq)
tail.next = node
tail = node
if node.next is not None:
heapq.heappush(pq, (node.next.val, i, node.next))
return dummy.nextfunction mergeKLists(lists: (ListNode | null)[]): ListNode | null {
const heap: ListNode[] = [];
const less = (a: ListNode, b: ListNode) => a.val < b.val;
const push = (node: ListNode) => {
heap.push(node);
let i = heap.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (!less(heap[i], heap[p])) break;
[heap[p], heap[i]] = [heap[i], heap[p]];
i = p;
}
};
const pop = (): ListNode => {
const top = heap[0];
const last = heap.pop()!;
if (heap.length > 0) {
heap[0] = last;
let i = 0;
while (true) {
const l = 2 * i + 1, r = 2 * i + 2;
let s = i;
if (l < heap.length && less(heap[l], heap[s])) s = l;
if (r < heap.length && less(heap[r], heap[s])) s = r;
if (s === i) break;
[heap[s], heap[i]] = [heap[i], heap[s]];
i = s;
}
}
return top;
};
for (const l of lists) if (l !== null) push(l);
const dummy = new ListNode(0);
let tail = dummy;
while (heap.length > 0) {
const node = pop();
tail.next = node;
tail = node;
if (node.next !== null) push(node.next);
}
return dummy.next;
}1→4→5, 1→3→4, 2→6. Heap pops 1,1,2,3,4,4,5,6 ⇒ fully merged sorted list.15. Linked List Manipulation
... Merge many sorted lists NO
Pointer surgery: reverse, reorder, swap pairs, remove nodes. Keep prev, save next before you overwrite a link, and use a dummy head when the head itself may change.
ListNode reverseList(ListNode head) {
ListNode prev = null, cur = head;
while (cur != null) {
ListNode next = cur.next; // save before we clobber it
cur.next = prev; // flip the pointer
prev = cur; // advance both
cur = next;
}
return prev; // new head
}ListNode* reverseList(ListNode* head) {
ListNode *prev = nullptr, *cur = head;
while (cur != nullptr) {
ListNode* next = cur->next; // save before we clobber it
cur->next = prev; // flip the pointer
prev = cur; // advance both
cur = next;
}
return prev; // new head
}def reverse_list(head):
prev, cur = None, head
while cur is not None:
nxt = cur.next # save before we clobber it
cur.next = prev # flip the pointer
prev = cur # advance both
cur = nxt
return prev # new headfunction reverseList(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null, cur = head;
while (cur !== null) {
const next: ListNode | null = cur.next; // save before we clobber it
cur.next = prev; // flip the pointer
prev = cur; // advance both
cur = next;
}
return prev; // new head
}1→2→3. Step 1: 1→null, prev=1. Step 2: 2→1. Step 3: 3→2. Returns 3→2→1.Hashing
🎯 LeetCode practice & worked example
6 · Linked List
Tests pointer manipulation without extra memory — the tell is any phrase like "in place", "O(1) extra space", or a structure that must be rewired node by node; reach for fast/slow pointers, dummy heads, and iterative reversal.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Reverse Linked List ★ | Easy | AmazonMicrosoftApple | iterative pointer flip |
| 2 | Merge Two Sorted Lists ★ | Easy | AmazonAppleBloomberg | dummy head merge |
| 3 | Reorder List ★ | Medium | AmazonMetaMicrosoft | find-middle + reverse + interleave |
| 4 | Remove Nth Node From End of List ★ | Medium | AmazonMicrosoftAdobe | fast/slow offset by n |
| 5 | Copy List with Random Pointer ★ | Medium | AmazonMetaBloomberg | hashmap old->new node clone |
| 6 | Add Two Numbers ★ | Medium | AmazonMicrosoftAppleBloomberg | carry propagation, dummy head |
| 7 | Linked List Cycle ★ | Easy | AmazonMicrosoftApple | Floyd's tortoise & hare |
| 8 | Find the Duplicate Number | Medium | AmazonGoogleUber | array-as-linked-list cycle detection |
| 9 | LRU Cache ★ | Medium | AmazonGoogleMetaMicrosoft | doubly linked list + hashmap |
| 10 | Merge k Sorted Lists ★ | Hard | AmazonGoogleMicrosoft | min-heap of list heads / divide & conquer merge |
| 11 | Reverse Nodes in k-Group | Hard | AmazonMicrosoftByteDance | segment reversal with group boundaries |
Worked example — Reorder List (LC 143, Medium)
Frequently asked at: Amazon, Meta, Microsoft.
Problem. Given the head of a singly linked list L0 → L1 → ... → Ln-1 → Ln, reorder it in place to L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → ... You may not modify node values, only rewire next pointers, and you must use O(1) extra space.
Idea. Three classic sub-routines chained together: (1) locate the midpoint with slow/fast pointers, (2) reverse the second half in place, (3) splice the two halves together by alternating one node from each side. Each step is a well-known O(n) primitive, so the composition stays O(n) time, O(1) space.
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null) return;
// 1. find the middle (slow ends on the last node of the first half)
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. split and reverse the second half
ListNode second = slow.next;
slow.next = null;
ListNode prev = null;
while (second != null) {
ListNode next = second.next;
second.next = prev;
prev = second;
second = next;
}
// 3. merge the two halves alternately
ListNode first = head, secondHalf = prev;
while (secondHalf != null) {
ListNode tmp1 = first.next;
ListNode tmp2 = secondHalf.next;
first.next = secondHalf;
secondHalf.next = tmp1;
first = tmp1;
secondHalf = tmp2;
}
}
}
struct ListNode {
int val;
ListNode *next;
ListNode(int val) : val(val), next(nullptr) {}
};
class Solution {
public:
void reorderList(ListNode* head) {
if (head == nullptr || head->next == nullptr) return;
// 1. find the middle (slow ends on the last node of the first half)
ListNode *slow = head, *fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
// 2. split and reverse the second half
ListNode* second = slow->next;
slow->next = nullptr;
ListNode* prev = nullptr;
while (second != nullptr) {
ListNode* next = second->next;
second->next = prev;
prev = second;
second = next;
}
// 3. merge the two halves alternately
ListNode *first = head, *secondHalf = prev;
while (secondHalf != nullptr) {
ListNode* tmp1 = first->next;
ListNode* tmp2 = secondHalf->next;
first->next = secondHalf;
secondHalf->next = tmp1;
first = tmp1;
secondHalf = tmp2;
}
}
};class ListNode:
def __init__(self, val=0):
self.val = val
self.next = None
class Solution:
def reorderList(self, head: ListNode) -> None:
if head is None or head.next is None:
return
# 1. find the middle (slow ends on the last node of the first half)
slow, fast = head, head
while fast.next is not None and fast.next.next is not None:
slow = slow.next
fast = fast.next.next
# 2. split and reverse the second half
second = slow.next
slow.next = None
prev = None
while second is not None:
next = second.next
second.next = prev
prev = second
second = next
# 3. merge the two halves alternately
first, secondHalf = head, prev
while secondHalf is not None:
tmp1 = first.next
tmp2 = secondHalf.next
first.next = secondHalf
secondHalf.next = tmp1
first = tmp1
secondHalf = tmp2class ListNode {
val: number;
next: ListNode | null;
constructor(val: number) {
this.val = val;
this.next = null;
}
}
class Solution {
reorderList(head: ListNode | null): void {
if (head === null || head.next === null) return;
// 1. find the middle (slow ends on the last node of the first half)
let slow: ListNode | null = head, fast: ListNode | null = head;
while (fast!.next !== null && fast!.next!.next !== null) {
slow = slow!.next;
fast = fast!.next!.next;
}
// 2. split and reverse the second half
let second: ListNode | null = slow!.next;
slow!.next = null;
let prev: ListNode | null = null;
while (second !== null) {
const next: ListNode | null = second.next;
second.next = prev;
prev = second;
second = next;
}
// 3. merge the two halves alternately
let first: ListNode | null = head, secondHalf: ListNode | null = prev;
while (secondHalf !== null) {
const tmp1: ListNode | null = first!.next;
const tmp2: ListNode | null = secondHalf.next;
first!.next = secondHalf;
secondHalf.next = tmp1;
first = tmp1;
secondHalf = tmp2;
}
}
}Complexity. Time O(n) Space O(1) — a constant number of single passes over the list, all pointer rewiring with no auxiliary structures.
Walkthrough. Input: 1 → 2 → 3 → 4 → 5.
| Step | State |
|---|---|
| Find middle | slow lands on 3, so first half = 1→2→3, second = 4→5 |
| Reverse second half | 4→5 becomes 5→4 |
| Merge iteration 1 | first=1, secondHalf=5 → 1→5, first advances to 2 |
| Merge iteration 2 | first=2, secondHalf=4 → 2→4, first advances to 3 |
| secondHalf exhausted | loop stops; 3's original next (null) already terminates the list |
| Result | 1 → 5 → 2 → 4 → 3 |
🔬 Deeper dive & dry run
Why it works. Reordering L0→Ln→L1→Ln-1→… decomposes into three well-known O(n) linked-list primitives — find the middle with slow/fast pointers, reverse the second half with iterative prev/cur rewiring, then merge the two halves by alternating one node from each — all done via pointer surgery with no auxiliary array.
When to reach for it. Any problem manipulating a singly linked list's structure based on position or symmetry (reorder, palindrome check, k-group reversal): fast/slow pointers locate the middle, prev/cur/next reverses a segment, and a two-pointer interleave merges two lists.
Complexity. O(n) time across the three linear passes; O(1) extra space — everything is in-place pointer rewiring, no recursion stack or array copy.
Pitfalls & variations. Forgetting to cut slow.next = None before reversing the second half creates a cycle. Odd vs. even length changes which node slow lands on and thus which half is longer. Overwriting .next during reversal before saving the next pointer loses the rest of the list.
Trace input: 1→2→3→4→5 — reorder in place to L0,Ln,L1,Ln-1,…
| Step | Phase | Pointers / state | List snapshot | Note |
|---|---|---|---|---|
| 0 | start | slow=1, fast=1 | 1-2-3-4-5 | n=5 (odd) |
| 1 | find-middle | slow=2, fast=3 | 1-2-3-4-5 | advance slow ×1, fast ×2 |
| 2 | find-middle | slow=3, fast=5 | 1-2-3-4-5 | fast.next.next is null → stop; middle = 3 |
| 3 | split | first=1-2-3, second=4-5 | (cut) | set slow.next = None |
| 4 | reverse 2nd half | prev=None→4, cur=4→5 | second: 4-5 → 5,4 (partial) | reversed so far: 4 |
| 5 | reverse 2nd half | prev=4→5, cur=5→None | second fully reversed: 5-4 | reversal done |
| 6 | merge | p1=1, p2=5 | 1-5 | attach 1.next=5; advance both |
| 7 | merge | p1=2, p2=4 | 1-5-2-4 | attach 2.next=4; advance both |
| 8 | merge | p1=3, p2=None | 1-5-2-4-3 | p2 exhausted; 3 becomes the tail |
Result: Final list 1→5→2→4→3. Because n is odd, the second half (4-5) is one node shorter than the first, so after merging, the leftover node 3 naturally becomes the tail with no special-casing needed.
21 · Hashing / Counting
Flowchart leaf: fast lookup, counting, grouping
The idea
A hash map gives O(1) average membership, frequency, and "have I seen the complement?" — turning many O(n²) scans into O(n). Reach for it for two-sum-style complements, frequency counts, grouping (anagrams), and de-duplication.
🧰 API — Hash map & hash set methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
Map<String, Integer> m = new HashMap<>(); // declare/create map
Set<String> s = new HashSet<>(); // declare/create set
// ----- map: insert / update -----
m.put(k, v); // insert/overwrite key k -> value v, O(1) avg
m.putIfAbsent(k, v); // insert only if key absent, O(1) avg
m.merge(k, 1, Integer::sum); // combine with existing value (e.g. counting), O(1) avg
// ----- map: access -----
m.get(k); // value for k, or null if absent, O(1) avg
m.getOrDefault(k, d); // value for k, or d if absent, O(1) avg
m.containsKey(k); // true if key present, O(1) avg
m.size(); // number of entries, O(1)
// ----- map: remove -----
m.remove(k); // delete key k, O(1) avg
m.clear(); // remove all entries, O(n)
// ----- map: iterate -----
for (var e : m.entrySet()) { } // iterate key-value pairs, O(n)
for (String key : m.keySet()) { } // iterate keys, O(n)
for (Integer val : m.values()) { } // iterate values, O(n)
// ----- set -----
s.add(x); // insert x, O(1) avg
s.contains(x); // membership test, O(1) avg
s.remove(x); // delete x, O(1) avg
s.size(); // number of elements, O(1)
unordered_map<string,int> m; // declare/create map
unordered_set<string> s; // declare/create set
// ----- map: insert / update -----
m[k] = v; // insert/overwrite key k -> value v, O(1) avg (default-constructs if absent)
m.insert({k, v}); // insert only if key absent, O(1) avg
m[k]++; // increment count, auto-creates with 0, O(1) avg
// ----- map: access -----
m[k]; // value for k, CREATES entry with default if absent, O(1) avg
m.at(k); // value for k, throws if absent, O(1) avg
m.count(k); // membership test (0/1), O(1) avg
m.find(k); // iterator to entry, or end() if absent, O(1) avg
m.size(); // number of entries, O(1)
// ----- map: remove -----
m.erase(k); // delete key k, O(1) avg
m.clear(); // remove all entries, O(n)
// ----- map: iterate -----
for (auto& [key, val] : m) { } // iterate key-value pairs (unspecified order), O(n)
// ----- set -----
s.insert(x); // insert x, O(1) avg
s.count(x); // membership test (0/1), O(1) avg
s.erase(x); // delete x, O(1) avg
s.size(); // number of elements, O(1)
d = {} # declare/create dict
s = set() # declare/create set
# ----- dict: insert / update -----
d[k] = v # insert/overwrite key k -> value v, O(1) avg
d.setdefault(k, v) # insert only if key absent, returns value, O(1) avg
# ----- dict: access -----
d[k] # value for k, raises KeyError if absent, O(1) avg
d.get(k, default) # value for k, or default if absent, O(1) avg
k in d # membership test, O(1) avg
len(d) # number of entries, O(1)
# ----- dict: remove -----
del d[k] # delete key k, O(1) avg
d.pop(k, default) # remove & return value for k (or default), O(1) avg
d.clear() # remove all entries, O(n)
# ----- dict: iterate -----
for key in d: ... # iterate keys, insertion order, O(n)
for key, val in d.items(): ... # iterate key-value pairs, O(n)
for val in d.values(): ... # iterate values, O(n)
# ----- set -----
s.add(x) # insert x, O(1) avg
x in s # membership test, O(1) avg
s.discard(x) # remove x if present, no error if absent, O(1) avg
s.remove(x) # remove x, raises KeyError if absent, O(1) avg
len(s) # number of elements, O(1)
const m = new Map<string, number>(); // declare/create map
const s = new Set<string>(); // declare/create set
// ----- map: insert / update -----
m.set(k, v); // insert/overwrite key k -> value v, O(1) avg
m.set(k, (m.get(k) ?? 0) + 1); // common "increment count" idiom, O(1) avg
// ----- map: access -----
m.get(k); // value for k, or undefined if absent, O(1) avg
m.has(k); // membership test, O(1) avg
m.size; // number of entries, O(1)
// ----- map: remove -----
m.delete(k); // delete key k, returns boolean, O(1) avg
m.clear(); // remove all entries, O(n)
// ----- map: iterate -----
for (const [key, val] of m) { } // iterate key-value pairs, insertion order, O(n)
for (const key of m.keys()) { } // iterate keys, O(n)
for (const val of m.values()) { } // iterate values, O(n)
// ----- set -----
s.add(x); // insert x, O(1) avg
s.has(x); // membership test, O(1) avg
s.delete(x); // remove x, returns boolean, O(1) avg
s.size; // number of elements, O(1)
Brute force — nested search O(n²)
For each element, scan the rest to find a match. The map remembers what you've seen so the match is an O(1) lookup.
Template A · complement lookup — two sum O(n)
a— the input array of numbers.target— the sum the two chosen numbers must add up to.seen— map from a value already scanned to its index, so a matching partner can be looked up in O(1).need— the complement value (target - a[i]) that would pair with the current element to hittarget.i— index of the element currently being checked.- Returns — the indices of the two numbers in
athat add up totarget.
int[] twoSum(int[] a, int target){
Map<Integer,Integer> seen = new HashMap<>();
for (int i = 0; i < a.length; i++){
int need = target - a[i];
if (seen.containsKey(need)) return new int[]{seen.get(need), i};
seen.put(a[i], i);
}
return new int[0];
}std::vector<int> twoSum(std::vector<int>& a, int target){
std::unordered_map<int,int> seen;
for (int i = 0; i < (int)a.size(); i++){
int need = target - a[i];
auto it = seen.find(need);
if (it != seen.end()) return {it->second, i};
seen[a[i]] = i;
}
return {};
}def two_sum(a, target):
seen = {}
for i, x in enumerate(a):
need = target - x
if need in seen:
return [seen[need], i]
seen[x] = i
return []function twoSum(a: number[], target: number): number[] {
const seen = new Map<number, number>();
for (let i = 0; i < a.length; i++) {
const need = target - a[i];
if (seen.has(need)) return [seen.get(need)!, i];
seen.set(a[i], i);
}
return [];
}Template B · grouping — anagrams by sorted-key
strs— the input list of words.groups— map from a canonical key (the word's letters sorted) to the list of original words sharing that key.c— the sorted-character version of the current word, used as the grouping key since every anagram of a word shares the same sorted form.s— the current word being placed into a group.- Returns — the input words grouped into lists, one list per set of anagrams.
List<List<String>> groupAnagrams(String[] strs){
Map<String,List<String>> groups = new HashMap<>();
for (String s : strs){
char[] c = s.toCharArray(); Arrays.sort(c);
groups.computeIfAbsent(new String(c), k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}std::vector<std::vector<std::string>> groupAnagrams(std::vector<std::string>& strs){
std::unordered_map<std::string, std::vector<std::string>> groups;
for (const std::string& s : strs){
std::string c = s;
std::sort(c.begin(), c.end());
groups[c].push_back(s);
}
std::vector<std::vector<std::string>> res;
for (auto& [k, v] : groups) res.push_back(v);
return res;
}def group_anagrams(strs):
groups = {}
for s in strs:
c = "".join(sorted(s))
groups.setdefault(c, []).append(s)
return list(groups.values())function groupAnagrams(strs: string[]): string[][] {
const groups = new Map<string, string[]>();
for (const s of strs) {
const c = s.split("").sort().join("");
if (!groups.has(c)) groups.set(c, []);
groups.get(c)!.push(s);
}
return Array.from(groups.values());
}Template C · frequency count
a— the input collection of values to tally.freq— map from a value to how many times it has appeared so far.x— the current value being counted.- Returns — a map/counter giving the occurrence count of each distinct value in
a.
Map<Integer,Integer> freq = new HashMap<>();
for (int x : a) freq.merge(x, 1, Integer::sum); // idiomatic counterstd::unordered_map<int,int> freq;
for (int x : a) freq[x]++; // idiomatic counterfreq = Counter(a) # idiomatic counterconst freq = new Map<number, number>();
for (const x of a) freq.set(x, (freq.get(x) ?? 0) + 1); // idiomatic counterWorked example — two sum, target 9, [2,7,11,15]
| i | a[i] | need | need in map? | map after |
|---|---|---|---|---|
| 0 | 2 | 7 | no | {2:0} |
| 1 | 7 | 2 | yes → [0,1] | — |
Answer [0,1] in one pass; the map converts "does a partner exist earlier?" into an O(1) check.
🔬 Deeper dive & dry run
Why it works. A hashmap turns "have I seen a value that completes this condition?" into an O(1) average lookup, letting one pass replace nested loops. The invariant is: at index i, the map contains exactly the elements from indices 0..i-1, so a hit reflects a valid, earlier partner — never the current element itself.
When to reach for it. Phrases like "find two numbers that sum to", "count subarrays with sum k", "first repeating element", "longest run with at most k distinct" or "group items by a derived key" — anything trading O(n²) pairwise comparison for O(n) with O(1) membership/frequency checks.
Complexity. Time O(n): one pass, O(1) average hashmap ops. Space O(n) worst case: every element gets its own map entry.
Pitfalls & variations. Checking the map before inserting the current element (otherwise an element can pair with itself); storing only presence when the answer needs the index/count; when the key domain is small and bounded (26 letters, digits 0–9), a fixed-size array is faster than a hashmap and avoids collision overhead.
Trace input: nums = [2, 11, 3, 15, 7, 4], target = 9 — find the indices of two numbers that sum to the target.
| Step | i | num | target−num | seen (before) | Hit? | Action |
|---|---|---|---|---|---|---|
| 0 | 0 | 2 | 7 | {} | no | insert 2→0 |
| 1 | 1 | 11 | -2 | {2:0} | no | insert 11→1 |
| 2 | 2 | 3 | 6 | {2:0, 11:1} | no | insert 3→2 |
| 3 | 3 | 15 | -6 | {2:0, 11:1, 3:2} | no | insert 15→3 |
| 4 | 4 | 7 | 2 | {2:0, 11:1, 3:2, 15:3} | yes (2 at idx 0) | return [0, 4] |
Result: indices [0, 4] (values 2 and 7) sum to 9. The trace shows the lookup always happens before the insert, so an element can never be matched with itself.
🧰 Quick reference templates
16. Hash Table / Counting
... Linked list NO ▸ Fast lookup, counting, or grouping YES
O(1) membership, frequency, or "have I seen the complement?" One pass with a map turns many O(n²) scans into O(n).
int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>(); // value → index
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
if (seen.containsKey(need)) return new int[]{seen.get(need), i};
seen.put(nums[i], i);
}
return new int[0];
}vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> seen; // value → index
for (int i = 0; i < (int)nums.size(); i++) {
int need = target - nums[i];
auto it = seen.find(need);
if (it != seen.end()) return {it->second, i};
seen[nums[i]] = i;
}
return {};
}def two_sum(nums, target):
seen = {} # value → index
for i, x in enumerate(nums):
need = target - x
if need in seen:
return [seen[need], i]
seen[x] = i
return []function twoSum(nums: number[], target: number): number[] {
const seen = new Map<number, number>(); // value → index
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if (seen.has(need)) return [seen.get(need)!, i];
seen.set(nums[i], i);
}
return [];
}[2,7,11,15], target 9. i=0 store 2. i=1 need 2 found ⇒ indices [0,1].Intervals & Array Partitioning
32. Brute Force / Backtracking — Counting
... Count number of ways YES ▸ Brute force enough YES
Count configurations by generating them with pruning (N-Queens, valid placements). Bit tricks make the "is this square attacked?" check O(1).
int totalNQueens(int n) { return solve(n, 0, 0, 0, 0); }
int solve(int n, int row, int cols, int diag, int anti) {
if (row == n) return 1; // a full board
int count = 0;
int avail = ((1 << n) - 1) & ~(cols | diag | anti); // free columns as bits
while (avail != 0) {
int bit = avail & (-avail); // lowest free column
avail -= bit;
count += solve(n, row + 1, cols | bit,
(diag | bit) << 1, (anti | bit) >> 1);
}
return count;
}int totalNQueens(int n) { return solve(n, 0, 0, 0, 0); }
int solve(int n, int row, int cols, int diag, int anti) {
if (row == n) return 1; // a full board
int count = 0;
int avail = ((1 << n) - 1) & ~(cols | diag | anti); // free columns as bits
while (avail != 0) {
int bit = avail & (-avail); // lowest free column
avail -= bit;
count += solve(n, row + 1, cols | bit,
(diag | bit) << 1, (anti | bit) >> 1);
}
return count;
}def total_n_queens(n):
return solve(n, 0, 0, 0, 0)
def solve(n, row, cols, diag, anti):
if row == n:
return 1 # a full board
count = 0
avail = ((1 << n) - 1) & ~(cols | diag | anti) # free columns as bits
while avail != 0:
bit = avail & (-avail) # lowest free column
avail -= bit
count += solve(n, row + 1, cols | bit,
(diag | bit) << 1, (anti | bit) >> 1)
return countfunction totalNQueens(n: number): number { return solve(n, 0, 0, 0, 0); }
function solve(n: number, row: number, cols: number, diag: number, anti: number): number {
if (row === n) return 1; // a full board
let count = 0;
let avail = ((1 << n) - 1) & ~(cols | diag | anti); // free columns as bits
while (avail !== 0) {
const bit = avail & (-avail); // lowest free column
avail -= bit;
count += solve(n, row + 1, cols | bit,
(diag | bit) << 1, (anti | bit) >> 1);
}
return count;
}n=4 ⇒ 2 distinct solutions. n=8 ⇒ 92. Diagonal masks shift each row.🎯 LeetCode practice & worked example
1 · Arrays & Hashing
Tests whether you reach for O(1) lookup structures (hash maps/sets) to trade space for time whenever you see "duplicate", "pair that sums to", "grouped by", or "frequency of" in a problem statement.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Contains Duplicate | Easy | AmazonGoogleApple | hash set membership |
| 2 | Valid Anagram | Easy | AmazonBloombergMicrosoft | char frequency count |
| 3 | Two Sum ★ | Easy | AmazonGoogleAdobe | hash map complement |
| 4 | Group Anagrams ★ | Medium | AmazonMetaUber | hash map of sorted-key buckets |
| 5 | Top K Frequent Elements ★ | Medium | AmazonMetaGoogle | bucket sort by frequency |
| 6 | Encode and Decode Strings | Medium | GoogleMetaTikTok | length-prefix delimiter encoding |
| 7 | Product of Array Except Self ★ | Medium | AmazonMicrosoftApple | prefix/suffix product arrays |
| 8 | Valid Sudoku | Medium | AmazonAppleUber | hash sets per row/col/box |
| 9 | Longest Consecutive Sequence ★ | Medium | AmazonGoogleMeta | hash set + sequence expansion |
Worked example — Group Anagrams (LC 49, Medium)
Frequently asked at: Amazon, Meta, Uber, Bloomberg
Problem. Given an array of strings, group the strings that are anagrams of one another, returning the groups in any order.
Idea. Two strings are anagrams iff they share the same multiset of characters, so sorting each string's characters yields a canonical key. Bucket every string into a hash map under that key; the map's values are the answer groups.
🧰 API — Dynamic array / list methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
List<Integer> list = new ArrayList<>(); // declare/create
// ----- insert -----
list.add(x); // append x to end, O(1) amortized
list.add(i, x); // insert x at index i, O(n)
list.set(i, x); // overwrite element at index i, O(1)
// ----- access -----
list.get(i); // element at index i, O(1)
list.size(); // number of elements, O(1)
list.isEmpty(); // true if size == 0, O(1)
// ----- remove -----
list.remove(i); // remove element at index i (int overload), O(n)
list.remove(Integer.valueOf(x)); // remove first occurrence of value x, O(n)
list.clear(); // remove all elements, O(n)
// ----- search -----
list.contains(x); // true if x present, O(n)
list.indexOf(x); // first index of x, or -1, O(n)
// ----- iterate / sort -----
for (int v : list) { } // forward iteration, O(n)
Collections.sort(list); // sort ascending, O(n log n)
Collections.sort(list, Collections.reverseOrder()); // sort descending
vector<int> v; // declare/create
vector<int> v(n); // create with n zero-initialized elements
// ----- insert -----
v.push_back(x); // append x to end, O(1) amortized
v.emplace_back(x); // construct-in-place append, O(1) amortized
v.insert(v.begin() + i, x); // insert x at index i, O(n)
// ----- access -----
v[i]; // element at index i, no bounds check, O(1)
v.at(i); // element at index i, bounds-checked (throws), O(1)
v.front(); v.back(); // first / last element, O(1)
v.size(); // number of elements, O(1)
v.empty(); // true if size == 0, O(1)
// ----- remove -----
v.pop_back(); // remove last element, O(1)
v.erase(v.begin() + i); // remove element at index i, O(n)
v.clear(); // remove all elements, O(n)
// ----- search / sort -----
find(v.begin(), v.end(), x); // iterator to first match or end(), O(n)
sort(v.begin(), v.end()); // sort ascending, O(n log n)
sort(v.begin(), v.end(), greater<int>()); // sort descending
lst = [] # declare/create
# ----- insert -----
lst.append(x) # append x to end, O(1) amortized
lst.insert(i, x) # insert x at index i, O(n)
lst[i] = x # overwrite element at index i, O(1)
# ----- access -----
lst[i] # element at index i, O(1)
lst[-1] # last element, O(1)
len(lst) # number of elements, O(1)
# ----- remove -----
lst.pop() # remove & return last element, O(1)
lst.pop(i) # remove & return element at index i, O(n)
lst.remove(x) # remove first occurrence of value x, O(n)
lst.clear() # remove all elements, O(n)
# ----- search -----
x in lst # membership test, O(n)
lst.index(x) # first index of x, raises ValueError if absent, O(n)
# ----- iterate / sort -----
for v in lst: ... # forward iteration, O(n)
lst.sort() # sort ascending in place, O(n log n)
lst.sort(reverse=True) # sort descending in place, O(n log n)
sorted(lst) # return a new sorted list, O(n log n)
const arr: number[] = []; // declare/create (equivalently Array<number>)
// ----- insert -----
arr.push(x); // append x to end, O(1) amortized
arr.unshift(x); // prepend x to front, O(n)
arr.splice(i, 0, x); // insert x at index i, O(n)
// ----- access -----
arr[i]; // element at index i, O(1)
arr.length; // number of elements, O(1)
arr.at(-1); // last element via negative index, O(1)
// ----- remove -----
arr.pop(); // remove & return last element, O(1)
arr.shift(); // remove & return first element, O(n)
arr.splice(i, 1); // remove element at index i, O(n)
// ----- search -----
arr.includes(x); // membership test, O(n)
arr.indexOf(x); // first index of x, or -1, O(n)
// ----- iterate / sort -----
for (const v of arr) { } // forward iteration, O(n)
arr.sort((a, b) => a - b); // sort ascending in place, O(n log n)
arr.sort((a, b) => b - a); // sort descending in place, O(n log n)
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> groups = new HashMap<>();
for (String s : strs) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
String key = new String(chars);
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}
}class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> groups;
for (const string& s : strs) {
string key = s;
sort(key.begin(), key.end());
groups[key].push_back(s);
}
vector<vector<string>> result;
for (auto& [key, group] : groups) {
result.push_back(group);
}
return result;
}
};class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
groups = {}
for s in strs:
key = "".join(sorted(s))
groups.setdefault(key, []).append(s)
return list(groups.values())class Solution {
groupAnagrams(strs: string[]): string[][] {
const groups = new Map<string, string[]>();
for (const s of strs) {
const key = s.split("").sort().join("");
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(s);
}
return Array.from(groups.values());
}
}Complexity. Time O(n * k log k) Space O(n * k) — n strings each sorted in O(k log k) for length k, plus map storage for every character.
Walkthrough. Input ["eat","tea","tan","ate","nat","bat"]:
| string | sorted key | bucket after insert |
|---|---|---|
| eat | aet | aet -> [eat] |
| tea | aet | aet -> [eat, tea] |
| tan | ant | ant -> [tan] |
| ate | aet | aet -> [eat, tea, ate] |
| nat | ant | ant -> [tan, nat] |
| bat | abt | abt -> [bat] |
Final groups: [eat, tea, ate], [tan, nat], [bat].
🔬 Deeper dive & dry run
Why it works. Anagrams share a canonical form — their letters sorted, or a fixed-length letter-count signature — so hashing every string by that canonical key groups every permutation into the same bucket. The invariant enforced is "two strings land in the same bucket iff they're anagrams," which sorting (or counting) guarantees exactly.
When to reach for it. "Group items by a derived signature" problems — anagrams, isomorphic strings, bucketing by digit sum or frequency profile — whenever equality after some transformation defines the equivalence classes you need.
Complexity. Time O(n·k log k) with sort-based keys (n strings, average length k), or O(n·k) with a fixed-alphabet count-tuple key. Space O(n·k) to store all strings across the map's buckets.
Pitfalls & variations. Using the raw string as the key misses anagrams entirely; sorting each string is simple but O(k log k) — a 26-length count array as the key is O(k) and faster for pure-lowercase inputs; building a string-encoded count key (e.g. "1a2b0c…") works but is easy to get delimiter bugs on vs. just using a tuple/array directly as the key.
Trace input: strs = ["eat","tea","tan","ate","nat","bat"] — group all anagrams together (the classic LC49 example).
| Step | word | sorted key | map state after insert |
|---|---|---|---|
| 1 | eat | aet | {aet:[eat]} |
| 2 | tea | aet | {aet:[eat, tea]} |
| 3 | tan | ant | {aet:[eat, tea], ant:[tan]} |
| 4 | ate | aet | {aet:[eat, tea, ate], ant:[tan]} |
| 5 | nat | ant | {aet:[eat, tea, ate], ant:[tan, nat]} |
| 6 | bat | abt | {aet:[eat, tea, ate], ant:[tan, nat], abt:[bat]} |
Result: [["eat","tea","ate"], ["tan","nat"], ["bat"]] — matches LeetCode's own example. The sorted key is computed once per word and is all the hashmap ever sees; the original word is just the payload riding along in the bucket.
22 · Stack (Parsing)
Flowchart leaf: parse symbols → stack
The idea
A stack mirrors nesting: push on an opener, pop on the matching closer. It handles balanced brackets, expression evaluation (RPN / shunting-yard), and any "most recent unmatched thing" logic. Postfix (RPN) evaluation is a stack of operands; each operator pops two and pushes the result.
🧰 API — Stack (LIFO) methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
Deque<Integer> st = new ArrayDeque<>();
st.push(x); // add on top, O(1)
st.pop(); // remove & return top, throws if empty, O(1)
st.peek(); // look at top without removing, null if empty, O(1)
st.isEmpty(); // check empty, O(1)
st.size(); // number of elements, O(1)
for (int v : st) {} // iterate top-to-bottom, O(n)
#include <stack>
std::stack<int> st;
st.push(x); // add on top, O(1)
st.pop(); // remove top (no return value), O(1)
st.top(); // reference to top element, O(1)
st.empty(); // check empty, O(1)
st.size(); // number of elements, O(1)
// std::stack is a container adapter — not directly iterable.
st = []
st.append(x) # push on top, O(1) amortized
st.pop() # remove & return top, O(1)
st[-1] # peek top, O(1)
len(st) # size, O(1)
not st # check empty (True if empty)
for v in reversed(st): ... # iterate top-to-bottom, O(n)
const st: number[] = [];
st.push(x); // add on top, O(1) amortized
st.pop(); // remove & return top, O(1)
st[st.length - 1]; // peek top, O(1)
st.length === 0; // check empty, O(1)
st.length; // size, O(1)
for (const v of [...st].reverse()) {} // iterate top-to-bottom, O(n)
Brute force — repeatedly remove matched pairs O(n²)
Scanning the string over and over deleting "()" pairs until stable. The stack resolves each closer in O(1) against the top, one pass.
Template A · balanced brackets O(n)
s— the input string of brackets to check.st— a stack holding the opening brackets seen so far that haven't been closed yet.match— a lookup from each closing bracket to the opening bracket it must pair with.c— the current character being scanned froms.- Returns —
trueif every bracket is properly opened and closed in the right order, elsefalse.
boolean isValid(String s){
Deque<Character> st = new ArrayDeque<>();
Map<Character,Character> match = Map.of(')','(', ']','[', '}','{');
for (char c : s.toCharArray()){
if (c=='('||c=='['||c=='{') st.push(c);
else if (st.isEmpty() || st.pop() != match.get(c)) return false;
}
return st.isEmpty();
}bool isValid(const std::string& s){
std::vector<char> st;
std::unordered_map<char,char> match = {{')','('}, {']','['}, {'}','{'}};
for (char c : s){
if (c=='('||c=='['||c=='{') st.push_back(c);
else {
if (st.empty() || st.back() != match[c]) return false;
st.pop_back();
}
}
return st.empty();
}def is_valid(s):
st = []
match = {')': '(', ']': '[', '}': '{'}
for c in s:
if c in '([{':
st.append(c)
elif not st or st.pop() != match[c]:
return False
return not stfunction isValid(s: string): boolean {
const st: string[] = [];
const match: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
for (const c of s) {
if (c === '(' || c === '[' || c === '{') st.push(c);
else if (st.length === 0 || st.pop() !== match[c]) return false;
}
return st.length === 0;
}Template B · evaluate reverse Polish notation
tokens— the expression written in Reverse Polish Notation, one number or operator per entry.st— a stack holding operands waiting to be combined.t— the current token being processed.a,b— the two most recently pushed operands popped for a binary operator, withbas the right-hand operand andaas the left-hand one.- Returns — the single number left on the stack once every token has been consumed.
int evalRPN(String[] tokens){
Deque<Integer> st = new ArrayDeque<>();
for (String t : tokens){
switch (t){
case "+": st.push(st.pop() + st.pop()); break;
case "*": st.push(st.pop() * st.pop()); break;
case "-": { int b=st.pop(), a=st.pop(); st.push(a-b); } break;
case "/": { int b=st.pop(), a=st.pop(); st.push(a/b); } break;
default: st.push(Integer.parseInt(t));
}
}
return st.pop();
}int evalRPN(std::vector<std::string>& tokens){
std::vector<int> st;
for (const std::string& t : tokens){
if (t == "+") { int b=st.back(); st.pop_back(); int a=st.back(); st.pop_back(); st.push_back(a+b); }
else if (t == "*") { int b=st.back(); st.pop_back(); int a=st.back(); st.pop_back(); st.push_back(a*b); }
else if (t == "-") { int b=st.back(); st.pop_back(); int a=st.back(); st.pop_back(); st.push_back(a-b); }
else if (t == "/") { int b=st.back(); st.pop_back(); int a=st.back(); st.pop_back(); st.push_back(a/b); }
else st.push_back(std::stoi(t));
}
return st.back();
}def eval_rpn(tokens):
st = []
for t in tokens:
if t == "+":
st.append(st.pop() + st.pop())
elif t == "*":
st.append(st.pop() * st.pop())
elif t == "-":
b, a = st.pop(), st.pop()
st.append(a - b)
elif t == "/":
b, a = st.pop(), st.pop()
st.append(int(a / b))
else:
st.append(int(t))
return st.pop()function evalRPN(tokens: string[]): number {
const st: number[] = [];
for (const t of tokens) {
switch (t) {
case "+": st.push(st.pop()! + st.pop()!); break;
case "*": st.push(st.pop()! * st.pop()!); break;
case "-": { const b = st.pop()!, a = st.pop()!; st.push(a - b); break; }
case "/": { const b = st.pop()!, a = st.pop()!; st.push(Math.trunc(a / b)); break; }
default: st.push(parseInt(t, 10));
}
}
return st.pop()!;
}Worked example — "([{}])"
| char | action | stack |
|---|---|---|
| ( | push | ( |
| [ | push | ( [ |
| { | push | ( [ { |
| } | pop { == match | ( [ |
| ] | pop [ == match | ( |
| ) | pop ( == match | (empty) |
Ends empty → valid. "([)]" would pop ( to match ] — mismatch → invalid.
🔬 Deeper dive & dry run
Why it works. A stack enforces "last opened, first closed" — the invariant is that the top of the stack is always the most recently opened, unmatched item, which is exactly what nested structures (brackets, tags, function calls, expressions) require for a match.
When to reach for it. Validating/matching nested delimiters, evaluating expressions with precedence, "undo the most recent unresolved thing" (basic calculator, decode string, remove adjacent duplicates), or finding the next greater/smaller element via a monotonic stack.
Complexity. Time O(n): one pass, O(1) push/pop. Space O(n) worst case: an all-openers input like "((((".
Pitfalls & variations. Popping without first checking the stack isn't empty (unmatched closer crashes or false-matches); forgetting to check the stack is empty at the very end (unmatched opener passes silently); mixing bracket types without a closer→opener lookup map. Variant: a monotonic stack keeps elements sorted instead of matching pairs, used for next-greater-element and histogram problems.
Trace input: s = "{[()]}" — determine whether the brackets are validly nested.
| Step | char | Action | Stack after (bottom→top) |
|---|---|---|---|
| 1 | { | opener → push | { |
| 2 | [ | opener → push | { [ |
| 3 | ( | opener → push | { [ ( |
| 4 | ) | closer → top is ( → match → pop | { [ |
| 5 | ] | closer → top is [ → match → pop | { |
| 6 | } | closer → top is { → match → pop | (empty) |
| 7 | end of string | stack is empty → valid | (empty) |
Result: valid (true). Every closer resolved against the current top of stack, and the stack was empty at the end — the trace shows both conditions are necessary, not just "no crash mid-scan".
🧰 Quick reference templates
39. Stack — Parsing
... Need to parse symbols YES ▸ Optimize/count valid spans NO
Matching / evaluating nested symbols (balanced brackets, RPN, expression eval) ⇒ push openers, pop on the matching closer. The stack mirrors nesting depth.
boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
Map<Character, Character> pairs = Map.of(')', '(', ']', '[', '}', '{');
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') stack.push(c);
else if (stack.isEmpty() || stack.pop() != pairs.get(c)) return false;
}
return stack.isEmpty();
}bool isValid(string s) {
stack<char> st;
unordered_map<char, char> pairs = {{')', '('}, {']', '['}, {'}', '{'}};
for (char c : s) {
if (c == '(' || c == '[' || c == '{') st.push(c);
else if (st.empty() || st.top() != pairs[c]) return false;
else st.pop();
}
return st.empty();
}def is_valid(s):
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for c in s:
if c in '([{':
stack.append(c)
elif not stack or stack.pop() != pairs[c]:
return False
return not stackfunction isValid(s: string): boolean {
const stack: string[] = [];
const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
for (const c of s) {
if (c === '(' || c === '[' || c === '{') stack.push(c);
else if (stack.length === 0 || stack.pop() !== pairs[c]) return false;
}
return stack.length === 0;
}"([{}])" ⇒ push (,[,{ then pop in reverse ⇒ empty ⇒ true. "([)]" ⇒ mismatch ⇒ false.Design, Simulation, Math & Everything Else
🎯 LeetCode practice & worked example
4 · Stack
Tests whether you recognize "most recent unmatched thing" or "next element that breaks a trend" problems — nested/matching structures and monotonic scans both collapse to O(n) with a stack instead of repeated backward scans.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Valid Parentheses ★ | Easy | AmazonMetaMicrosoftBloomberg | stack of expected closing brackets |
| 2 | Min Stack ★ | Medium | AmazonGoogleApple | stack paired with running min |
| 3 | Evaluate Reverse Polish Notation ★ | Medium | AmazonMetaGoogle | operand stack, pop two on operator |
| 4 | Generate Parentheses ★ | Medium | AmazonMetaGoogle | backtracking with open/close budget |
| 5 | Daily Temperatures ★ | Medium | AmazonMetaBloomberg | monotonic decreasing stack of indices |
| 6 | Car Fleet | Medium | GoogleAmazon | stack of arrival times by position |
| 7 | Largest Rectangle in Histogram ★ | Hard | AmazonGoogleMeta | monotonic stack of bar indices |
| 8 | Asteroid Collision | Medium | AmazonGoogleBloomberg | stack simulation of collisions |
| 9 | Simplify Path | Medium | AmazonMetaMicrosoft | stack-based path token normalization |
Worked example — Daily Temperatures (LC 739, Medium)
Frequently asked at: Amazon, Meta, Bloomberg, Google
Problem. Given daily temperatures, return for each day the number of days until a warmer temperature, or 0 if none exists.
Idea. Walk left to right keeping a stack of indices whose answer is still unknown, with temperatures in decreasing order down the stack. When the current temperature beats the temperature at the top index, that index's wait is resolved (current index minus stored index); pop and repeat, then push the current index.
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Deque<Integer> stack = new ArrayDeque<>(); // indices, temps decreasing
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int idx = stack.pop();
res[idx] = i - idx;
}
stack.push(i);
}
return res;
}
}class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> res(n, 0);
vector<int> stack; // indices, temps decreasing
for (int i = 0; i < n; i++) {
while (!stack.empty() && temperatures[i] > temperatures[stack.back()]) {
int idx = stack.back();
stack.pop_back();
res[idx] = i - idx;
}
stack.push_back(i);
}
return res;
}
};class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
res = [0] * n
stack = [] # indices, temps decreasing
for i in range(n):
while stack and temperatures[i] > temperatures[stack[-1]]:
idx = stack.pop()
res[idx] = i - idx
stack.append(i)
return resclass Solution {
dailyTemperatures(temperatures: number[]): number[] {
const n = temperatures.length;
const res = new Array(n).fill(0);
const stack: number[] = []; // indices, temps decreasing
for (let i = 0; i < n; i++) {
while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const idx = stack.pop()!;
res[idx] = i - idx;
}
stack.push(i);
}
return res;
}
}Complexity. Time O(n) Space O(n) — each index is pushed once and popped at most once.
Walkthrough. Input [73,74,75,71,69,72,76,73]:
| i | temp | pops (resolved) | stack after (indices) |
|---|---|---|---|
| 0 | 73 | - | [0] |
| 1 | 74 | pop 0 -> res[0]=1 | [1] |
| 2 | 75 | pop 1 -> res[1]=1 | [2] |
| 3 | 71 | - | [2,3] |
| 4 | 69 | - | [2,3,4] |
| 5 | 72 | pop 4 -> res[4]=1, pop 3 -> res[3]=2 | [2,5] |
| 6 | 76 | pop 5 -> res[5]=1, pop 2 -> res[2]=4 | [6] |
| 7 | 73 | - | [6,7] |
Result: [1,1,4,2,1,1,0,0].
🔬 Deeper dive & dry run
Why it works. The stack holds indices of days whose "next warmer day" is still unknown, kept in decreasing order of temperature. When a new, warmer day arrives it resolves every cooler pending day still on the stack in one pass — each index is pushed once and popped at most once, so the total work across all pops is O(n) even though a single day can trigger many pops.
When to reach for it. "Next greater/smaller element to the right" problems, and anything phrased as a running span or histogram comparison (stock span, largest rectangle, next warmer/colder day) — the signal is needing, for every element, the nearest qualifying element ahead of it.
Complexity. O(n) time — each index enters and leaves the stack exactly once; O(n) space for the stack and the answer array.
Pitfalls & variations. Storing values instead of indices on the stack loses the distance needed for the answer. Using if instead of while when popping misses multiple resolutions triggered by one new day. Swapping the comparison direction turns this into "next colder day" instead.
Trace input: temperatures = [73,74,75,71,69,72,76,73] — for each day, how many days until a strictly warmer day (0 if never).
| Step | i | temp | stack before (idx:temp) | pops & ans updates | stack after |
|---|---|---|---|---|---|
| 0 | — | — | — | init ans=[0,0,0,0,0,0,0,0] | [] |
| 1 | 0 | 73 | [] | no pops; push 0 | [0:73] |
| 2 | 1 | 74 | [0:73] | 74 > 73 → pop 0, ans[0]=1-0=1; push 1 | [1:74] |
| 3 | 2 | 75 | [1:74] | 75 > 74 → pop 1, ans[1]=2-1=1; push 2 | [2:75] |
| 4 | 3 | 71 | [2:75] | 71 < 75, no pop; push 3 | [2:75, 3:71] |
| 5 | 4 | 69 | [2:75, 3:71] | 69 < 71, no pop; push 4 | [2:75, 3:71, 4:69] |
| 6 | 5 | 72 | [2:75, 3:71, 4:69] | 72>69 pop4 ans[4]=1; 72>71 pop3 ans[3]=2; 72<75 stop; push 5 | [2:75, 5:72] |
| 7 | 6 | 76 | [2:75, 5:72] | 76>72 pop5 ans[5]=1; 76>75 pop2 ans[2]=4; stack empty; push 6 | [6:76] |
| 8 | 7 | 73 | [6:76] | 73 < 76, no pop; push 7 | [6:76, 7:73] |
Result: ans = [1,1,4,2,1,1,0,0]. Indices 6 and 7 keep their default 0 — nothing warmer ever comes, so they are never popped, which is exactly what the leftover stack contents mean.
23 · Design + Supporting Structures
Flowchart leaf: object with operation guarantees
The idea
"Design a structure where every operation is O(1)/O(log n)." The trick is composing primitives: a hash map for lookup plus a doubly linked list for order (LRU), an array plus a map for insert-delete-getRandom, a heap plus lazy deletion for a max-stream. Pick the data structure whose strengths match each required operation.
🧰 API — Ordered (sorted) map & set methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
TreeMap<Integer, String> m = new TreeMap<>(); // declare/create ordered map (red-black tree)
TreeSet<Integer> s = new TreeSet<>(); // declare/create ordered set
// ----- insert / update -----
m.put(k, v); // insert/overwrite key k -> value v, O(log n)
s.add(x); // insert x, O(log n)
// ----- access -----
m.get(k); // value for k, or null if absent, O(log n)
m.containsKey(k); // true if key present, O(log n)
m.size(); // number of entries, O(1)
// ----- ordered queries -----
m.firstKey(); m.lastKey(); // smallest / largest key, O(log n)
m.floorKey(k); m.ceilingKey(k); // largest key <= k / smallest key >= k, O(log n)
m.lowerKey(k); m.higherKey(k); // largest key < k / smallest key > k, O(log n)
s.floor(x); s.ceiling(x); // largest elem <= x / smallest elem >= x, O(log n)
s.lower(x); s.higher(x); // largest elem < x / smallest elem > x, O(log n)
// ----- remove -----
m.remove(k); // delete key k, O(log n)
s.remove(x); // delete x, O(log n)
m.pollFirstEntry(); m.pollLastEntry(); // remove & return smallest/largest entry, O(log n)
// ----- iterate (sorted order) -----
for (var e : m.entrySet()) { } // iterate entries in ascending key order, O(n)
for (int x : s) { } // iterate elements in ascending order, O(n)
map<int,string> m; // declare/create ordered map (red-black tree)
set<int> s; // declare/create ordered set
// ----- insert / update -----
m[k] = v; // insert/overwrite key k -> value v, O(log n)
m.insert({k, v}); // insert only if key absent, O(log n)
s.insert(x); // insert x, O(log n)
// ----- access -----
m.at(k); // value for k, throws if absent, O(log n)
m.count(k); // membership test (0/1), O(log n)
m.size(); // number of entries, O(1)
// ----- ordered queries -----
m.begin()->first; m.rbegin()->first; // smallest / largest key (via iterator), O(1)
m.lower_bound(k); // iterator to first key >= k, O(log n)
m.upper_bound(k); // iterator to first key > k, O(log n)
s.lower_bound(x); s.upper_bound(x); // analogous floor/ceiling-style queries for set, O(log n)
// ----- remove -----
m.erase(k); // delete key k, O(log n)
s.erase(x); // delete x, O(log n)
// ----- iterate (sorted order) -----
for (auto& [key, val] : m) { } // iterate entries in ascending key order, O(n)
for (int x : s) { } // iterate elements in ascending order, O(n)
# Python's built-in dict/set are NOT sorted-by-key structures (dict keeps
# insertion order, not sort order, and set has no order at all).
# For real ordered-map/set behavior use the third-party 'sortedcontainers'
# package, or roll your own with the stdlib 'bisect' module on a list.
from sortedcontainers import SortedList, SortedDict # pip install sortedcontainers
sl = SortedList() # declare/create sorted list (acts like an ordered multiset)
sd = SortedDict() # declare/create sorted dict (ordered map)
# ----- insert -----
sl.add(x) # insert x, keeps sort order, O(log n)
sd[k] = v # insert/overwrite key k -> value v, O(log n)
# ----- access -----
sl[0]; sl[-1] # smallest / largest element, O(1)
sd.peekitem(0); sd.peekitem(-1) # smallest / largest (key, value) pair, O(log n)
x in sl # membership test, O(log n)
# ----- ordered queries -----
sl.bisect_left(x); sl.bisect_right(x) # insertion point for x, O(log n)
sd.irange(lo, hi) # iterate keys in [lo, hi], O(log n) to start iteration
# ----- remove -----
sl.remove(x) # remove x, O(log n)
del sd[k] # remove key k, O(log n)
# ----- stdlib-only fallback: bisect on a plain sorted list -----
import bisect
arr = []
bisect.insort(arr, x) # insert x keeping arr sorted, O(n) due to shifting
i = bisect.bisect_left(arr, x) # index where x would go (floor/ceiling query), O(log n) search
// TypeScript/JavaScript has NO built-in ordered (sorted) map or set.
// Map/Set preserve insertion order only, not sort order.
// Common workarounds:
// 1) Keep a plain sorted array + binary-search insert (shown below).
// 2) Use a library such as 'js-sorted-set', 'functional-red-black-tree',
// or 'sorted-btree' for O(log n) inserts/deletes.
const arr: number[] = []; // declare/create backing sorted array
function lowerBound(arr: number[], x: number): number {
// first index with arr[i] >= x, O(log n)
let lo = 0, hi = arr.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] < x) lo = mid + 1; else hi = mid;
}
return lo;
}
arr.splice(lowerBound(arr, x), 0, x); // insert x keeping arr sorted, O(n) due to shifting
arr[0]; arr[arr.length - 1]; // smallest / largest element, O(1)
const idx = lowerBound(arr, x); // insertion point (floor/ceiling-style query), O(log n)
arr.splice(lowerBound(arr, x), 1); // remove x, O(n)
Brute force — single structure O(n) somewhere
An LRU built on just an array or just a map makes one operation O(n) (finding the least-recent, or reordering). Combining two structures gives O(1) on all operations.
Template A · LRU cache (map + access-ordered linked map) O(1)
cap— the maximum number of entries the cache may hold.map— the key→value store kept in access order, so the least-recently-used entry always sits at the front.k— the key being looked up or inserted.v— the value being stored for that key.- Returns —
getreturns the value forkor-1if absent;putinserts/updates and evicts the least-recently-used entry oncecapis exceeded.
class LRUCache {
private final int cap;
private final LinkedHashMap<Integer,Integer> map;
LRUCache(int cap){
this.cap = cap;
map = new LinkedHashMap<>(cap, 0.75f, true){ // access-order
protected boolean removeEldestEntry(Map.Entry<Integer,Integer> e){
return size() > LRUCache.this.cap; // auto-evict LRU
}
};
}
int get(int k){ return map.getOrDefault(k, -1); }
void put(int k, int v){ map.put(k, v); }
}class LRUCache {
private:
int cap;
std::list<std::pair<int,int>> items; // front = most recent
std::unordered_map<int, std::list<std::pair<int,int>>::iterator> map;
public:
LRUCache(int cap) : cap(cap) {}
int get(int k){
auto it = map.find(k);
if (it == map.end()) return -1;
items.splice(items.begin(), items, it->second); // move to front (access-order)
return it->second->second;
}
void put(int k, int v){
auto it = map.find(k);
if (it != map.end()){
it->second->second = v;
items.splice(items.begin(), items, it->second);
} else {
items.emplace_front(k, v);
map[k] = items.begin();
if ((int)items.size() > cap){ // auto-evict LRU
auto last = items.end(); --last;
map.erase(last->first);
items.pop_back();
}
}
}
};class LRUCache:
def __init__(self, cap):
self.cap = cap
self.map = OrderedDict() # access-order
def get(self, k):
if k not in self.map:
return -1
self.map.move_to_end(k)
return self.map[k]
def put(self, k, v):
self.map[k] = v
self.map.move_to_end(k)
if len(self.map) > self.cap: # auto-evict LRU
self.map.popitem(last=False)class LRUCache {
private cap: number;
private map: Map<number, number>; // insertion order == access order (re-inserted on touch)
constructor(cap: number) {
this.cap = cap;
this.map = new Map();
}
get(k: number): number {
if (!this.map.has(k)) return -1;
const v = this.map.get(k)!;
this.map.delete(k);
this.map.set(k, v); // move to most-recent
return v;
}
put(k: number, v: number): void {
this.map.delete(k);
this.map.set(k, v);
if (this.map.size > this.cap) { // auto-evict LRU
const oldest = this.map.keys().next().value!;
this.map.delete(oldest);
}
}
}Template B · min-stack (pair each value with the min-so-far)
st— a stack where each entry pairs a pushed value with the minimum of everything at or below it.x— the value being pushed.m— the minimum amongxand everything currently on the stack, stored alongsidexso it's retrievable in O(1).- Returns —
topgives the most recently pushed value,getMingives the minimum over all values currently on the stack.
class MinStack {
private final Deque<int[]> st = new ArrayDeque<>(); // {value, minAtOrBelow}
void push(int x){ int m = st.isEmpty()? x : Math.min(x, st.peek()[1]); st.push(new int[]{x,m}); }
void pop(){ st.pop(); }
int top(){ return st.peek()[0]; }
int getMin(){ return st.peek()[1]; }
}class MinStack {
private:
std::vector<std::pair<int,int>> st; // {value, minAtOrBelow}
public:
void push(int x){ int m = st.empty() ? x : std::min(x, st.back().second); st.push_back({x, m}); }
void pop(){ st.pop_back(); }
int top(){ return st.back().first; }
int getMin(){ return st.back().second; }
};class MinStack:
def __init__(self):
self.st = [] # (value, minAtOrBelow)
def push(self, x):
m = x if not self.st else min(x, self.st[-1][1])
self.st.append((x, m))
def pop(self):
self.st.pop()
def top(self):
return self.st[-1][0]
def get_min(self):
return self.st[-1][1]class MinStack {
private st: [number, number][] = []; // [value, minAtOrBelow]
push(x: number): void {
const m = this.st.length === 0 ? x : Math.min(x, this.st[this.st.length - 1][1]);
this.st.push([x, m]);
}
pop(): void { this.st.pop(); }
top(): number { return this.st[this.st.length - 1][0]; }
getMin(): number { return this.st[this.st.length - 1][1]; }
}Worked example — LRU, capacity 2
| op | state (LRU→MRU) | returns |
|---|---|---|
| put(1,1) | 1 | — |
| put(2,2) | 1,2 | — |
| get(1) | 2,1 | 1 (1 becomes most-recent) |
| put(3,3) | 1,3 | evicts key 2 (least recent) |
| get(2) | 1,3 | −1 (gone) |
Every op is O(1) because the map finds entries and the ordering list tracks recency simultaneously.
🔬 Deeper dive & dry run
Why it works. Pairing a hashmap (O(1) key→node lookup) with a doubly linked list (O(1) unlink/move-to-front) maintains two invariants at once: the map always points to a live node, and the list order always reflects recency. Eviction becomes "drop the tail" with no scanning.
When to reach for it. The problem demands O(1) (or amortized O(1)) for both a keyed lookup and an order-sensitive operation: LRU/LFU cache, min-stack, browser history, "insert/delete/getRandom in O(1)".
Complexity. Time O(1) per get/put. Space O(capacity): one map entry + one list node per cached key.
Pitfalls & variations. A hashmap alone loses order (can't find the LRU key in O(1)); a list alone loses O(1) lookup; forgetting that get must also move the accessed node to the front (not just put) breaks the recency invariant; eviction must happen at capacity, and must happen for a new key, not an update to an existing one.
Trace input: LRUCache(capacity=2); put(1,1); put(2,2); get(1); put(3,3); get(2); put(4,4); get(1); get(3); get(4) — the canonical LC146 walkthrough.
| Step | Operation | Cache (MRU→LRU) | Evicted | Returned |
|---|---|---|---|---|
| 1 | put(1,1) | [1] | — | — |
| 2 | put(2,2) | [2, 1] | — | — |
| 3 | get(1) | [1, 2] | — | 1 |
| 4 | put(3,3) | [3, 1] | 2 | — |
| 5 | get(2) | [3, 1] | — | -1 |
| 6 | put(4,4) | [4, 3] | 1 | — |
| 7 | get(1) | [4, 3] | — | -1 |
| 8 | get(3) | [3, 4] | — | 3 |
| 9 | get(4) | [4, 3] | — | 4 |
Result: returns 1, -1, -1, 3, 4, matching the standard LC146 example exactly. The trace shows a plain get still reorders the list even though it never inserts — the detail most implementations get wrong.
🧰 Quick reference templates
40. Design + Supporting Data Structures
... Need to parse symbols NO ▸ Object with operation guarantees YES
"Design a structure with O(1)/O(log n) operations" (LRU cache, min-stack, insert-delete-getRandom). Combine primitives — a hash map for lookup plus a linked list or heap for ordering.
class LRUCache { // O(1) get/put
private final int capacity;
private final LinkedHashMap<Integer, Integer> map;
LRUCache(int capacity) {
this.capacity = capacity;
map = new LinkedHashMap<>(capacity, 0.75f, true) { // access-order
protected boolean removeEldestEntry(Map.Entry<Integer,Integer> e) {
return size() > LRUCache.this.capacity; // auto-evict LRU
}
};
}
int get(int key) { return map.getOrDefault(key, -1); }
void put(int key, int val) { map.put(key, val); }
}class LRUCache { // O(1) get/put
public:
LRUCache(int capacity) : capacity(capacity) {}
int get(int key) {
auto it = map.find(key);
if (it == map.end()) return -1;
order.splice(order.begin(), order, it->second); // move to front (most recent)
return it->second->second;
}
void put(int key, int val) {
auto it = map.find(key);
if (it != map.end()) {
it->second->second = val;
order.splice(order.begin(), order, it->second);
return;
}
order.emplace_front(key, val);
map[key] = order.begin();
if ((int)map.size() > capacity) {
auto last = order.back();
map.erase(last.first);
order.pop_back(); // auto-evict LRU
}
}
private:
int capacity;
list<pair<int,int>> order; // front = most recent
unordered_map<int, list<pair<int,int>>::iterator> map;
};from collections import OrderedDict
class LRUCache: # O(1) get/put
def __init__(self, capacity: int):
self.capacity = capacity
self.map = OrderedDict() # access-order
def get(self, key: int) -> int:
if key not in self.map:
return -1
self.map.move_to_end(key)
return self.map[key]
def put(self, key: int, val: int) -> None:
if key in self.map:
self.map.move_to_end(key)
self.map[key] = val
if len(self.map) > self.capacity:
self.map.popitem(last=False) # auto-evict LRUclass LRUCache { // O(1) get/put
private capacity: number;
private map: Map<number, number>; // Map preserves insertion order
constructor(capacity: number) {
this.capacity = capacity;
this.map = new Map();
}
get(key: number): number {
if (!this.map.has(key)) return -1;
const val = this.map.get(key)!;
this.map.delete(key);
this.map.set(key, val); // move to end (most recent)
return val;
}
put(key: number, val: number): void {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, val);
if (this.map.size > this.capacity) {
const lruKey = this.map.keys().next().value;
this.map.delete(lruKey); // auto-evict LRU
}
}
}24 · Simulation
Flowchart leaf: direct transformation / simulation
The idea
The problem literally describes a process — do exactly what it says, carefully. No clever algorithm, just correct bookkeeping and boundaries: spiral traversal, matrix rotation, Conway's life, robot movement. The skill is managing indices and directions without off-by-one errors.
Template A · spiral matrix (shrinking borders) O(m·n)
m— the input 2-D matrix to read in spiral order.res— the output list being built in spiral order.top,bot,left,right— the current unvisited rectangle's boundaries, each shrinking by one after its edge is walked.r,c— the row/column index used while walking one edge of the current boundary.- Returns — all matrix elements in spiral (clockwise, outside-in) order.
List<Integer> spiralOrder(int[][] m){
List<Integer> res = new ArrayList<>();
int top=0, bot=m.length-1, left=0, right=m[0].length-1;
while (top<=bot && left<=right){
for (int c=left;c<=right;c++) res.add(m[top][c]); top++;
for (int r=top;r<=bot;r++) res.add(m[r][right]); right--;
if (top<=bot){ for (int c=right;c>=left;c--) res.add(m[bot][c]); bot--; }
if (left<=right){ for (int r=bot;r>=top;r--) res.add(m[r][left]); left++; }
}
return res;
}std::vector<int> spiralOrder(std::vector<std::vector<int>>& m){
std::vector<int> res;
int top=0, bot=m.size()-1, left=0, right=m[0].size()-1;
while (top<=bot && left<=right){
for (int c=left;c<=right;c++) res.push_back(m[top][c]); top++;
for (int r=top;r<=bot;r++) res.push_back(m[r][right]); right--;
if (top<=bot){ for (int c=right;c>=left;c--) res.push_back(m[bot][c]); bot--; }
if (left<=right){ for (int r=bot;r>=top;r--) res.push_back(m[r][left]); left++; }
}
return res;
}def spiral_order(m):
res = []
top, bot, left, right = 0, len(m) - 1, 0, len(m[0]) - 1
while top <= bot and left <= right:
for c in range(left, right + 1): res.append(m[top][c])
top += 1
for r in range(top, bot + 1): res.append(m[r][right])
right -= 1
if top <= bot:
for c in range(right, left - 1, -1): res.append(m[bot][c])
bot -= 1
if left <= right:
for r in range(bot, top - 1, -1): res.append(m[r][left])
left += 1
return resfunction spiralOrder(m: number[][]): number[] {
const res: number[] = [];
let top = 0, bot = m.length - 1, left = 0, right = m[0].length - 1;
while (top <= bot && left <= right) {
for (let c = left; c <= right; c++) res.push(m[top][c]); top++;
for (let r = top; r <= bot; r++) res.push(m[r][right]); right--;
if (top <= bot) { for (let c = right; c >= left; c--) res.push(m[bot][c]); bot--; }
if (left <= right) { for (let r = bot; r >= top; r--) res.push(m[r][left]); left++; }
}
return res;
}Template B · rotate image 90° in place (transpose then reverse rows)
a— the square matrix, rotated in place (no new matrix is allocated).n— the matrix's side length.i,j— row/column indices used while transposing (swappinga[i][j]witha[j][i]).t— a scratch variable used to swap two values.row,l,r— the row being reversed and its left/right pointers, which walk inward to reverse it.- Returns — nothing;
aends up rotated 90° clockwise.
void rotate(int[][] a){
int n = a.length;
for (int i=0;i<n;i++) for (int j=i+1;j<n;j++){ int t=a[i][j]; a[i][j]=a[j][i]; a[j][i]=t; }
for (int[] row : a) for (int l=0,r=n-1;l<r;l++,r--){ int t=row[l]; row[l]=row[r]; row[r]=t; }
}void rotate(std::vector<std::vector<int>>& a){
int n = a.size();
for (int i=0;i<n;i++) for (int j=i+1;j<n;j++){ int t=a[i][j]; a[i][j]=a[j][i]; a[j][i]=t; }
for (auto& row : a) for (int l=0,r=n-1;l<r;l++,r--){ int t=row[l]; row[l]=row[r]; row[r]=t; }
}def rotate(a):
n = len(a)
for i in range(n):
for j in range(i + 1, n):
a[i][j], a[j][i] = a[j][i], a[i][j]
for row in a:
l, r = 0, n - 1
while l < r:
row[l], row[r] = row[r], row[l]
l += 1
r -= 1function rotate(a: number[][]): void {
const n = a.length;
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) { const t = a[i][j]; a[i][j] = a[j][i]; a[j][i] = t; }
for (const row of a) for (let l = 0, r = n - 1; l < r; l++, r--) { const t = row[l]; row[l] = row[r]; row[r] = t; }
}Worked example — spiral of a 3×3
1 2 3
4 5 6 → 1 2 3 · 6 9 · 8 7 · 4 · 5
7 8 9// 1 2 3
// 4 5 6 → 1 2 3 · 6 9 · 8 7 · 4 · 5
// 7 8 9# 1 2 3
# 4 5 6 → 1 2 3 · 6 9 · 8 7 · 4 · 5
# 7 8 9// 1 2 3
// 4 5 6 → 1 2 3 · 6 9 · 8 7 · 4 · 5
// 7 8 9Top row (1,2,3), right col (6,9), bottom row reversed (8,7), left col (4), center (5) → [1,2,3,6,9,8,7,4,5]. Each border shrinks after it's consumed, and the two if guards stop a single leftover row/column from being walked twice.
🔬 Deeper dive & dry run
Why it works. Simulation problems don't hinge on a clever formula — they hinge on precisely maintained state (position, direction, shrinking boundaries) updated exactly per the rules given. Correctness comes from getting each state transition and boundary check exactly right.
When to reach for it. The problem literally describes a step-by-step process — walk a grid, deal cards, run a simulated game/robot — and asks for the state after N steps or the full output order. No shortcut exists; you model exactly what's described.
Complexity. Time O(cells) or O(steps): every cell/step is touched once. Space O(1) extra beyond the output: a handful of boundary/position variables.
Pitfalls & variations. Off-by-one when shrinking an inclusive boundary; forgetting to re-check top ≤ bottom / left ≤ right between the horizontal and vertical sweeps of the same lap (causes double-visiting the middle row/column on odd-sized inputs); direction-cycling logic drifting on non-square matrices.
Trace input: matrix = [[1,2,3],[4,5,6],[7,8,9]] — read all elements in spiral order.
| Step | Leg | Boundaries before (T,B,L,R) | Cells visited | Result so far |
|---|---|---|---|---|
| 1 | top row, left→right | T0 B2 L0 R2 | 1, 2, 3 | [1,2,3] |
| 2 | right col, top→bottom | T1 B2 L0 R2 | 6, 9 | [1,2,3,6,9] |
| 3 | bottom row, right→left | T1 B2 L0 R1 | 8, 7 | [1,2,3,6,9,8,7] |
| 4 | left col, bottom→top | T1 B1 L0 R1 | 4 | [1,2,3,6,9,8,7,4] |
| 5 | top row, left→right | T1 B1 L1 R1 | 5 | [1,2,3,6,9,8,7,4,5] |
| 6 | loop check: T2 > B1 | T2 B1 L1 R1 | (none — loop exits) | [1,2,3,6,9,8,7,4,5] |
Result: [1, 2, 3, 6, 9, 8, 7, 4, 5]. Each leg shrinks exactly one boundary and re-tests the loop condition, which is what stops the sweep from re-visiting the center cell.
🧰 Quick reference templates
41. Simulation / Basic DSA
... Object with guarantees NO ▸ Direct transformation/simulation YES
The problem just describes a process — do exactly what it says, carefully (spiral traversal, matrix rotation, game of life). The skill is bookkeeping and boundaries, not a fancy algorithm.
List<Integer> spiralOrder(int[][] m) {
List<Integer> res = new ArrayList<>();
int top = 0, bottom = m.length - 1, left = 0, right = m[0].length - 1;
while (top <= bottom && left <= right) {
for (int c = left; c <= right; c++) res.add(m[top][c]); top++;
for (int r = top; r <= bottom; r++) res.add(m[r][right]); right--;
if (top <= bottom) { for (int c = right; c >= left; c--) res.add(m[bottom][c]); bottom--; }
if (left <= right) { for (int r = bottom; r >= top; r--) res.add(m[r][left]); left++; }
}
return res;
}vector<int> spiralOrder(vector<vector<int>>& m) {
vector<int> res;
int top = 0, bottom = m.size() - 1, left = 0, right = m[0].size() - 1;
while (top <= bottom && left <= right) {
for (int c = left; c <= right; c++) res.push_back(m[top][c]); top++;
for (int r = top; r <= bottom; r++) res.push_back(m[r][right]); right--;
if (top <= bottom) { for (int c = right; c >= left; c--) res.push_back(m[bottom][c]); bottom--; }
if (left <= right) { for (int r = bottom; r >= top; r--) res.push_back(m[r][left]); left++; }
}
return res;
}def spiral_order(m):
res = []
top, bottom, left, right = 0, len(m) - 1, 0, len(m[0]) - 1
while top <= bottom and left <= right:
for c in range(left, right + 1):
res.append(m[top][c])
top += 1
for r in range(top, bottom + 1):
res.append(m[r][right])
right -= 1
if top <= bottom:
for c in range(right, left - 1, -1):
res.append(m[bottom][c])
bottom -= 1
if left <= right:
for r in range(bottom, top - 1, -1):
res.append(m[r][left])
left += 1
return resfunction spiralOrder(m: number[][]): number[] {
const res: number[] = [];
let top = 0, bottom = m.length - 1, left = 0, right = m[0].length - 1;
while (top <= bottom && left <= right) {
for (let c = left; c <= right; c++) res.push(m[top][c]); top++;
for (let r = top; r <= bottom; r++) res.push(m[r][right]); right--;
if (top <= bottom) { for (let c = right; c >= left; c--) res.push(m[bottom][c]); bottom--; }
if (left <= right) { for (let r = bottom; r >= top; r--) res.push(m[r][left]); left++; }
}
return res;
}[[1,2,3],[4,5,6],[7,8,9]] ⇒ [1,2,3,6,9,8,7,4,5] — walk the shrinking border.25 · Math & Bit Manipulation
Flowchart leaf: math identities, powers, bit tricks
The idea
Exploit algebraic or bitwise identities to replace loops with O(1)/O(log) tricks. XOR cancels equal pairs (x^x=0); n & (n-1) clears the lowest set bit; fast exponentiation squares in O(log e).
Template A · single number via XOR O(n), O(1) space
a— the input array where every value appears exactly twice except one.x— the running XOR of all values seen so far; paired values cancel out to zero.v— the current array element being folded intox.- Returns — the one value that has no matching pair.
int singleNumber(int[] a){ int x=0; for (int v:a) x^=v; return x; } // pairs vanishint singleNumber(std::vector<int>& a){ int x=0; for (int v : a) x^=v; return x; } // pairs vanishdef single_number(a):
x = 0
for v in a:
x ^= v # pairs vanish
return xfunction singleNumber(a: number[]): number {
let x = 0;
for (const v of a) x ^= v; // pairs vanish
return x;
}Template B · fast modular exponentiation O(log e)
base— the number being raised to a power, reduced modmodas it's squared repeatedly.e— the exponent, consumed one bit at a time by shifting right each iteration.mod— the modulus all results are kept under.r— the running result, multiplied bybasewhenever the current low bit ofeis 1.- Returns —
baseraised to thee-th power, modulomod.
long power(long base, long e, long mod){
long r = 1; base %= mod;
while (e > 0){
if ((e & 1) == 1) r = r * base % mod; // odd bit ⇒ multiply
base = base * base % mod; // square
e >>= 1;
}
return r;
}long long power(long long base, long long e, long long mod){
long long r = 1; base %= mod;
while (e > 0){
if ((e & 1) == 1) r = r * base % mod; // odd bit ⇒ multiply
base = base * base % mod; // square
e >>= 1;
}
return r;
}def power(base, e, mod):
r = 1
base %= mod
while e > 0:
if e & 1 == 1:
r = r * base % mod # odd bit ⇒ multiply
base = base * base % mod # square
e >>= 1
return rfunction power(base: bigint, e: bigint, mod: bigint): bigint {
let r = 1n;
base %= mod;
while (e > 0n) {
if ((e & 1n) === 1n) r = r * base % mod; // odd bit ⇒ multiply
base = base * base % mod; // square
e >>= 1n;
}
return r;
}Common bit tricks
n— the integer whose bits are being inspected or manipulated.i— the bit position (from the low end) being toggled in the last line.- Returns — each line is a standalone expression: clearing the lowest set bit, isolating it, counting set bits (popcount), or flipping bit
i.
n & (n-1) // drop lowest set bit → power-of-two test: n>0 && (n&(n-1))==0
n & -n // isolate lowest set bit
Integer.bitCount(n) // popcount
x ^= (1 << i) // toggle bit in & (n-1) // drop lowest set bit → power-of-two test: n>0 && (n&(n-1))==0
n & -n // isolate lowest set bit
__builtin_popcount(n) // popcount
x ^= (1 << i) // toggle bit in & (n - 1) # drop lowest set bit → power-of-two test: n>0 and (n&(n-1))==0
n & -n # isolate lowest set bit
bin(n).count('1') # popcount
x ^= (1 << i) # toggle bit in & (n - 1); // drop lowest set bit → power-of-two test: n>0 && (n&(n-1))==0
n & -n; // isolate lowest set bit
n.toString(2).split('1').length - 1; // popcount
x ^= (1 << i); // toggle bit iWorked example — single number in [4,1,2,1,2]
XOR is associative/commutative, so order doesn't matter: 4^1^2^1^2 = 4^(1^1)^(2^2) = 4^0^0 = 4. The two 1s and two 2s annihilate, leaving the loner.
🔬 Deeper dive & dry run
Why it works. XOR is its own inverse and commutative/associative, so XOR-ing every element cancels any value that appears an even number of times (a^a=0). The running invariant is "accumulator = XOR of all values not yet cancelled," so after a full pass only the odd-count value(s) survive.
When to reach for it. "Every element appears twice except one," parity/toggle questions, swapping without a temp variable, or representing a set as a bitmask (subset enumeration, "missing number") — anywhere equality-under-XOR determines the answer.
Complexity. Time O(n): one pass. Space O(1): a single accumulator.
Pitfalls & variations. Plain XOR only isolates a single odd-count value cleanly when everything else appears in pairs — "Single Number II" (others appear 3×) and "Single Number III" (two singles) need bit-counting or a partitioning bit, not plain XOR alone; reaching for addition/subtraction instead of XOR risks overflow and needs sorting.
Trace input: nums = [4, 1, 2, 1, 2] — find the element that appears exactly once.
| Step | num | num (binary) | result before | result after (binary) |
|---|---|---|---|---|
| 0 (init) | — | — | — | 000 (0) |
| 1 | 4 | 100 | 000 | 100 (4) |
| 2 | 1 | 001 | 100 | 101 (5) |
| 3 | 2 | 010 | 101 | 111 (7) |
| 4 | 1 | 001 | 111 | 110 (6) |
| 5 | 2 | 010 | 110 | 100 (4) |
Result: 4. Every value that appears twice (1 and 2) cancels itself out in pairs, so the running XOR settles on the one value without a partner.
🧰 Quick reference templates
42. Math / Bit Manipulation
... Math identities/powers/bit tricks YES ▸ Number properties NO
Exploit algebraic or bitwise identities for O(1)/O(log) tricks: XOR cancels pairs, n & (n-1) clears the lowest set bit, powers of two have one bit.
int singleNumber(int[] nums) { // every value appears twice except one
int x = 0;
for (int n : nums) x ^= n; // pairs cancel to 0, loner survives
return x;
}
boolean isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }
int countBits(int n) { int c = 0; while (n != 0) { n &= n - 1; c++; } return c; }int singleNumber(std::vector<int>& nums) { // every value appears twice except one
int x = 0;
for (int n : nums) x ^= n; // pairs cancel to 0, loner survives
return x;
}
bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }
int countBits(int n) { int c = 0; while (n != 0) { n &= n - 1; c++; } return c; }def single_number(nums): # every value appears twice except one
x = 0
for n in nums:
x ^= n # pairs cancel to 0, loner survives
return x
def is_power_of_two(n): return n > 0 and (n & (n - 1)) == 0
def count_bits(n):
c = 0
while n != 0:
n &= n - 1
c += 1
return cfunction singleNumber(nums: number[]): number { // every value appears twice except one
let x = 0;
for (const n of nums) x ^= n; // pairs cancel to 0, loner survives
return x;
}
function isPowerOfTwo(n: number): boolean { return n > 0 && (n & (n - 1)) === 0; }
function countBits(n: number): number { let c = 0; while (n !== 0) { n &= n - 1; c++; } return c; }[4,1,2,1,2] ⇒ 1^1=0, 2^2=0, leaving 4. isPowerOfTwo(16) = true; countBits(13=1101) = 3.🎯 LeetCode practice & worked example
17 · Math & Geometry
Tests simulating array/matrix transformations or numeric identities directly — the tell is in-place matrix manipulation or a closed-form arithmetic trick rather than a classic data structure.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Rotate Image ★ | Medium | AmazonMicrosoftApple | transpose then reverse rows |
| 2 | Spiral Matrix ★ | Medium | AmazonMicrosoftGoogle | shrinking boundary walk |
| 3 | Set Matrix Zeroes | Medium | AmazonMicrosoft | marker row/col, O(1) extra space |
| 4 | Happy Number | Easy | AmazonGoogle | cycle detection via digit-square sum |
| 5 | Plus One | Easy | GoogleAmazon | carry propagation from the right |
| 6 | Pow(x, n) | Medium | LinkedInAmazon | fast exponentiation by squaring |
| 7 | Multiply Strings | Medium | AmazonFacebook | digit-by-digit convolution |
| 8 | Detect Squares | Medium | GoogleAmazon | hashmap counting + axis-aligned geometry |
Worked example — Spiral Matrix (LC 54, Medium)
Frequently asked at: Amazon, Microsoft, Google, Bloomberg.
Problem. Given an m x n matrix, return all elements of the matrix in spiral order (clockwise, starting top-left).
Idea. Maintain four shrinking boundaries: top, bottom, left, right. Walk right across the top row, down the right column, left across the bottom row, and up the left column, tightening each boundary after its pass. Guard the last two passes with boundary checks so a single remaining row or column isn't traversed twice.
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
int top = 0, bottom = matrix.length - 1;
int left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
for (int j = left; j <= right; j++) result.add(matrix[top][j]);
top++;
for (int i = top; i <= bottom; i++) result.add(matrix[i][right]);
right--;
if (top <= bottom) {
for (int j = right; j >= left; j--) result.add(matrix[bottom][j]);
bottom--;
}
if (left <= right) {
for (int i = bottom; i >= top; i--) result.add(matrix[i][left]);
left++;
}
}
return result;
}
}
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> result;
int top = 0, bottom = matrix.size() - 1;
int left = 0, right = matrix[0].size() - 1;
while (top <= bottom && left <= right) {
for (int j = left; j <= right; j++) result.push_back(matrix[top][j]);
top++;
for (int i = top; i <= bottom; i++) result.push_back(matrix[i][right]);
right--;
if (top <= bottom) {
for (int j = right; j >= left; j--) result.push_back(matrix[bottom][j]);
bottom--;
}
if (left <= right) {
for (int i = bottom; i >= top; i--) result.push_back(matrix[i][left]);
left++;
}
}
return result;
}
};class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
result = []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
while top <= bottom and left <= right:
for j in range(left, right + 1):
result.append(matrix[top][j])
top += 1
for i in range(top, bottom + 1):
result.append(matrix[i][right])
right -= 1
if top <= bottom:
for j in range(right, left - 1, -1):
result.append(matrix[bottom][j])
bottom -= 1
if left <= right:
for i in range(bottom, top - 1, -1):
result.append(matrix[i][left])
left += 1
return resultclass Solution {
spiralOrder(matrix: number[][]): number[] {
const result: number[] = [];
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
for (let j = left; j <= right; j++) result.push(matrix[top][j]);
top++;
for (let i = top; i <= bottom; i++) result.push(matrix[i][right]);
right--;
if (top <= bottom) {
for (let j = right; j >= left; j--) result.push(matrix[bottom][j]);
bottom--;
}
if (left <= right) {
for (let i = bottom; i >= top; i--) result.push(matrix[i][left]);
left++;
}
}
return result;
}
}Complexity. Time O(m × n) Space O(1) extra — every cell visited exactly once, output list aside.
Walkthrough. matrix = [[1,2,3],[4,5,6],[7,8,9]], boundaries start at top=0, bottom=2, left=0, right=2:
| pass | direction | emitted | boundaries after |
|---|---|---|---|
| 1 | right along top row | 1, 2, 3 | top=1 |
| 2 | down right column | 6, 9 | right=1 |
| 3 | left along bottom row | 8, 7 | bottom=1 |
| 4 | up left column | 4 | left=1 |
| 5 | right along top row | 5 | top=2, loop ends (top>bottom) |
Result: [1, 2, 3, 6, 9, 8, 7, 4, 5].
🔬 Deeper dive & dry run
Why it works. Maintaining four shrinking boundaries (top, bottom, left, right) turns "walk in a spiral" into four straight-line walks repeated in a fixed order (right along top, down along right, left along bottom, up along left), each followed by shrinking the boundary just traversed. Guard checks (top ≤ bottom, left ≤ right) before each leg prevent re-visiting or double-counting cells once a row or column has been fully consumed — this is what correctly handles non-square and odd-sized leftover slivers.
When to reach for it. Any "walk the matrix in a specific geometric order" problem (spiral, diagonal, boustrophedon/snake) where the pattern is really just several straight passes glued together, and you can simulate it directly rather than searching for a closed-form index formula.
Complexity. Time O(m × n) — every cell is visited exactly once. Space O(1) extra beyond the output array (four integer boundaries).
Pitfalls & variations. Missing the boundary re-check between the 3rd and 4th legs of each loop iteration causes cells to be added twice (or a row/column to leak into the count) once the spiral runs out of room, most visibly on a single row, single column, or non-square input; generating a spiral (the inverse problem, LC 59) uses the exact same four-boundary skeleton but writes instead of reads.
Trace input: matrix = [[1,2,3],[4,5,6],[7,8,9]] — return all elements in spiral (clockwise, inward) order.
| Step | top,bottom,left,right (before) | Leg | Cells visited | result so far |
|---|---|---|---|---|
| 1 | 0,2,0,2 | top row, left→right | 1,2,3 | [1,2,3] |
| 2 | 1,2,0,2 | right col, top→bottom | 6,9 | [1,2,3,6,9] |
| 3 | 1,2,0,1 | bottom row, right→left | 8,7 | [1,2,3,6,9,8,7] |
| 4 | 1,1,0,1 | left col, bottom→top | 4 | [1,2,3,6,9,8,7,4] |
| 5 | 1,1,1,1 | top row, left→right (loop re-check top≤bottom & left≤right passes) | 5 | [1,2,3,6,9,8,7,4,5] |
Result: [1,2,3,6,9,8,7,4,5]. After step 4, top becomes 2 which is > bottom(1), so the outer loop exits right after the lone leftover cell 5 — the boundary checks are what stop the spiral from re-reading already-visited cells.
18 · Bit Manipulation
Tests manipulating numbers at the binary level — the tell is XOR/AND/shift tricks that replace an O(n) hash set or auxiliary array with O(1) bit arithmetic.
| # | Problem | Diff | Companies | Sub-pattern |
|---|---|---|---|---|
| 1 | Single Number ★ | Easy | AmazonGooglePalantir | XOR self-cancellation |
| 2 | Number of 1 Bits ★ | Easy | AmazonAppleMicrosoft | Brian Kernighan's n & (n-1) |
| 3 | Counting Bits ★ | Easy | GoogleAmazon | dp on lowest set bit |
| 4 | Reverse Bits | Easy | AppleMicrosoft | bit-by-bit shift and reversal |
| 5 | Missing Number | Easy | AmazonMicrosoft | XOR range trick / sum formula |
| 6 | Sum of Two Integers | Medium | AmazonFacebook | XOR for sum, AND+shift for carry |
| 7 | Reverse Integer | Medium | AmazonApple | overflow-safe digit-by-digit reversal |
Worked example — Counting Bits (LC 338, Easy)
Frequently asked at: Google, Amazon, Adobe.
Problem. Given an integer n, return an array ans of length n + 1 where ans[i] is the number of 1 bits (popcount) in the binary representation of i, for every i from 0 to n.
Idea. Every integer i > 0 is i >> 1 shifted left by one bit, plus possibly a trailing 1. So its popcount equals the popcount of i >> 1 (already computed) plus the last bit i & 1: dp[i] = dp[i >> 1] + (i & 1). This turns an O(n log n) "count bits per number" scan into O(n) total work.
class Solution {
public int[] countBits(int n) {
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
dp[i] = dp[i >> 1] + (i & 1);
}
return dp;
}
}
class Solution {
public:
vector<int> countBits(int n) {
vector<int> dp(n + 1, 0);
for (int i = 1; i <= n; i++) {
dp[i] = dp[i >> 1] + (i & 1);
}
return dp;
}
};class Solution:
def countBits(self, n: int) -> List[int]:
dp = [0] * (n + 1)
for i in range(1, n + 1):
dp[i] = dp[i >> 1] + (i & 1)
return dpclass Solution {
countBits(n: number): number[] {
const dp = new Array(n + 1).fill(0);
for (let i = 1; i <= n; i++) {
dp[i] = dp[i >> 1] + (i & 1);
}
return dp;
}
}Complexity. Time O(n) Space O(n) — O(1) work per index, reusing the previously computed entry.
Walkthrough. n = 5:
| i | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| i >> 1 | - | 0 | 1 | 1 | 2 | 2 |
| i & 1 | - | 1 | 0 | 1 | 0 | 1 |
| dp[i] | 0 | 1 | 1 | 2 | 1 | 2 |
E.g. dp[3] = dp[1] + 1 = 1 + 1 = 2 (binary 11), and dp[5] = dp[2] + 1 = 1 + 1 = 2 (binary 101). Result: [0, 1, 1, 2, 1, 2].
🔬 Deeper dive & dry run
Why it works. Every integer i is either even (its last bit is 0, so its popcount equals the popcount of i>>1, i.e. dropping that trailing 0 changes nothing) or odd (its last bit is 1, so its popcount is one more than i>>1's). That gives the recurrence dp[i] = dp[i >> 1] + (i & 1), which only ever looks at an already-computed smaller index, so a single forward pass fills the whole table.
When to reach for it. You need popcounts (or a bit-derived property) for every integer in a range and want to avoid recomputing each one from scratch with O(log n) bit-counting; the tell is "for every number from 0 to n" combined with something expressible via a smaller/related integer (i>>1, i & (i-1), etc).
Complexity. Time O(n) total — O(1) work per index instead of the naive O(n log n) from counting bits of each number independently. Space O(n) for the output array.
Pitfalls & variations. Confusing i>>1 (correct: strips the last bit's position, keeps popcount minus that bit) with i>>>1 or integer division edge cases in languages without a dedicated unsigned shift; an equally valid alternate recurrence uses i & (i-1) which clears the lowest set bit, giving dp[i] = dp[i & (i-1)] + 1; both are O(1)-per-step DP, just relating i to a different, smaller prior index.
Trace input: n = 5 — for every integer 0..5, count the number of 1 bits in its binary representation.
| i | binary | i>>1 | dp[i>>1] | i & 1 | dp[i] = dp[i>>1] + (i&1) |
|---|---|---|---|---|---|
| 0 | 0 | — | — | — | 0 (base case) |
| 1 | 1 | 0 | 0 | 1 | 0+1=1 |
| 2 | 10 | 1 | 1 | 0 | 1+0=1 |
| 3 | 11 | 1 | 1 | 1 | 1+1=2 |
| 4 | 100 | 2 | 1 | 0 | 1+0=1 |
| 5 | 101 | 2 | 1 | 1 | 1+1=2 |
Result: [0,1,1,2,1,2]. The trace shows each answer reusing a strictly smaller, already-finalized index (i>>1 < i for all i>0) rather than re-scanning bits, which is why the whole table fills in linear time.
18 NeetCode categories · core problem sets with company tags · one fully-worked Java solution each. Print to PDF: File ▸ Print ▸ Save as PDF (each category starts on a new page).
26 · Number Theory
Flowchart leaf: number properties
The idea
Primes, divisors, GCD/LCM, modular arithmetic. Two workhorses: the Sieve of Eratosthenes finds all primes below n in O(n log log n), and Euclid's GCD runs in O(log min).
Brute force — trial division per number O(n√n)
Testing each number for primality independently. The sieve instead crosses off multiples once, sharing work across all numbers.
Template A · Sieve of Eratosthenes O(n log log n)
n— count primes strictly below this bound.composite— a boolean array wherecomposite[x]is true oncexhas been marked as having a factor, meaning it's not prime.count— the running tally of primes found so far.i— the current candidate number being tested; if still unmarked, it's prime.j— walks through the multiples ofi(starting ati*i) to mark them composite.- Returns — the number of primes strictly less than
n.
int countPrimes(int n){
if (n < 3) return 0;
boolean[] composite = new boolean[n];
int count = 0;
for (int i = 2; i < n; i++){
if (composite[i]) continue;
count++;
for (long j = (long)i*i; j < n; j += i) composite[(int)j] = true; // mark multiples
}
return count;
}int countPrimes(int n){
if (n < 3) return 0;
std::vector<bool> composite(n, false);
int count = 0;
for (int i = 2; i < n; i++){
if (composite[i]) continue;
count++;
for (long long j = (long long)i*i; j < n; j += i) composite[j] = true; // mark multiples
}
return count;
}def count_primes(n):
if n < 3:
return 0
composite = [False] * n
count = 0
for i in range(2, n):
if composite[i]:
continue
count += 1
for j in range(i * i, n, i):
composite[j] = True # mark multiples
return countfunction countPrimes(n: number): number {
if (n < 3) return 0;
const composite = new Array<boolean>(n).fill(false);
let count = 0;
for (let i = 2; i < n; i++) {
if (composite[i]) continue;
count++;
for (let j = i * i; j < n; j += i) composite[j] = true; // mark multiples
}
return count;
}Template B · GCD / LCM
a,b— the two integers to combine;gcdrepeatedly replaces them with(b, a % b)until one is zero.- Returns —
gcdreturns the greatest common divisor;lcmreturns the least common multiple, computed asa / gcd(a, b) * bto avoid overflowing before dividing.
long gcd(long a, long b){ return b == 0 ? a : gcd(b, a % b); }
long lcm(long a, long b){ return a / gcd(a, b) * b; } // divide first to avoid overflowlong long gcd(long long a, long long b){ return b == 0 ? a : gcd(b, a % b); }
long long lcm(long long a, long long b){ return a / gcd(a, b) * b; } // divide first to avoid overflowdef gcd(a, b):
return a if b == 0 else gcd(b, a % b)
def lcm(a, b):
return a // gcd(a, b) * b # divide first to avoid overflowfunction gcd(a: number, b: number): number { return b === 0 ? a : gcd(b, a % b); }
function lcm(a: number, b: number): number { return a / gcd(a, b) * b; } // divide first to avoid overflowWorked example — primes below 10
| i | composite? | marks | count |
|---|---|---|---|
| 2 | no | 4,6,8 | 1 |
| 3 | no | 9 | 2 |
| 4 | yes | skip | 2 |
| 5 | no | (25≥10) | 3 |
| 6..8 | yes | skip | 3 |
| 9 | yes | skip | 3 |
Wait — 7 is also prime: at i=7 composite is false, count→4 (2,3,5,7). Starting the inner loop at i*i (not 2i) skips redundant marking.
🔬 Deeper dive & dry run
Why it works. The Sieve of Eratosthenes maintains the invariant "every composite ≤ the current limit has already been marked by the time we reach its smallest prime factor" — each prime p eliminates its own multiples the moment it's confirmed prime, and every composite's smallest prime factor is ≤ √n, so the sieve never has to "discover" a composite the hard way.
When to reach for it. Primality is needed for many numbers up to n, not just one — "count primes below n," precomputing a smallest-prime-factor table for fast factorization, GCD/LCM-heavy batch problems.
Complexity. Time O(n log log n) (sum of n/p over all primes p ≤ n). Space O(n) for the boolean array.
Pitfalls & variations. Off-by-one on inclusive vs. exclusive n; forgetting 0 and 1 aren't prime; starting the inner marking loop at 2p instead of p*p is still correct, just slower (multiples below p² are already marked by smaller primes); not capping the outer loop at √n is a performance miss, not a correctness bug. Variants: segmented sieve for huge n; linear sieve to build a smallest-prime-factor table in O(n).
Trace input: n = 10 — sieve to find all primes below 10 (check indices 2..9).
| Step | p | p prime? | Action | Remaining candidates after |
|---|---|---|---|---|
| 1 | 2 | yes | mark multiples 4, 6, 8 composite | 2, 3, 5, 7, 9 |
| 2 | 3 | yes | mark multiples 6 (already), 9 | 2, 3, 5, 7 |
| 3 | 4 | no (composite) | skip | 2, 3, 5, 7 |
| 4 | 5 | yes | no multiple of 5 below 10 left to mark | 2, 3, 5, 7 |
| 5 | 6 | no (composite) | skip | 2, 3, 5, 7 |
| 6 | 7 | yes | no multiple of 7 below 10 left to mark | 2, 3, 5, 7 |
| 7 | 8 | no (composite) | skip | 2, 3, 5, 7 |
| 8 | 9 | no (composite, marked at p=3) | skip | 2, 3, 5, 7 |
Result: primes below 10 = {2, 3, 5, 7}, count 4. Past p=3 (⌈√10⌉) no new numbers ever get marked — the trace shows the tail of the outer loop is pure overhead once p exceeds √n.
🧰 Quick reference templates
43. Number Theory
... Math tricks YES ▸ Number properties YES
Primes, divisors, GCD/LCM, modular arithmetic. The Sieve of Eratosthenes marks composites in O(n log log n); Euclid's GCD is O(log min).
int countPrimes(int n) { // primes strictly below n
if (n < 3) return 0;
boolean[] composite = new boolean[n];
int count = 0;
for (int i = 2; i < n; i++) {
if (composite[i]) continue;
count++;
for (long j = (long) i * i; j < n; j += i) composite[(int) j] = true;
}
return count;
}
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }int countPrimes(int n) { // primes strictly below n
if (n < 3) return 0;
std::vector<bool> composite(n, false);
int count = 0;
for (int i = 2; i < n; i++) {
if (composite[i]) continue;
count++;
for (long long j = (long long) i * i; j < n; j += i) composite[(int) j] = true;
}
return count;
}
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }def count_primes(n): # primes strictly below n
if n < 3:
return 0
composite = [False] * n
count = 0
for i in range(2, n):
if composite[i]:
continue
count += 1
for j in range(i * i, n, i):
composite[j] = True
return count
def gcd(a, b):
return a if b == 0 else gcd(b, a % b)function countPrimes(n: number): number { // primes strictly below n
if (n < 3) return 0;
const composite = new Array<boolean>(n).fill(false);
let count = 0;
for (let i = 2; i < n; i++) {
if (composite[i]) continue;
count++;
for (let j = i * i; j < n; j += i) composite[j] = true;
}
return count;
}
function gcd(a: number, b: number): number { return b === 0 ? a : gcd(b, a % b); }countPrimes(10) ⇒ 2,3,5,7 = 4. gcd(48,18) → gcd(18,12) → gcd(12,6) → 6.27 · Fenwick / Segment Tree
Flowchart leaf: dynamic range / order queries
The idea
When values change and you keep asking range aggregates (sum / min / kth), a plain prefix-sum array can't keep up (O(n) per update). A Fenwick tree (BIT) gives O(log n) point-update + prefix-query using the lowest-set-bit trick; a segment tree generalizes to any associative aggregate and range updates.
🧰 API — Fenwick (Binary Indexed) Tree methods
Use the language switcher (top bar) to see the equivalent methods in Java, C++, Python & TypeScript.
// Create
int[] tree = new int[n + 1]; // 1-indexed BIT array, all zeros initially
void update(int i, int delta); // add delta at index i (1-indexed), O(log n)
// for (; i <= n; i += i & -i) tree[i] += delta;
int prefixQuery(int i); // sum of [1..i], O(log n)
// for (; i > 0; i -= i & -i) sum += tree[i];
int rangeQuery(int l, int r); // sum of [l..r] = prefixQuery(r) - prefixQuery(l - 1), O(log n)
// Note: a Segment Tree solves the same problem for non-invertible ops (min/max/gcd) that this subtraction trick can't handle.
// Create
vector<int> tree(n + 1, 0); // 1-indexed BIT array
void update(int i, int delta); // add delta at index i, O(log n)
// for (; i <= n; i += i & -i) tree[i] += delta;
int prefixQuery(int i); // sum of [1..i], O(log n)
// for (; i > 0; i -= i & -i) sum += tree[i];
int rangeQuery(int l, int r); // sum of [l..r] = prefixQuery(r) - prefixQuery(l - 1), O(log n)
// Note: a Segment Tree solves the same problem for non-invertible ops (min/max/gcd) that this subtraction trick can't handle.
# Create
tree = [0] * (n + 1) # 1-indexed BIT array
def update(i: int, delta: int) -> None: # add delta at index i, O(log n)
while i <= n:
tree[i] += delta
i += i & -i
def prefix_query(i: int) -> int: # sum of [1..i], O(log n)
s = 0
while i > 0:
s += tree[i]
i -= i & -i
return s
def range_query(l: int, r: int) -> int: # sum of [l..r], O(log n)
return prefix_query(r) - prefix_query(l - 1)
# Note: a Segment Tree solves the same problem for non-invertible ops (min/max/gcd) that this subtraction trick can't handle.
// Create
const tree: number[] = new Array(n + 1).fill(0); // 1-indexed BIT array
function update(i: number, delta: number): void { // add delta at index i, O(log n)
for (; i <= n; i += i & -i) tree[i] += delta;
}
function prefixQuery(i: number): number { // sum of [1..i], O(log n)
let sum = 0;
for (; i > 0; i -= i & -i) sum += tree[i];
return sum;
}
function rangeQuery(l: number, r: number): number { // sum of [l..r] = prefixQuery(r) - prefixQuery(l - 1), O(log n)
return prefixQuery(r) - prefixQuery(l - 1);
}
// Note: a Segment Tree solves the same problem for non-invertible ops (min/max/gcd) that this subtraction trick can't handle.
Brute force — prefix array rebuild O(n) per update
Recomputing prefix sums after every change. The BIT localizes each update to O(log n) cells.
Template A · Fenwick tree (BIT) O(log n) update & query
t— the 1-indexed internal array; each slot stores the sum of a power-of-two-sized block of the original array.n— the number of elements the tree covers.i— the 1-indexed position being updated or queried;i & -iisolates its lowest set bit to jump to the next node.delta— the amount added to positioniinupdate.l,r— the inclusive bounds of the range whose sumrangecomputes.s— the running total accumulated while walking up (or down) the tree inprefix.- Returns —
prefix(i)gives the sum of elements1..i;range(l, r)gives the sum of elementsl..r.
class Fenwick { // 1-indexed
int[] t; int n;
Fenwick(int n){ this.n=n; t=new int[n+1]; }
void update(int i, int delta){ for (; i<=n; i += i&-i) t[i]+=delta; }
int prefix(int i){ int s=0; for (; i>0; i-=i&-i) s+=t[i]; return s; }
int range(int l, int r){ return prefix(r) - prefix(l-1); }
}class Fenwick { // 1-indexed
public:
std::vector<int> t; int n;
Fenwick(int n){ this->n=n; t.assign(n+1, 0); }
void update(int i, int delta){ for (; i<=n; i += i&-i) t[i]+=delta; }
int prefix(int i){ int s=0; for (; i>0; i-=i&-i) s+=t[i]; return s; }
int range(int l, int r){ return prefix(r) - prefix(l-1); }
};class Fenwick: # 1-indexed
def __init__(self, n):
self.n = n
self.t = [0] * (n + 1)
def update(self, i, delta):
while i <= self.n:
self.t[i] += delta
i += i & -i
def prefix(self, i):
s = 0
while i > 0:
s += self.t[i]
i -= i & -i
return s
def range(self, l, r):
return self.prefix(r) - self.prefix(l - 1)class Fenwick { // 1-indexed
t: number[]; n: number;
constructor(n: number) { this.n = n; this.t = new Array(n + 1).fill(0); }
update(i: number, delta: number): void { for (; i <= this.n; i += i & -i) this.t[i] += delta; }
prefix(i: number): number { let s = 0; for (; i > 0; i -= i & -i) s += this.t[i]; return s; }
range(l: number, r: number): number { return this.prefix(r) - this.prefix(l - 1); }
}Template B · segment tree (range sum, point update)
t— the array-based binary tree of size2n; leaves occupy indicesn..2n-1and each internal node stores the sum of its two children.n— the number of leaves (elements in the original array).i— inupdate, the leaf index being changed (before being shifted inton..2n-1).val— the new value written to leafi.l,r— the half-open query range[l, r), shifted bynto walk the tree from the leaves upward.s— the running sum accumulated whilequeryclimbs toward the root.- Returns —
query(l, r)gives the sum of the original array over indices[l, r)after any prior updates.
class SegTree {
int[] t; int n;
SegTree(int[] a){ n=a.length; t=new int[2*n];
for (int i=0;i<n;i++) t[n+i]=a[i];
for (int i=n-1;i>0;i--) t[i]=t[2*i]+t[2*i+1];
}
void update(int i, int val){ for (t[i+=n]=val; i>1; i>>=1) t[i>>1]=t[i]+t[i^1]; }
int query(int l, int r){ int s=0; // [l, r)
for (l+=n, r+=n; l<r; l>>=1, r>>=1){
if ((l&1)==1) s+=t[l++];
if ((r&1)==1) s+=t[--r];
} return s;
}
}class SegTree {
public:
std::vector<int> t; int n;
SegTree(std::vector<int>& a){ n=a.size(); t.assign(2*n, 0);
for (int i=0;i<n;i++) t[n+i]=a[i];
for (int i=n-1;i>0;i--) t[i]=t[2*i]+t[2*i+1];
}
void update(int i, int val){ for (t[i+=n]=val; i>1; i>>=1) t[i>>1]=t[i]+t[i^1]; }
int query(int l, int r){ int s=0; // [l, r)
for (l+=n, r+=n; l<r; l>>=1, r>>=1){
if ((l&1)==1) s+=t[l++];
if ((r&1)==1) s+=t[--r];
} return s;
}
};class SegTree:
def __init__(self, a):
self.n = len(a)
self.t = [0] * (2 * self.n)
for i in range(self.n):
self.t[self.n + i] = a[i]
for i in range(self.n - 1, 0, -1):
self.t[i] = self.t[2 * i] + self.t[2 * i + 1]
def update(self, i, val):
i += self.n
self.t[i] = val
while i > 1:
i >>= 1
self.t[i] = self.t[2 * i] + self.t[2 * i + 1]
def query(self, l, r): # [l, r)
s = 0
l += self.n
r += self.n
while l < r:
if l & 1 == 1:
s += self.t[l]
l += 1
if r & 1 == 1:
r -= 1
s += self.t[r]
l >>= 1
r >>= 1
return sclass SegTree {
t: number[]; n: number;
constructor(a: number[]) {
this.n = a.length;
this.t = new Array(2 * this.n).fill(0);
for (let i = 0; i < this.n; i++) this.t[this.n + i] = a[i];
for (let i = this.n - 1; i > 0; i--) this.t[i] = this.t[2 * i] + this.t[2 * i + 1];
}
update(i: number, val: number): void {
i += this.n;
this.t[i] = val;
for (; i > 1; i >>= 1) this.t[i >> 1] = this.t[i] + this.t[i ^ 1];
}
query(l: number, r: number): number { // [l, r)
let s = 0;
for (l += this.n, r += this.n; l < r; l >>= 1, r >>= 1) {
if ((l & 1) === 1) s += this.t[l++];
if ((r & 1) === 1) s += this.t[--r];
}
return s;
}
}Worked example — Fenwick on 6 slots
| op | effect | result |
|---|---|---|
| update(3,+5) | touches t[3],t[4] | index 3 holds 5 |
| update(5,+2) | touches t[5],t[6] | index 5 holds 2 |
| range(3,5) | prefix(5)−prefix(2)=7−0 | 7 |
| update(5,−2) | index 5 back to 0 | — |
| range(3,5) | prefix(5)−prefix(2)=5 | 5 |
Each query/update walks ≤ log₂6 ≈ 3 cells via the i & -i jumps.
🔬 Deeper dive & dry run
Why it works. A Fenwick (Binary Indexed) tree stores at index i the sum of a power-of-two-sized block ending at i, where the block size is i's lowest set bit. That invariant lets update climb via i += i & (-i) and query climb via i -= i & (-i), each touching only O(log n) nodes instead of rewriting or scanning O(n) of them.
When to reach for it. Point updates and prefix/range-sum queries are interleaved and need to beat O(n) recompute per query — "range sum query — mutable," inversion counting, frequency-rank queries (order statistics on a stream).
Complexity. Time O(log n) per update or prefix-sum query; O(n log n) to build via n sequential updates. Space O(n).
Pitfalls & variations. Fenwick trees are 1-indexed — calling update/query with index 0 breaks the i & (-i) climb; a range sum over [l, r] needs query(r) − query(l-1), not a single call; Fenwick relies on subtraction being valid, so it's a poor fit for range-min/max (use a segment tree there instead).
Trace input: a[1..6] = [3, 2, -1, 6, 5, 4] — build a Fenwick tree over 6 slots, then query the prefix sum(6).
| Step | Operation | Indices touched | tree[1..6] after |
|---|---|---|---|
| 1 | update(1, +3) | 1 → 2 → 4 | [3, 3, 0, 3, 0, 0] |
| 2 | update(2, +2) | 2 → 4 | [3, 5, 0, 5, 0, 0] |
| 3 | update(3, -1) | 3 → 4 | [3, 5, -1, 4, 0, 0] |
| 4 | update(4, +6) | 4 (4+4=8>6, stop) | [3, 5, -1, 10, 0, 0] |
| 5 | update(5, +5) | 5 → 6 | [3, 5, -1, 10, 5, 5] |
| 6 | update(6, +4) | 6 (6+2=8>6, stop) | [3, 5, -1, 10, 5, 9] |
| 7 | query(6) | 6 → 4 → 0 (stop) | sum = tree[6]+tree[4] = 9+10 = 19 |
Result: prefix sum(6) = 19, matching 3+2-1+6+5+4 = 19 directly. The trace shows update climbing by adding the lowest set bit and query climbing by subtracting it — mirror-image walks over the same tree.
🧰 Quick reference templates
10. Ordered Set / Fenwick / Segment Tree
Graph NO ▸ Sorted/monotonic YES ▸ Dynamic range/order queries YES
When values change and you keep asking range sums / kth / rank queries, a Fenwick (Binary Indexed) tree gives O(log n) point-update and prefix-sum. The i & -i trick isolates the lowest set bit to hop between responsibility ranges.
class Fenwick { // 1-indexed
int[] tree; int n;
Fenwick(int n) { this.n = n; tree = new int[n + 1]; }
void update(int i, int delta) {
for (; i <= n; i += i & (-i)) tree[i] += delta;
}
int prefix(int i) { // sum of [1..i]
int s = 0;
for (; i > 0; i -= i & (-i)) s += tree[i];
return s;
}
int range(int l, int r) { return prefix(r) - prefix(l - 1); }
}class Fenwick { // 1-indexed
public:
vector<int> tree; int n;
Fenwick(int n) : n(n) { tree.assign(n + 1, 0); }
void update(int i, int delta) {
for (; i <= n; i += i & (-i)) tree[i] += delta;
}
int prefix(int i) { // sum of [1..i]
int s = 0;
for (; i > 0; i -= i & (-i)) s += tree[i];
return s;
}
int range(int l, int r) { return prefix(r) - prefix(l - 1); }
};class Fenwick: # 1-indexed
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def update(self, i, delta):
while i <= self.n:
self.tree[i] += delta
i += i & (-i)
def prefix(self, i): # sum of [1..i]
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i)
return s
def range(self, l, r):
return self.prefix(r) - self.prefix(l - 1)class Fenwick { // 1-indexed
tree: number[];
n: number;
constructor(n: number) {
this.n = n;
this.tree = new Array(n + 1).fill(0);
}
update(i: number, delta: number): void {
for (; i <= this.n; i += i & (-i)) this.tree[i] += delta;
}
prefix(i: number): number { // sum of [1..i]
let s = 0;
for (; i > 0; i -= i & (-i)) s += this.tree[i];
return s;
}
range(l: number, r: number): number { return this.prefix(r) - this.prefix(l - 1); }
}range(3,5)=7. Then update(5,-2) ⇒ range(3,5)=5. Each call touches ≤ log n cells.28 · Specialized (Quickselect, Reservoir Sampling)
Flowchart leaf: specialized / advanced pattern
The idea
The catch-all for techniques that don't fit a common bucket. Two you should actually know: quickselect (kth element in average O(n), beating heap/sort) and reservoir sampling (uniform random pick from a stream of unknown length in O(1) memory).
Template A · quickselect — kth smallest avg O(n)
a— the array being searched for its k-th smallest value; it's reordered in place.k— the target 0-based rank (0 = smallest).lo,hi— the current search window withina.p— the final resting index of the pivot afterpartition, i.e. its rank among the current window.pivot— the value chosen to partition around (here,a[hi]).i— the boundary index; everything before it in the window is known to be less thanpivot.j— the scan index sweeping the window to find elements smaller thanpivot.- Returns — the value that would sit at index
kifawere fully sorted.
int quickselect(int[] a, int k){ // k is 0-based rank
int lo=0, hi=a.length-1;
while (lo < hi){
int p = partition(a, lo, hi);
if (p == k) return a[p];
else if (p < k) lo = p + 1;
else hi = p - 1;
}
return a[lo];
}
int partition(int[] a, int lo, int hi){
int pivot = a[hi], i = lo;
for (int j = lo; j < hi; j++) if (a[j] < pivot) swap(a, i++, j);
swap(a, i, hi); return i;
}int quickselect(vector<int>& a, int k){ // k is 0-based rank
int lo = 0, hi = (int)a.size() - 1;
while (lo < hi){
int p = partition(a, lo, hi);
if (p == k) return a[p];
else if (p < k) lo = p + 1;
else hi = p - 1;
}
return a[lo];
}
int partition(vector<int>& a, int lo, int hi){
int pivot = a[hi], i = lo;
for (int j = lo; j < hi; j++) if (a[j] < pivot) std::swap(a[i++], a[j]);
std::swap(a[i], a[hi]); return i;
}def quickselect(a, k): # k is 0-based rank
lo, hi = 0, len(a) - 1
while lo < hi:
p = partition(a, lo, hi)
if p == k:
return a[p]
elif p < k:
lo = p + 1
else:
hi = p - 1
return a[lo]
def partition(a, lo, hi):
pivot, i = a[hi], lo
for j in range(lo, hi):
if a[j] < pivot:
a[i], a[j] = a[j], a[i]
i += 1
a[i], a[hi] = a[hi], a[i]
return ifunction quickselect(a: number[], k: number): number { // k is 0-based rank
let lo = 0, hi = a.length - 1;
while (lo < hi) {
const p = partition(a, lo, hi);
if (p === k) return a[p];
else if (p < k) lo = p + 1;
else hi = p - 1;
}
return a[lo];
}
function partition(a: number[], lo: number, hi: number): number {
const pivot = a[hi];
let i = lo;
for (let j = lo; j < hi; j++) {
if (a[j] < pivot) {
[a[i], a[j]] = [a[j], a[i]];
i++;
}
}
[a[i], a[hi]] = [a[hi], a[i]];
return i;
}Template B · reservoir sampling — one uniform pick from a stream
stream— the sequence of elements to sample one uniformly-random pick from.result— the element currently held as the sample; it may be replaced as later elements are seen.count— how many elements have been seen so far, including the current one.x— the current element fromstream.rand— the random source used to decide, with probability1/count, whether to replaceresultwithx.- Returns — one element chosen uniformly at random from the whole stream, using O(1) memory regardless of stream length.
int pick(int[] stream){
Random rand = new Random();
int result = 0, count = 0;
for (int x : stream){
count++;
if (rand.nextInt(count) == 0) result = x; // keep x with prob 1/count
}
return result;
}int pick(vector<int>& stream){
std::random_device rd;
std::mt19937 gen(rd());
int result = 0, count = 0;
for (int x : stream){
count++;
std::uniform_int_distribution<int> dist(0, count - 1);
if (dist(gen) == 0) result = x; // keep x with prob 1/count
}
return result;
}def pick(stream):
result, count = 0, 0
for x in stream:
count += 1
if random.randrange(count) == 0:
result = x # keep x with prob 1/count
return resultfunction pick(stream: number[]): number {
let result = 0, count = 0;
for (const x of stream) {
count++;
if (Math.floor(Math.random() * count) === 0) result = x; // keep x with prob 1/count
}
return result;
}Worked example — reservoir uniformity, stream of 5
Element #3 is chosen when seen (prob 1/3), then must survive #4 (kept-out prob 3/4) and #5 (4/5):
P(keep #3) = 1/3 × 3/4 × 4/5 = 1/5// P(keep #3) = 1/3 × 3/4 × 4/5 = 1/5# P(keep #3) = 1/3 × 3/4 × 4/5 = 1/5// P(keep #3) = 1/3 × 3/4 × 4/5 = 1/5Every element ends with probability exactly 1/5 — uniform, using a single variable and one pass. Quickselect's average O(n) comes from discarding one partition side each round (n + n/2 + n/4 + … ≈ 2n).
🔬 Deeper dive & dry run
Why it works. Reservoir sampling maintains the invariant that after processing i elements, every element seen so far has probability exactly k/i of being in the reservoir. When element i+1 arrives, it's admitted with probability k/(i+1), and any existing occupant is displaced with that same probability — so the invariant renews itself inductively for every new element.
When to reach for it. Sampling uniformly from a stream of unknown or huge length using O(k) space — "pick a random node from a linked list of unknown length," "sample k lines from a massive file/stream you can't hold in memory."
Complexity. Time O(n): one pass, one random draw per element. Space O(k): the reservoir itself.
Pitfalls & variations. Drawing the random index over the wrong range — it must be [0, i] inclusive of i, not [0, k); replacing with the wrong probability (must be k/(i+1), not 1/(i+1)); assuming later elements are less likely to survive — they aren't, that's the entire point of the invariant. Quickselect uses a related "maintain an invariant partition" idea, but for order statistics via random pivots rather than uniform sampling.
Trace input: k = 1, stream = [A, B, C, D, E] — pick one element with uniform 1/5 probability each, using O(1) extra space.
| Step | i (0-indexed) | Incoming | Draw j in [0,i] | Replace? (j<k) | Reservoir after |
|---|---|---|---|---|---|
| 0 | 0 | A | — (seed) | — | A |
| 1 | 1 | B | j=0 | yes (0<1) | B |
| 2 | 2 | C | j=2 | no (2≥1) | B |
| 3 | 3 | D | j=0 | yes (0<1) | D |
| 4 | 4 | E | j=4 | no (4≥1) | D |
Result: the reservoir holds D. D's survival probability telescopes to exactly 1/5: it wins its own draw at i=3 with probability 1/4, then survives not being replaced at i=4 with probability 4/5 — 1/4 × 4/5 = 1/5, matching every other element's chance.
28 patterns · brute force → optimal · Java templates + traced examples. Print to PDF: File ▸ Print ▸ Save as PDF (each pattern starts on a new page).
🧰 Quick reference templates
18. Two Pointers / Partitioning
... Intervals NO ▸ In-place array partitioning YES
Rearrange in place by category (Dutch national flag, move-zeroes, quickselect pivot). Pointers from both ends (or low/mid/high) swap elements into their region in one pass, O(1) extra space.
void sortColors(int[] nums) { // 0s, 1s, 2s — Dutch flag
int low = 0, mid = 0, high = nums.length - 1;
while (mid <= high) {
if (nums[mid] == 0) swap(nums, low++, mid++);
else if (nums[mid] == 1) mid++;
else swap(nums, mid, high--); // 2 → back
}
}
void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }void sortColors(vector<int>& nums) { // 0s, 1s, 2s — Dutch flag
int low = 0, mid = 0, high = nums.size() - 1;
while (mid <= high) {
if (nums[mid] == 0) swap(nums, low++, mid++);
else if (nums[mid] == 1) mid++;
else swap(nums, mid, high--); // 2 → back
}
}
void swap(vector<int>& a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }def sort_colors(nums): # 0s, 1s, 2s — Dutch flag
low, mid, high = 0, 0, len(nums) - 1
while mid <= high:
if nums[mid] == 0:
swap(nums, low, mid); low += 1; mid += 1
elif nums[mid] == 1:
mid += 1
else:
swap(nums, mid, high); high -= 1 # 2 → back
def swap(a, i, j):
a[i], a[j] = a[j], a[i]function sortColors(nums: number[]): void { // 0s, 1s, 2s — Dutch flag
let low = 0, mid = 0, high = nums.length - 1;
while (mid <= high) {
if (nums[mid] === 0) swap(nums, low++, mid++);
else if (nums[mid] === 1) mid++;
else swap(nums, mid, high--); // 2 → back
}
}
function swap(a: number[], i: number, j: number): void { const t = a[i]; a[i] = a[j]; a[j] = t; }[2,0,2,1,1,0] → after the sweep [0,0,1,1,2,2], done in a single pass with three pointers.Strings: Dictionary & Pattern
44. Specialized / Advanced Pattern
... Math tricks NO (catch-all)
The bucket for problems that don't match anything above: reservoir sampling, randomized selection, suffix structures, segment-tree-beats, geometry, etc. Below is reservoir sampling — pick one element uniformly from a stream of unknown length in one pass, O(1) memory.
int pickRandom(int[] stream) { // uniform pick, single pass
Random rand = new Random();
int result = 0, count = 0;
for (int x : stream) {
count++;
if (rand.nextInt(count) == 0) result = x; // keep x with prob 1/count
}
return result;
}int pickRandom(std::vector<int>& stream) { // uniform pick, single pass
static std::mt19937 rand(std::random_device{}());
int result = 0, count = 0;
for (int x : stream) {
count++;
if (rand() % count == 0) result = x; // keep x with prob 1/count
}
return result;
}def pick_random(stream): # uniform pick, single pass
result, count = 0, 0
for x in stream:
count += 1
if random.randrange(count) == 0: # keep x with prob 1/count
result = x
return resultfunction pickRandom(stream: number[]): number { // uniform pick, single pass
let result = 0, count = 0;
for (const x of stream) {
count++;
if (Math.floor(Math.random() * count) === 0) result = x; // keep x with prob 1/count
}
return result;
}End of playbook — 44 leaf patterns. Print to PDF: File ▸ Print ▸ Save as PDF (each card avoids splitting across pages).
🔬 Deeper dive & dry run
Why it works. BFS explores the graph in strict layers: the FIFO queue guarantees that every node at distance d is dequeued before any node at distance d+1 is even enqueued. That is the invariant it maintains — "the queue never holds nodes from more than two consecutive distance layers." Because of this, the very first time you dequeue the target, the distance you popped it at is provably the shortest possible: no cheaper path could exist without having surfaced in an earlier layer.
When to reach for it. Any prompt with "fewest steps", "minimum moves", "shortest path" on an unweighted graph or an implicit state graph — grid mazes, word ladders, sliding puzzles, knight moves, multi-source rot/infection problems. If every transition costs exactly one unit, BFS's layer property gives you optimality for free.
Complexity. O(V + E) time (for a grid, O(rows × cols)) — each vertex is enqueued once and each edge examined once. O(V) space for the queue and the visited/distance array.
Pitfalls & variations. Marking a cell visited at dequeue time instead of at enqueue time lets the same cell be pushed multiple times, blowing up runtime. Forgetting bounds/wall checks before enqueueing crashes or corrupts the frontier. Multi-source BFS (seed the queue with several starts at once) and 0-1 BFS (deque instead of queue, for edge weights of 0 or 1) are the two most common variants interviewers escalate to.
Trace input: grid = [[0,0,1,0],[0,1,0,0],[0,0,0,0]] (0 = open, 1 = wall), start = (0,0), target = (2,3) — find the fewest 4-directional moves from start to target.
| Step | Popped | dist | Newly enqueued | Queue after | Note |
|---|---|---|---|---|---|
| 0 | — | — | (0,0) d=0 | [(0,0)] | seed queue with start, dist[0][0]=0 |
| 1 | (0,0) | 0 | (0,1) d=1, (1,0) d=1 | [(0,1),(1,0)] | up/left out of bounds |
| 2 | (0,1) | 1 | — | [(1,0)] | (0,2) & (1,1) are walls |
| 3 | (1,0) | 1 | (2,0) d=2 | [(2,0)] | (1,1) is a wall |
| 4 | (2,0) | 2 | (2,1) d=3 | [(2,1)] | bottom row now, row/col bounds hit |
| 5 | (2,1) | 3 | (2,2) d=4 | [(2,2)] | (1,1) wall blocks going up |
| 6 | (2,2) | 4 | (2,3) d=5, (1,2) d=5 | [(2,3),(1,2)] | (2,3) is the target — but not yet popped |
| 7 | (2,3) | 5 | — | [(1,2)] | target dequeued → stop, distance is final |
Result: shortest distance = 5, via (0,0)→(1,0)→(2,0)→(2,1)→(2,2)→(2,3). The trace shows the queue's distances are always non-decreasing — that ordering is exactly why the first pop of the target is optimal.