DSA Prep · Interactive Guide 0/0
DSA Interview Prep — all-in-one 1 · Pattern Flowchart 2 · In-Depth Guide 3 · LeetCode by Pattern

🧭 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 →

How to read the chart. Start at the top question. Follow YES / NO until you hit a blue leaf. Most questions have both branches; a few side-questions only show a YES exit — if the answer is NO, drop back to the main vertical spine and continue to the next question. Explanations and Java below are my own concise versions written for study, not copied from the source.
Common definitions used in the snippets.
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)

Pattern Reference (one card per blue leaf)

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

  1. Put the source in the queue and mark it visited.
  2. Pop the front node. It is now "settled" — its distance is final.
  3. Push every unvisited neighbour, marking each visited at push time (not pop time — otherwise a node can enter the queue twice).
  4. 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 = True
const 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)

What each variable means
  • root — top node of the tree; if it's null/None the 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 res
function 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

What each variable means
  • grid — the maze as a 2-D array. Each cell is 0 = walkable (open) or 1 = blocked (wall).
  • start — a 2-element array {row, col}: where you begin. (This was the cryptic s.)
  • target — a 2-element array {row, col}: where you want to reach. (This was the cryptic t.)
  • 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 start to target, or -1 if 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 -1
function 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)

What each variable means
  • 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 loop

Template D · state-space BFS (states are strings/ints, edges are moves)

What each variable means
  • 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 from cur via one legal move.
  • Returns — the minimum number of moves from start to target, or -1 if target is 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 -1
function 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 0
0 0 1
1 0 0
1 1 0
0 0 1
1 0 0
1 1 0
0 0 1
1 0 0
1 1 0
PopDistNew neighbours pushedQueue 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.

Variants worth knowing: 0-1 BFS — edges weigh 0 or 1; use a 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.

Pitfalls: marking visited at pop time instead of push time lets duplicates pile up and can blow the complexity. BFS gives shortest paths only when every edge has the same weight — the moment weights differ, switch to Dijkstra (§6).

🧰 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.

Java
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 res
function 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;
}
Worked example
Tree 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.

Java
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 -1
function 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;
}
Worked example
3×3 all-open grid, start (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.

Java
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 -1
function 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;
}
Worked example
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.

#ProblemDiffCompaniesSub-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 == numCourses
class 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?

StepPopped from queueProcessed so farIn-degree updatesQueue after
0init[]indegree = [0,1,1,2,1][0]
10[0]course1: 1→0 (push); course2: 1→0 (push)[1,2]
21[0,1]course3: 2→1 (not pushed)[2]
32[0,1,2]course3: 1→0 (push)[3]
43[0,1,2,3]course4: 1→0 (push)[4]
54[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

  1. Visit the current node (mark it visited in a graph).
  2. For each neighbour/child, if unvisited, recurse into it.
  3. 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)

What each variable means
  • 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 parent
let 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)

What each variable means
  • 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 — numIslands returns the total island count; sink returns 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)

What each variable means
  • src — the node to start the traversal from.
  • adj — the adjacency list; adj[u] lists the neighbors of node u.
  • 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 of u being 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 1
1 1 0
0 1 0
0 0 1
1 1 0
0 1 0
0 0 1
1 1 0
0 1 0
0 0 1
  1. (0,0) is land → count=1, flood sinks (0,0),(0,1),(1,1) into water.
  2. Scan continues; every cell of that blob is now '0', so it is not recounted.
  3. (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.

Pitfalls: recursion depth on a 10⁶-node path graph will overflow the stack — go iterative. On graphs, forgetting 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.

StepActionStackVisited (new)IslandsNote
0outer scan hits (0,0)=1, unvisited[(0,0)]—0launch DFS #1
1pop (0,0), push land neighbors[(1,0),(0,1)](0,0)0down & right are land
2pop (0,1)[(1,0)](0,1)0(0,2) and (1,1) are water
3pop (1,0)[](1,0)0(1,1) water, (2,0) water → stack empty
4island #1 closed, size 3[]—1{(0,0),(0,1),(1,0)}
5outer scan hits (1,3)=1, unvisited[(1,3)]—1launch DFS #2
6pop (1,3), push down neighbor[(2,3)](1,3)1(0,3) water, left (1,2) water
7pop (2,3), push down & left[(3,3),(2,2)](2,3)1both (3,3) and (2,2) are land
8pop (2,2)[(3,3)](2,2)1up/down/left all water
9pop (3,3)[](3,3)1left (3,2) is water → stack empty
10island #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.

Java
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);
}
Worked example
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.

Java
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 choice
function 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
    }
}
Worked example
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.

#ProblemDiffCompaniesSub-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).

CallBounds (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?

StepPopped call (node, low, high)CheckResultPushed children
0init——stack = [(5, -∞, +∞)]
1(5, -∞, +∞)-∞ < 5 < +∞truepush (8, 5, +∞), push (3, -∞, 5)
2(3, -∞, 5)-∞ < 3 < 5truepush (6, 3, 5), push (1, -∞, 3)
3(1, -∞, 3)-∞ < 1 < 3true (leaf)nothing to push
4(6, 3, 5)3 < 6 < 5 → 6 < 5 is falseINVALID — 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

What each variable means
  • s — the state (partial candidate) being built up one choice at a time.
  • c — one candidate choice, drawn from choicesFrom(s), being tried at this step.
  • isComplete(s) — tells whether s is a full, valid answer worth recording.
  • valid(c, s) — the pruning check — is choice c even legal given the state built so far.
  • Returns — nothing directly; complete candidates are captured via record(s), and every applied choice is undone so s is 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-choose
function 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)

What each variable means
  • 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 once i reaches the end.
  • Returns — nothing directly; every subset is appended into out as 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)

What each variable means
  • nums — the input array being permuted.
  • cur — the permutation built so far along this branch.
  • used — marks which elements of nums are already placed in cur, 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] = False
function 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

What each variable means
  • 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 from free as its lowest set bit.
  • Returns — the number of ways to complete a valid board from row onward; 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 count
function 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.

