Cheat Sheet

Quick Patterns Reference

13 core templates. Copy, adapt, solve.

Sliding Window

O(n)

When: subarray/substring, contiguous, at most k

let lo = 0, ans = 0;
for (let hi = 0; hi < n; hi++) {
  // expand window with nums[hi]
  while (/* window invalid */) {
    // shrink from left
    lo++;
  }
  ans = Math.max(ans, hi - lo + 1);
}
  • Sort order is preserved in window
  • Exactly k = atMost(k) - atMost(k-1)
  • Variable window: while loop to shrink; fixed: slide both ends

Two Pointers

O(n)

When: sorted array, pair sum, palindrome, remove duplicates

let lo = 0, hi = arr.length - 1;
while (lo < hi) {
  if (condition) {
    // found answer
  } else if (arr[lo] + arr[hi] < target) {
    lo++;
  } else {
    hi--;
  }
}
  • Array must be sorted (or use fast/slow pointers on linked list)
  • Fast/slow: cycle detection, find middle
  • Three sum: fix one, two-pointer on rest

Binary Search

O(log n)

When: sorted, find minimum/maximum valid value, monotone predicate

let lo = 0, hi = n - 1;
while (lo < hi) {
  const mid = (lo + hi) >> 1;
  if (check(mid)) hi = mid;   // mid is valid, try smaller
  else lo = mid + 1;           // mid is invalid, try larger
}
return lo; // first valid
  • lo < hi + return lo for "first valid"
  • lo <= hi + return -1 for "find exact"
  • Binary search on answer: check(mid) = "can we achieve mid?"

Dynamic Programming

O(n²) or O(n·k)

When: count ways, min/max value, optimal substructure, overlapping subproblems

// Top-down (memo)
const memo = new Map();
function dp(i) {
  if (base_case) return base_value;
  if (memo.has(i)) return memo.get(i);
  const result = Math.max(dp(i-1) + ..., dp(i-2) + ...);
  memo.set(i, result);
  return result;
}

// Bottom-up (tabulation)
const dp = new Array(n+1).fill(0);
dp[0] = base;
for (let i = 1; i <= n; i++)
  dp[i] = Math.max(dp[i-1] + ..., ...);
  • Define state carefully: dp[i] = ?
  • 2D string DP: dp[i][j] = answer for s[0..i-1] and t[0..j-1]
  • Knapsack: outer loop items, inner loop capacity

BFS (Shortest Path)

O(V + E)

When: minimum steps, shortest path unweighted, level-by-level

const visited = new Set([start]);
let queue = [start], level = 0;
while (queue.length) {
  const next = [];
  for (const node of queue) {
    if (node === target) return level;
    for (const nb of graph[node])
      if (!visited.has(nb)) { visited.add(nb); next.push(nb); }
  }
  queue = next;
  level++;
}
  • BFS = shortest path in unweighted graph
  • Multi-source: push ALL starting nodes at level 0
  • 0-1 BFS: use deque, push 0-cost to front

DFS (Components/Cycle)

O(V + E)

When: connected components, cycle detection, topological sort, flood fill

// Recursive DFS with 3-color cycle detection
const color = new Array(n).fill(0); // 0=unvisited, 1=visiting, 2=done
function dfs(u) {
  color[u] = 1;
  for (const v of graph[u]) {
    if (color[v] === 1) return false; // cycle!
    if (color[v] === 0 && !dfs(v)) return false;
  }
  color[u] = 2;
  return true;
}
  • DFS for: components, cycles, topo sort, bridges, SCCs
  • Postorder = reverse topological order
  • Kahn's BFS topo = process indegree-0 first

Tree DP (Postorder)

O(n)

When: tree path, subtree value, diameter, max sum through node

let globalAns = -Infinity;
function dfs(node) {
  if (!node) return 0;
  const left  = Math.max(0, dfs(node.left));
  const right = Math.max(0, dfs(node.right));
  // update answer: path THROUGH this node (uses both branches)
  globalAns = Math.max(globalAns, left + node.val + right);
  // return to parent: single branch only
  return node.val + Math.max(left, right);
}
  • Return value = single branch going UP
  • Global update = path bending at this node (both children)
  • Clamp to 0 for max-sum; keep negatives for diameter/length

Backtracking

O(2ⁿ) or O(n!)

When: all combinations, permutations, subsets, valid arrangements

function backtrack(start, current) {
  if (/* is valid complete solution */) {
    result.push([...current]);
    return;
  }
  for (let i = start; i < candidates.length; i++) {
    if (/* pruning: skip invalid */) continue;
    current.push(candidates[i]);     // choose
    backtrack(i + 1, current);       // explore (i+1 for combos, i for repeats)
    current.pop();                    // unchoose
  }
}
  • Sort candidates first for pruning (skip duplicates, early exit)
  • Combinations: pass i+1; Permutations: pass visited set
  • Pruning is the key to beating brute force

Greedy

O(n log n)

When: interval scheduling, activity selection, always take best now

// Activity selection: max non-overlapping intervals
intervals.sort((a, b) => a[1] - b[1]); // sort by END
let count = 0, lastEnd = -Infinity;
for (const [start, end] of intervals) {
  if (start >= lastEnd) {
    count++;        // take this interval
    lastEnd = end;
  }
}
  • Interval problems: sort by END for max selection, START for merging
  • Prove greedy: exchange argument or induction
  • Heap-greedy: repeatedly take the locally best available choice

Heap / Priority Queue

O(n log k)

When: k-th largest/smallest, streaming top-k, merge k sorted

// Top-k smallest: max-heap of size k
const heap = new MaxHeap();
for (const num of nums) {
  heap.push(num);
  if (heap.size() > k) heap.pop(); // remove largest
}
return heap.top(); // kth smallest

// k-way merge: min-heap with source index
const pq = new MinHeap(); // [val, listIdx, elemIdx]
for (let i = 0; i < lists.length; i++)
  if (lists[i]) pq.push([lists[i].val, i, lists[i]]);
  • Top-k smallest: max-heap of size k (pop when > k)
  • Top-k largest: min-heap of size k (pop when > k)
  • Dijkstra uses min-heap on (distance, node)

Monotonic Stack

O(n)

When: next greater element, span, rectangle, trapped water

const stack = []; // indices, in decreasing value order
const result = new Array(n).fill(-1);
for (let i = 0; i < n; i++) {
  // pop elements that are smaller (for next greater)
  while (stack.length && nums[stack.at(-1)] < nums[i]) {
    result[stack.pop()] = i; // current i is "next greater"
  }
  stack.push(i);
}
  • Decreasing stack → finds next GREATER element
  • Increasing stack → finds next SMALLER element
  • For previous element: process stack before push

Bit Manipulation

O(1) or O(2ⁿ)

When: XOR, subset enumeration, power of 2, single number

// Common bit tricks:
x & (x-1)     // clear lowest set bit (check power of 2: x & (x-1) === 0)
x & (-x)      // isolate lowest set bit
x ^ x === 0  // XOR of same number = 0
a ^ b ^ a === b // XOR is its own inverse

// Enumerate all subsets of mask
for (let sub = mask; sub > 0; sub = (sub-1) & mask) {
  // process sub
}
  • XOR: a^a=0, a^0=a → find single number, missing number
  • Subset DP: iterate submasks in O(3ⁿ) total
  • Bit 30 is safe upper bound for 1e9 problems

Trie (Prefix Tree)

O(L) per op

When: prefix search, autocomplete, word matching, XOR maximize

class TrieNode { constructor() { this.children = {}; this.isEnd = false; } }
class Trie {
  constructor() { this.root = new TrieNode(); }
  insert(word) {
    let node = this.root;
    for (const c of word) {
      if (!node.children[c]) node.children[c] = new TrieNode();
      node = node.children[c];
    }
    node.isEnd = true;
  }
  search(word) { /* walk and check isEnd */ }
  startsWith(prefix) { /* walk, return any node found */ }
}
  • Array[26] children faster than Map for lowercase letters
  • XOR trie: insert binary digits (bit 29 to 0) for max XOR queries
  • Compressed trie (Patricia) for space: merge single-child chains