Pruning ideas: sort first and skip duplicates (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.

StepstartpathActionresult so far
10[]record[[]]
21[1]choose nums[0]=1, record[[], [1]]
32[1,2]choose nums[1]=2, record+[1,2]
43[1,2,3]choose nums[2]=3, record+[1,2,3]
53[1,3]pop 2 & 3, choose nums[2]=3, record+[1,3]
62[2]pop back to [], choose nums[1]=2, record+[2]
73[2,3]choose nums[2]=3, record+[2,3]
83[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.

Java
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 res
function 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
    }
}
Worked example
[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.

#ProblemDiffCompaniesSub-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()  # undo
class 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.

StepCall (start, path)ActionResult so far
0backtrack(0, [])record path[ [] ]
1backtrack(1, [1])chose i=0→1; record path[ [], [1] ]
2backtrack(2, [1,2])chose i=1→2; record path[ …, [1,2] ]
3backtrack(3, [1,2,3])chose i=2→3; record path; start=3=n, no choices; return; pop 3[ …, [1,2,3] ]
4back in backtrack(2,[1,2])loop exhausted; return; pop 2 → path=[1]unchanged
5backtrack(1,[1]) → backtrack(3,[1,3])chose i=2→3; record path[ …, [1,3] ]
6back in backtrack(1,[1])no more choices; return; pop 1 → path=[]unchanged
7backtrack(0,[]) → backtrack(2,[2])chose i=1→2; record path[ …, [2] ]
8backtrack(2,[2]) → backtrack(3,[2,3])chose i=2→3; record path[ …, [2,3] ]
9back in backtrack(2,[2])return; pop 3, pop 2 → path=[]unchanged
10backtrack(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

What each variable means
  • 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 values 1..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 res
function 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)

What each variable means
  • n — the number of nodes in the tree.
  • dp[k] — the number of structurally unique BSTs that can be built from k nodes.
  • 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 size root-1 and a right subtree of size nodes-root.
  • Returns — dp[n], the total count of unique BST shapes possible with n nodes.
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

RootLeft rangeRight 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.

StepNodeleftHrightHdiam so farheight returned
170001
241012
350011
422133
560031
630132
71 (root)3254

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).

Java
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 res
function 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;
}
Worked example
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

  1. Compute in-degree (number of incoming edges) for every node.
  2. Queue all nodes with in-degree 0 — they depend on nothing.
  3. Pop one, append to the order, and decrement each successor's in-degree; if a successor hits 0, queue it.
  4. 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)

What each variable means
  • n — the number of nodes.
  • edges — the directed edges, each meaning "must come before".
  • adj — adjacency list built from edges.
  • 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 into order so far.
  • Returns — the full topological order if all n nodes 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 ⇒ cycle
function 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.

StepIn-degreesQueueEmit
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.)

Pitfall: to detect an impossible schedule you must check the emitted count, not assume success — a cycle silently leaves nodes stuck at in-degree > 0.

🔬 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.

StepPoppedindegree[0..3]Queue afterOrder so far
0—[0,1,1,2][0][]
10[0,0,0,2][1,2][0]
21[0,0,0,1][2][0,1]
32[0,0,0,0][3][0,1,2]
43[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.

Java
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 detected
function 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
}
Worked example
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

  1. dist[src]=0, everything else ∞; push (src,0).
  2. Pop the smallest-distance entry. If it's stale (d > dist[u]) skip it.
  3. Relax each edge u→v: if dist[u]+w < dist[v], improve dist[v] and push the new pair.
  4. 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)

What each variable means
  • adj — weighted adjacency list; adj[u] holds each neighbor v and edge weight w.
  • src — the starting node.
  • dist — the best (smallest) distance found so far from src to each node.
  • pq — a min-heap of (node, distance) pairs, always expanding the currently-closest unvisited node next.
  • u, d — the node popped from pq and 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 of u and the weight of the edge to it.
  • Returns — an array of shortest distances from src to 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 dist
function 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)Relaxationsdist 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) stale4>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.

Family map: all-same-weight → BFS (§1); weights ∈ {0,1} → 0-1 BFS deque; non-negative weights → Dijkstra; negative edges → Bellman-Ford; all-pairs → Floyd-Warshall O(V³).

🔬 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.

StepPop (node,dist)RelaxationsPQ afterdist[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) — staleskip, 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) — staleskip, 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.

Java
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 dist
function 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;
}
Worked example
Edges 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.

#ProblemDiffCompaniesSub-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 maxDist
class 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.

StepActionNode finalizeddist[1,2,3,4]Heap after
0init—[∞, 0, ∞, ∞][(0,2)]
1pop (0,2)node 2, dist=0[∞, 0, ∞, ∞][]
2relax 2→1 (w=1): 0+1=1 < ∞—[1, 0, ∞, ∞][(1,1)]
3relax 2→3 (w=1): 0+1=1 < ∞—[1, 0, 1, ∞][(1,1),(1,3)]
4pop (1,1)node 1, dist=1 (no outgoing edges)[1, 0, 1, ∞][(1,3)]
5pop (1,3)node 3, dist=1[1, 0, 1, ∞][]
6relax 3→4 (w=1): 1+1=2 < ∞—[1, 0, 1, 2][(2,4)]
7pop (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

What each variable means
  • parent — for each node, a pointer toward its set's representative ("root"); parent[x] == x means x is 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 to x's root, compressing the path along the way so future lookups are faster.
  • ra, rb — the set representatives (roots) of a and b, compared to decide whether they're already connected.
  • Returns — find returns the representative (root) of a node's set; union returns false if a and b were already in the same set (no-op), true if 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 True
class 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):

Unionparent[] (roots)count
start0 1 2 3 45
(0,1)0 0 2 3 44
(1,2)0 0 0 3 43
(3,4)0 0 0 3 32

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.

α(n) is the inverse Ackermann function — ≤ 4 for any n you'll ever see, so treat DSU ops as effectively constant.

🔬 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).

StepPair (i,j)matrix[i][j]Actionparent[0..4]Provinces
0——init[0,1,2,3,4]5
1(0,1)1union — root(0)=0, root(1)=1, differ[0,0,2,3,4]4
2(0,2)0skip[0,0,2,3,4]4
3(0,3)0skip[0,0,2,3,4]4
4(0,4)0skip[0,0,2,3,4]4
5(1,2)1union — root(1)=0, root(2)=2, differ[0,0,0,3,4]3
6(1,3)0skip[0,0,0,3,4]3
7(1,4)0skip[0,0,0,3,4]3
8(2,3)0skip[0,0,0,3,4]3
9(2,4)0skip[0,0,0,3,4]3
10(3,4)1union — 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.

Java
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 True
class 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;
    }
}
Worked example
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)

What each variable means
  • 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 none
function 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)

What each variable means
  • 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 within h hours at this speed?
  • hours — the total hours feasible computes it would take at the candidate speed.
  • Returns — the smallest eating speed that still finishes all piles within h hours.
// 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]:

lohimidhours at midfeasible?move
11161+1+2+2=6yeshi=6
1631+2+3+4=10nolo=4
4651+2+2+3=8yeshi=5
4541+2+2+3=8yeshi=4
44lo==hi → answer 4

Answer 4. Note we never enumerate speeds — 4 probes cover the whole range 1..11.

Pitfalls: off-by-one is the whole game. Pin down whether your interval is [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.

Steplohimidhours(mid)feasible?Action
0111———search space = candidate speeds 1..max(piles)
111161+1+2+2=6yes (6≤8)hi = 6
21631+2+3+4=10no (10>8)lo = 4
34651+2+2+3=8yes (8≤8)hi = 5
44541+2+2+3=8yes (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.

Java
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)
}
Worked example
[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).

Java
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 h
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 (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;
}
Worked example
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".

#ProblemDiffCompaniesSub-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 -1
class 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, hinums[lo..hi] viewsorted halfdecision
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]==targetreturn 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.

Steplohimidnums[mid]which half sortedtarget in range?action
008————start
10849nums[0]=5 <= 9 → left sorted3 in [5,9]? nolo = mid+1 = 5
25861nums[5]=10 <= 1? no → right sorted3 in [1,3]? yeslo = mid+1 = 7
37872nums[7]=2 <= 2 → left sorted3 in [2,2]? nolo = mid+1 = 8
48883—nums[mid] == targetreturn 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)

What each variable means
  • 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 size k, always holding the k largest values seen so far.
  • x — the current element from nums being inserted into the heap.
  • Returns — the smallest value among the k largest — i.e. the kth largest element overall (the top of the min-heap once it settles at size k).
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 overall
function 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)

What each variable means
  • 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 at dummy.next.
  • tail — a pointer to the last node appended so far in the merged output.
  • n — the smallest node just popped from pq; 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.next
function 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

What each variable means
  • 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 by add.
  • funnel/rebalance step — every new value goes into lo first, then its max is pushed to hi, then sizes are rebalanced so lo never holds more than one extra element over hi.
  • Returns — add returns nothing (it just updates the two heaps); median() returns the median of every number added so far, taken from the top(s) of lo/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.0
const 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

ReadHeap (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.

StepElementHeap beforeActionHeap afterSize
17[]push (under capacity)[7]1
210[7]push (now at capacity)[7,10]2
34[7,10]push then pop-min(4)[7,10]2
43[7,10]push then pop-min(3)[7,10]2
520[7,10]push then pop-min(7)[10,20]2
615[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.

Java
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 largest
function 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
}
Worked example
[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.

#ProblemDiffCompaniesSub-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 processedHeap 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
Resultheap = [5,6], top = 55 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.

Stepnumheap before (min-heap)actionheap after
0—[]init[]
13[]push 3 (size 1 ≤ k)[3]
22[3]push 2 (size 2 = k)[2,3]
31[2,3]push 1 → size 3 > k, pop min 1[2,3]
45[2,3]push 5 → size 3 > k, pop min 2[3,5]
56[3,5]push 6 → size 3 > k, pop min 3[5,6]
64[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)

What each variable means
  • a — the sorted (ascending) array being searched
  • target — the sum the two chosen elements must add up to
  • lo — pointer starting at the left, smallest-value end
  • hi — pointer starting at the right, largest-value end
  • sum — 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

What each variable means
  • head — the first node of the linked list
  • slow — pointer that advances one node per step
  • fast — 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 False
function 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)

What each variable means
  • a — array containing only the values 0, 1, and 2, rearranged in place
  • low — boundary marking where the block of 0s ends
  • mid — index of the element currently being classified
  • high — 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 -= 1
function 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

What each variable means
  • a, b — two sorted arrays whose elements are paired up
  • target — the maximum sum a pair is allowed to have
  • j — pointer into b that only ever moves left, marking the largest b-index that still keeps a[i] + b[j] within target
  • count — 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 count
function 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]

lohia[lo]+a[hi]vs 9move
032+15=17>hi→2
022+11=13>hi→1
012+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.

Finding the cycle's entry: after fast/slow meet, reset one pointer to the head and advance both at 1x — they meet at the loop start (Floyd's second phase).

🔬 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.

Steplrnums[l]nums[r]sumAction
109-61155 < 9 → l++
219-41177 < 9 → l++
329-1111010 > 9 → r--
428-1988 < 9 → l++
5380999 == 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).

Java
// 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;
}
Worked example
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.

Java
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];
}
Worked example
[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.

Java
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 best
function 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;
}
Worked example
[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".

#ProblemDiffCompaniesSub-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 res
class 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, hisumaction
i=0 (-4)lo=1(-1), hi=5(2)-3sum<0, lo++ ... no triplet found for -4
i=1 (-1)lo=2(-1), hi=5(2)0record [-1,-1,2], then lo++, hi--
i=1 (-1)lo=3(0), hi=4(1)0record [-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).

StepilrsumAction
0sort———sorted = [-4,-1,-1,0,1,2]
10 (-4)15-4-1+2=-3-3<0 → l++
20 (-4)25-4-1+2=-3-3<0 → l++
30 (-4)35-4+0+2=-2-2<0 → l++
40 (-4)45-4+1+2=-1-1<0 → l++ → l==r, done with i=0
51 (-1)25-1-1+2=0match [-1,-1,2] → l++, r--
61 (-1)34-1+0+1=0match [-1,0,1] → l++, r-- → l>r, done with i=1
72 (-1)———duplicate of nums[1] → skip i
83 (0)450+1+2=33>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

What each variable means
  • left — start index of the current window
  • right — end index of the current window, extended one element at a time
  • add() — placeholder step: fold a[right] into the window's tracked state
  • invalid() — placeholder check: does the window currently break the required property?
  • remove() — placeholder step: drop a[left] from the window's tracked state while shrinking
  • best — 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 here
let 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)

What each variable means
  • s — the input string being scanned
  • last — array mapping a character to the most recent index where it was seen (-1 if never)
  • left — start of the current repeat-free window
  • right — end of the current window, the character just examined
  • c — the character at position right
  • best — 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 best
function 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

What each variable means
  • a — the input array
  • k — the fixed number of elements the window must contain
  • sum — running total of the current k-element window
  • best — 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 best
function 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"

What each variable means
  • a — the input array
  • k — the maximum number of distinct values a window may contain
  • freq — map from a value to how many times it currently appears in the window
  • left — start of the window, pushed forward whenever there are too many distinct values
  • right — end of the window, the element just added
  • v — the value being dropped from the window as it shrinks
  • count — 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"

rightcharleft before → afterwindowbest
0a0→0a1
1b0→0ab2
2c0→0abc3
3a0→1bca3
4b1→2cab3
5c2→3abc3
6b3→5cb3
7b5→7b3

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.

rightcharlastSeen beforeleftwindowlenbest
0a-0[0,0]11
1b-0[0,1]22
2c-0[0,2]33
3a0 (>=left)1[1,3]33
4b1 (>=left)2[2,4]33
5c2 (>=left)3[3,5]33
6b4 (>=left)5[5,6]23
7b6 (>=left)7[7,7]13

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.

Java
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 best
function 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;
}
Worked example
"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".

#ProblemDiffCompaniesSub-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 best
class 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":

rightcharwindow beforeactionbest
0a{}add a1
1b{a}add b2
2c{a,b}add c3
3a{a,b,c}duplicate: evict a (left=1), then add a3
4b{b,c,a}duplicate: evict b (left=2), then add b3

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.

StepicharlastSeen[char]left beforeactionleft afterwindowmaxLen
0————init0""0
10anone0record a@00"a"1
21bnone0record b@10"ab"2
32cnone0record c@20"abc"3
43a000 >= left(0) → left=1; record a@31"bca"3
54b111 >= left(1) → left=2; record b@42"cab"3
65c222 >= left(2) → left=3; record c@53"abc"3
76b434 >= left(3) → left=5; record b@65"cb"3
87b656 >= left(5) → left=7; record b@77"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)

What each variable means
  • t — array of temperatures, one per day
  • res — output array; res[i] becomes the number of days until a warmer temperature
  • st — stack of day indices still waiting for their next-warmer day, kept temperature-decreasing from bottom to top
  • i — the day currently being processed
  • j — 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 0
function 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)

What each variable means
  • h — array of bar heights making up the histogram
  • st — stack of bar indices whose rectangle isn't closed yet, kept height-increasing from bottom to top
  • i — the bar currently being processed; the loop runs one extra step past the end as a sentinel
  • cur — height of the current bar, or 0 on the sentinel step so the stack fully empties
  • height — height of the bar popped off st — the shortest bar in the rectangle being closed
  • width — how many bars wide that rectangle spans
  • best — 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 best
function 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.

itempstack beforepops (idx→ans)stack after
073[]-[0]
174[0]0→1[1]
275[1]1→1[2]
371[2]-[2,3]
469[2,3]-[2,3,4]
572[2,3,4]4→1, 3→2[2,5]
676[2,5]5→1, 2→4[6]
773[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).

Java
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 res
function 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;
}
Worked example
[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.

Java
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 best
function 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;
}
Worked example
[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)

What each variable means
  • nums — the input array
  • k — the target sum a subarray must add up to
  • seen — map from a prefix-sum value to how many times it has occurred, seeded with sum 0 occurring once for the empty prefix
  • sum — running prefix sum through the current element
  • x — the element currently being folded into sum
  • count — 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 count
function 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))

What each variable means
  • a — the input 2D matrix
  • m, n — number of rows and columns in a
  • P — 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 queried
  • sum — 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))

What each variable means
  • 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) update
  • run — 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 a
const 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]

xrunning sumneed sum−k = sum−2seen count of itcountmap after
—0——0{0:1}
11-100{0:1,1:1}
12011{0:1,1:1,2:1}
13112{…,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.

jnumprefixlookup key (prefix-k)count foundansfreq map after
--0--0{0:1}
011-100{0:1,1:1}
112011{0:1,1:1,2:1}
213112{0:1,1:1,2:1,3:1}
303113{0:1,1:1,2:1,3:2}
414214{0:1,1:1,2:1,3:2,4:1}
515326{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.

Java
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 count
function 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;
}
Worked example
[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:

  1. State: what does dp[i] mean, in one precise sentence?
  2. Transition: how is dp[i] built from smaller states?
  3. Base case: the smallest states you can answer directly.
  4. Order: fill so every state's dependencies are ready first.
  5. Answer: which state (or aggregate) is the result?

Brute force — naive recursion exponential

What each variable means
  • c — array of available coin denominations
  • amt — amount still needed to reach exactly 0
  • coin — one denomination being tried as the next coin to use
  • sub — result of recursing on the smaller remaining amount (amt - coin), or -1 if that's impossible
  • best — 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 best
function 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)

What each variable means
  • coins — array of available coin denominations
  • amount — the target amount to make
  • dp — dp[a] holds the fewest coins needed to make amount a, started at a large "infinity" sentinel
  • a — the amount currently being solved, built up from 1 to amount
  • c — 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)

What each variable means
  • amount — the target amount to make
  • coins — array of available coin denominations
  • dp — dp[a] holds the number of distinct combinations of coins that sum to a
  • c — the coin denomination in the outer loop — iterating coins first keeps combinations order-independent
  • a — 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)

What each variable means
  • w — array of item weights
  • v — array of item values, parallel to w
  • W — total knapsack capacity
  • dp — dp[c] holds the best total value achievable using capacity c so far
  • i — index of the item currently being considered
  • c — 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)

What each variable means
  • a — the input array of numbers, which may include negatives
  • cur — the best sum of a subarray that must end at the current index
  • best — 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 best
function 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)

What each variable means
  • 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 back
  • prev1 — number of ways to decode the string up to one character back
  • cur — 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 prev2
  • two — 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 prev1
function 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

acandidates dp[a-c]+1dp[a]
0base0
1dp[0]+11
2dp[1]+1=2, dp[0]+1=11
3dp[2]+1=2, dp[1]+1=22
5dp[4]+1=3, dp[3]+1=3, dp[0]+1=11
6dp[5]+1=2, dp[4]+1=3, dp[1]+1=22
10dp[5]+1=2 …2
11dp[10]+1=3, dp[9]+1=4, dp[6]+1=33

Answer 3 coins (5+5+1). Each dp[a] is computed once and reused — that reuse is the entire speedup over the recursion above.

Combinations vs permutations: in counting problems, coin-outer/amount-inner counts unordered combos; swapping the loops counts ordered sequences. Getting this backwards is the most common DP counting bug.

🔬 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.

icandidates (dp[i-c]+1)dp[i]
0base case0
1dp[0]+1=11
2dp[1]+1=22
3dp[2]+1=3, dp[0]+1=11
4dp[3]+1=2, dp[1]+1=2, dp[0]+1=11
5dp[4]+1=2, dp[2]+1=3, dp[1]+1=22
6dp[5]+1=3, dp[3]+1=2, dp[2]+1=32

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).

Java
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 best
function 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;
}
Worked example
[-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.

Java
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].
Worked example
[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.

Java
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];
}
Worked example
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.

Java
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 prev1
function 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;
}
Worked example
"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.

#ProblemDiffCompaniesSub-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:

a01234567891011
dp[a]011221223323

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.

ivia coin 1 → dp[i-1]+1via coin 3 → dp[i-3]+1via coin 4 → dp[i-4]+1dp[i]Note
0———0base case, 0 coins for amount 0
1dp[0]+1=1n/a (1<3)n/a (1<4)1only coin 1 fits
2dp[1]+1=2n/an/a2two 1's
3dp[2]+1=3dp[0]+1=1n/a1coin 3 alone beats three 1's
4dp[3]+1=2dp[1]+1=2dp[0]+1=11coin 4 alone wins
5dp[4]+1=2dp[2]+1=3dp[1]+1=221+4 or 1+1+3, tie at 2 coins
6dp[5]+1=3dp[3]+1=2dp[2]+1=32coin 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)

What each variable means
  • a, b — the two input strings being compared.
  • m, n — lengths of a and b.
  • dp[i][j] — length of the longest common subsequence using the first i chars of a and first j chars of b.
  • i, j — 1-indexed scan positions into a and b (so the actual chars are a[i-1], b[j-1]).
  • Returns — the length of the longest common subsequence shared by a and b.
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)

What each variable means
  • a, b — the source string and the target string to transform it into.
  • m, n — lengths of a and b.
  • dp[i][j] — minimum edits (insert/delete/replace) to turn the first i chars of a into the first j chars of b.
  • 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 a into b.
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)

What each variable means
  • s — the source string you pick characters from.
  • t — the target string you are trying to spell out as a subsequence.
  • n — length of t.
  • dp[j] — number of ways to form the first j characters of t using the part of s scanned 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 out t.
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)

What each variable means
  • 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 prefix s[0..i) can be fully split into dictionary words.
  • j — a candidate split point tried for each i, checking whether s[j..i) is itself a dictionary word.
  • Returns — whether the entire string s can 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)

What each variable means
  • 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 length k+1 found so far.
  • size — length of the longest increasing subsequence found so far.
  • x — the current number from nums being placed into tails.
  • lo, hi, mid — binary-search bounds used to find where x belongs in tails.
  • 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 size
function 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  4

Bottom-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.

is1[i]dp row (j=1..6, vs B,D,C,A,B,A)
0-0,0,0,0,0,0
1A0,0,0,1,1,1
2B1,1,1,1,2,2
3C1,1,2,2,2,2
4B1,1,2,2,3,3
5D1,2,2,2,3,3
6A1,2,2,3,3,4
7B1,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).

Java
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];
}
Worked example
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.

#ProblemDiffCompaniesSub-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""ace
""0000
a0111
b0111
c0122
d0122
e0123

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.

itext1[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
1a[0, 1, 1, 1]'a'=='a' at j=1 → dp[0][0]+1=1; then max-carries right
2b[0, 1, 1, 1]'b' matches nothing new → row copies previous row exactly
3c[0, 1, 2, 2]'c'=='c' at j=2 → diagonal dp[2][1]+1 = 1+1 = 2
4d[0, 1, 2, 2]'d' matches nothing new → row copies previous row
5e[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

What each variable means
  • 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 bit i set, 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 bit
mask & (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 bit
mask & (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 bit
mask & (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 bit

Brute 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²)

What each variable means
  • dist — the distance/cost matrix between every pair of nodes.
  • n — number of nodes (cities/tasks) to visit.
  • FULL — the bitmask with all n bits set, meaning "every node has been visited".
  • dp[mask][i] — cheapest cost of a path that has visited exactly the nodes in mask and currently ends at node i.
  • mask — bitmask of nodes visited so far on a given path.
  • j — a candidate next node to move to (must not already be set in mask).
  • nm — the new mask after adding node j to mask.
  • 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)meaningkey values
001,010,100single start nodedp=0 at that node
011 = {0,1}visited 0&1dp[011][1]=1 (0→1)
111 = {0,1,2}all visiteddp[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]=100→1
{2}dp[2][2]=150→2
{4}dp[4][3]=200→3
{1,2}dp[3][1]=50, dp[3][2]=452→1, 1→2
{1,4}dp[5][1]=45, dp[5][3]=353→1, 1→3
{2,4}dp[6][2]=50, dp[6][3]=453→2, 2→3
{1,2,4}dp[7][1]=70, dp[7][2]=65, dp[7][3]=75best of predecessor masks + edge
close tourmin(70+10, 65+15, 75+20) = 80best: 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.

Java
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;
}
Worked example
4 cities, mask ranges 0…15. 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.

Java
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];
}
Worked example
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)

What each variable means
  • 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 count
function 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

What each variable means
  • a — array where a[i] is the farthest number of steps you can jump from index i.
  • 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 jumps
function 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]:

Meetingstart ≥ end?take?end aftercount
[1,3]1 ≥ −∞yes31
[2,4]2 ≥ 3?no31
[3,5]3 ≥ 3?yes52
[0,6]0 ≥ 5?no52
[5,7]5 ≥ 5?yes73

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).

Pitfall: greedy on the wrong key silently gives near-right answers. For "max non-overlapping" sort by end; sorting by start or by length is a classic wrong turn.

🔬 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.

intervallastEnd beforestart >= lastEnd?decisionlastEnd after
A(1,3)-∞yestake3
B(2,5)3no (2<3)skip3
C(4,6)3yestake6
D(6,7)6yestake7
E(5,8)7no (5<7)skip7
F(7,9)7yestake9
G(8,10)9no (8<9)skip9

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.

Java
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 jumps
function 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;
}
Worked example
[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.

#ProblemDiffCompaniesSub-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 best
class 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]:

i012345678
nums[i]-21-34-121-54
curSum-21-2435615
best-211445666

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.

inums[i]cur + nums[i]cur = max(nums[i], cur+nums[i])bestNote
0-2—-2-2seed both with first element
11-2+1=-111restart beats extending negative run
2-31-3=-2-21extending still beats starting fresh (-2>-3)
34-2+4=244restart at 4, best updates
4-14-1=334keep extending, best unchanged
523+2=555new best
615+1=666new best
7-56-5=116run absorbs the dip but drops below best
841+4=556best 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

What each variable means
  • 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 by startsWith.
  • node — the trie node currently being visited while walking down character by character.
  • walk(s) — helper that follows the path spelled by s and returns the node reached, or null if the path breaks partway.
  • Returns — insert builds the tree (no value); search/startsWith return true/false; walk returns 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 node
class 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)

What each variable means
  • text — the string being searched in.
  • pat — the pattern being searched for.
  • m — length of pat.
  • 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 position k, so a mismatch can skip re-comparing characters already known to match.
  • i — position scanning through text (or through pat while building lps).
  • j / len — position in pat currently being matched (or matched against itself while building lps).
  • Returns — the index in text where pat first 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 -1
function 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)
Querywalkresult
search("app")reach node after 2nd p, end=truetrue
search("ap")reach node, end=falsefalse
startsWith("appl")path exists to ltrue
search("apples")no child 's' after efalse

"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.

opcharnode pathactionisEnd after
insert "app"aroot→acreatefalse
insert "app"pa→pcreatefalse
insert "app"pap→pcreatetrue
insert "apple"aroot→areusefalse
insert "apple"pa→preusefalse
insert "apple"pap→preuse (already isEnd)true
insert "apple"lapp→lcreatefalse
insert "apple"eappl→ecreatetrue
search "app"a,p,proot→a→p→ppath exists, check isEndtrue → found
search "appl"a,p,p,lroot→a→p→p→lpath exists, check isEndfalse → 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.)

Java
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 node
class 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;
    }
}
Worked example
Insert "apple". 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.

Java
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];
}
Worked example
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.

#ProblemDiffCompaniesSub-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 node
class 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").

CallWhat happensReturns
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 therefalse
startsWith("app")walks a-p-p, node exists regardless of isEndtrue
insert("app")walks existing a-p-p node, now marks its isEnd=true—
search("app")walks a-p-p, isEnd is now truetrue

🔬 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.

StepOperationPath walkedNode changesReturn
0insert("cat")root→c→a→t (all new)create c, a, t; mark t.isEnd=true—
1insert("car")root→c→a (reused)→r (new)create r branching off shared "ca"; mark r.isEnd=true—
2search("car")root→c→a→rr exists and r.isEnd=truetrue
3search("ca")root→c→aa exists but a.isEnd=falsefalse
4startsWith("ca")root→c→aa exists (isEnd irrelevant)true
5search("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)

What each variable means
  • 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 out
function 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

What each variable means
  • 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 into starts: the next interval about to begin.
  • j — pointer into ends: the next interval about to finish.
  • cur — number of intervals currently overlapping at the sweep position.
  • best — the highest value cur has 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 best
function 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]]

Intervalstarts ≤ cur.end?actioncur / output
[1,3]seed—cur=[1,3]
[2,6]2 ≤ 3 yesextend end to 6cur=[1,6]
[8,10]8 ≤ 6 noflush [1,6], startcur=[8,10]
[15,18]15 ≤ 10 noflush [8,10], startcur=[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.

currentlast mergedoverlap? (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."

Java
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 res
function 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;
}
Worked example
[[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.

#ProblemDiffCompaniesSub-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 Google 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 merged
class 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):

stepcurrent intervalactionmerged 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.

StepCurrent intervalLast mergedOverlap? (cur.start ≤ last.end)Actionmerged
0[1,3]none—push as first merged interval[[1,3]]
1[2,6][1,3]2 ≤ 3 → yesmerge: extend end to max(3,6)=6[[1,6]]
2[8,10][1,6]8 ≤ 6 → nono overlap, push new interval[[1,6],[8,10]]
3[15,18][8,10]15 ≤ 10 → nono 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

What each variable means
  • head — the first node of the linked list.
  • prev — the node that will become cur'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 of cur's original next node, taken before cur.next is 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 prev
function 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)

What each variable means
  • 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 original head (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 segment
function 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)

What each variable means
  • 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 before head so the removal logic works even when the target is the very first real node.
  • fast — pointer advanced n+1 steps ahead of slow to open a fixed-size gap.
  • slow — pointer that ends up just before the node to remove once fast reaches 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.next
function 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

stepprevcurlist so far
initnull11→2→3
1121→null
2232→1→null
33null3→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.

stepprevcurrnext (saved)action
0 (init)null1-start
11221.next = null
22332.next = 1
33443.next = 2
44554.next = 3
55665.next = 4
66nullnull6.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.

Java
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 middle
function 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
}
Worked example
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.)

Java
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.next
function 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;
}
Worked example
Lists 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.

Java
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 head
function 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
}
Worked example
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.

#ProblemDiffCompaniesSub-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 = tmp2
class 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.

StepState
Find middleslow lands on 3, so first half = 1→2→3, second = 4→5
Reverse second half4→5 becomes 5→4
Merge iteration 1first=1, secondHalf=5 → 1→5, first advances to 2
Merge iteration 2first=2, secondHalf=4 → 2→4, first advances to 3
secondHalf exhaustedloop stops; 3's original next (null) already terminates the list
Result1 → 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,…

StepPhasePointers / stateList snapshotNote
0startslow=1, fast=11-2-3-4-5n=5 (odd)
1find-middleslow=2, fast=31-2-3-4-5advance slow ×1, fast ×2
2find-middleslow=3, fast=51-2-3-4-5fast.next.next is null → stop; middle = 3
3splitfirst=1-2-3, second=4-5(cut)set slow.next = None
4reverse 2nd halfprev=None→4, cur=4→5second: 4-5 → 5,4 (partial)reversed so far: 4
5reverse 2nd halfprev=4→5, cur=5→Nonesecond fully reversed: 5-4reversal done
6mergep1=1, p2=51-5attach 1.next=5; advance both
7mergep1=2, p2=41-5-2-4attach 2.next=4; advance both
8mergep1=3, p2=None1-5-2-4-3p2 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)

What each variable means
  • 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 hit target.
  • i — index of the element currently being checked.
  • Returns — the indices of the two numbers in a that add up to target.
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

What each variable means
  • 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

What each variable means
  • 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 counter
std::unordered_map<int,int> freq;
for (int x : a) freq[x]++;   // idiomatic counter
freq = Counter(a)   # idiomatic counter
const freq = new Map<number, number>();
for (const x of a) freq.set(x, (freq.get(x) ?? 0) + 1);   // idiomatic counter

Worked example — two sum, target 9, [2,7,11,15]

ia[i]needneed in map?map after
027no{2:0}
172yes → [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.

Stepinumtarget−numseen (before)Hit?Action
0027{}noinsert 2→0
1111-2{2:0}noinsert 11→1
2236{2:0, 11:1}noinsert 3→2
3315-6{2:0, 11:1, 3:2}noinsert 15→3
4472{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).

Java
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 [];
}
Worked example
[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).

Java
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 count
function 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;
}
Worked example
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.

#ProblemDiffCompaniesSub-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"]:

stringsorted keybucket after insert
eataetaet -> [eat]
teaaetaet -> [eat, tea]
tanantant -> [tan]
ateaetaet -> [eat, tea, ate]
natantant -> [tan, nat]
batabtabt -> [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).

Stepwordsorted keymap state after insert
1eataet{aet:[eat]}
2teaaet{aet:[eat, tea]}
3tanant{aet:[eat, tea], ant:[tan]}
4ateaet{aet:[eat, tea, ate], ant:[tan]}
5natant{aet:[eat, tea, ate], ant:[tan, nat]}
6batabt{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)

What each variable means
  • 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 from s.
  • Returns — true if every bracket is properly opened and closed in the right order, else false.
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 st
function 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

What each variable means
  • 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, with b as the right-hand operand and a as 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 — "([{}])"

charactionstack
(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.

StepcharActionStack 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)
7end of stringstack 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.

Java
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 stack
function 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;
}
Worked example
"([{}])" ⇒ 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.

#ProblemDiffCompaniesSub-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 res
class 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]:

itemppops (resolved)stack after (indices)
073-[0]
174pop 0 -> res[0]=1[1]
275pop 1 -> res[1]=1[2]
371-[2,3]
469-[2,3,4]
572pop 4 -> res[4]=1, pop 3 -> res[3]=2[2,5]
676pop 5 -> res[5]=1, pop 2 -> res[2]=4[6]
773-[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).

Stepitempstack before (idx:temp)pops & ans updatesstack after
0———init ans=[0,0,0,0,0,0,0,0][]
1073[]no pops; push 0[0:73]
2174[0:73]74 > 73 → pop 0, ans[0]=1-0=1; push 1[1:74]
3275[1:74]75 > 74 → pop 1, ans[1]=2-1=1; push 2[2:75]
4371[2:75]71 < 75, no pop; push 3[2:75, 3:71]
5469[2:75, 3:71]69 < 71, no pop; push 4[2:75, 3:71, 4:69]
6572[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]
7676[2:75, 5:72]76>72 pop5 ans[5]=1; 76>75 pop2 ans[2]=4; stack empty; push 6[6:76]
8773[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)

What each variable means
  • 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 — get returns the value for k or -1 if absent; put inserts/updates and evicts the least-recently-used entry once cap is 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)

What each variable means
  • 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 among x and everything currently on the stack, stored alongside x so it's retrievable in O(1).
  • Returns — top gives the most recently pushed value, getMin gives 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

opstate (LRU→MRU)returns
put(1,1)1—
put(2,2)1,2—
get(1)2,11 (1 becomes most-recent)
put(3,3)1,3evicts 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.

StepOperationCache (MRU→LRU)EvictedReturned
1put(1,1)[1]——
2put(2,2)[2, 1]——
3get(1)[1, 2]—1
4put(3,3)[3, 1]2—
5get(2)[3, 1]—-1
6put(4,4)[4, 3]1—
7get(1)[4, 3]—-1
8get(3)[3, 4]—3
9get(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.

Java
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 LRU
class 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
        }
    }
}
Worked example
cap 2: put(1,1),put(2,2),get(1)→1 (1 now MRU), put(3,3) evicts key 2 (LRU), get(2)→ -1.

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)

What each variable means
  • 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 res
function 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)

What each variable means
  • 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 (swapping a[i][j] with a[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; a ends 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 -= 1
function 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 9

Top 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.

StepLegBoundaries before (T,B,L,R)Cells visitedResult so far
1top row, left→rightT0 B2 L0 R21, 2, 3[1,2,3]
2right col, top→bottomT1 B2 L0 R26, 9[1,2,3,6,9]
3bottom row, right→leftT1 B2 L0 R18, 7[1,2,3,6,9,8,7]
4left col, bottom→topT1 B1 L0 R14[1,2,3,6,9,8,7,4]
5top row, left→rightT1 B1 L1 R15[1,2,3,6,9,8,7,4,5]
6loop check: T2 > B1T2 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.

Java
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 res
function 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;
}
Worked example
[[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

What each variable means
  • 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 into x.
  • 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 vanish
int singleNumber(std::vector<int>& a){ int x=0; for (int v : a) x^=v; return x; }   // pairs vanish
def single_number(a):
    x = 0
    for v in a:
        x ^= v   # pairs vanish
    return x
function 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)

What each variable means
  • base — the number being raised to a power, reduced mod mod as 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 by base whenever the current low bit of e is 1.
  • Returns — base raised to the e-th power, modulo mod.
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 r
function 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

What each variable means
  • 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 i
n & (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 i
n & (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 i
n & (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 i

Worked 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.

Stepnumnum (binary)result beforeresult after (binary)
0 (init)———000 (0)
14100000100 (4)
21001100101 (5)
32010101111 (7)
41001111110 (6)
52010110100 (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.

Java
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 c
function 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; }
Worked example
[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.

#ProblemDiffCompaniesSub-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 result
class 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:

passdirectionemittedboundaries after
1right along top row1, 2, 3top=1
2down right column6, 9right=1
3left along bottom row8, 7bottom=1
4up left column4left=1
5right along top row5top=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.

Steptop,bottom,left,right (before)LegCells visitedresult so far
10,2,0,2top row, left→right1,2,3[1,2,3]
21,2,0,2right col, top→bottom6,9[1,2,3,6,9]
31,2,0,1bottom row, right→left8,7[1,2,3,6,9,8,7]
41,1,0,1left col, bottom→top4[1,2,3,6,9,8,7,4]
51,1,1,1top 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.

#ProblemDiffCompaniesSub-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 dp
class 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:

i012345
i >> 1-01122
i & 1-10101
dp[i]011212

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.

ibinaryi>>1dp[i>>1]i & 1dp[i] = dp[i>>1] + (i&1)
00———0 (base case)
110010+1=1
2101101+0=1
3111111+1=2
41002101+0=1
51012111+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)

What each variable means
  • n — count primes strictly below this bound.
  • composite — a boolean array where composite[x] is true once x has 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 of i (starting at i*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 count
function 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

What each variable means
  • a, b — the two integers to combine; gcd repeatedly replaces them with (b, a % b) until one is zero.
  • Returns — gcd returns the greatest common divisor; lcm returns the least common multiple, computed as a / gcd(a, b) * b to 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 overflow
long 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 overflow
def 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 overflow
function 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 overflow

Worked example — primes below 10

icomposite?markscount
2no4,6,81
3no92
4yesskip2
5no(25≥10)3
6..8yesskip3
9yesskip3

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).

Steppp prime?ActionRemaining candidates after
12yesmark multiples 4, 6, 8 composite2, 3, 5, 7, 9
23yesmark multiples 6 (already), 92, 3, 5, 7
34no (composite)skip2, 3, 5, 7
45yesno multiple of 5 below 10 left to mark2, 3, 5, 7
56no (composite)skip2, 3, 5, 7
67yesno multiple of 7 below 10 left to mark2, 3, 5, 7
78no (composite)skip2, 3, 5, 7
89no (composite, marked at p=3)skip2, 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).

Java
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); }
Worked example
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

What each variable means
  • 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 & -i isolates its lowest set bit to jump to the next node.
  • delta — the amount added to position i in update.
  • l, r — the inclusive bounds of the range whose sum range computes.
  • s — the running total accumulated while walking up (or down) the tree in prefix.
  • Returns — prefix(i) gives the sum of elements 1..i; range(l, r) gives the sum of elements l..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)

What each variable means
  • t — the array-based binary tree of size 2n; leaves occupy indices n..2n-1 and each internal node stores the sum of its two children.
  • n — the number of leaves (elements in the original array).
  • i — in update, the leaf index being changed (before being shifted into n..2n-1).
  • val — the new value written to leaf i.
  • l, r — the half-open query range [l, r), shifted by n to walk the tree from the leaves upward.
  • s — the running sum accumulated while query climbs 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 s
class 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

opeffectresult
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−07
update(5,−2)index 5 back to 0—
range(3,5)prefix(5)−prefix(2)=55

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).

StepOperationIndices touchedtree[1..6] after
1update(1, +3)1 → 2 → 4[3, 3, 0, 3, 0, 0]
2update(2, +2)2 → 4[3, 5, 0, 5, 0, 0]
3update(3, -1)3 → 4[3, 5, -1, 4, 0, 0]
4update(4, +6)4 (4+4=8>6, stop)[3, 5, -1, 10, 0, 0]
5update(5, +5)5 → 6[3, 5, -1, 10, 5, 5]
6update(6, +4)6 (6+2=8>6, stop)[3, 5, -1, 10, 5, 9]
7query(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.

Java
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); }
}
Worked example
Add 5 at index 3, 2 at index 5. 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)

What each variable means
  • 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 within a.
  • p — the final resting index of the pivot after partition, 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 than pivot.
  • j — the scan index sweeping the window to find elements smaller than pivot.
  • Returns — the value that would sit at index k if a were 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 i
function 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

What each variable means
  • 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 from stream.
  • rand — the random source used to decide, with probability 1/count, whether to replace result with x.
  • 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 result
function 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/5

Every 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.

Stepi (0-indexed)IncomingDraw j in [0,i]Replace? (j<k)Reservoir after
00A— (seed)—A
11Bj=0yes (0<1)B
22Cj=2no (2≥1)B
33Dj=0yes (0<1)D
44Ej=4no (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.

Java
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; }
Worked example
[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.

Java
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 result
function 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;
}
Worked example
Over a stream of 5 items, element #3 is kept with prob 1/3, then survives #4 (prob 3/4) and #5 (prob 4/5): 1/3·3/4·4/5 = 1/5 — uniform.

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.

StepPoppeddistNewly enqueuedQueue afterNote
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.