On this page
DSA Patterns
Last reviewed 21 Sept 2026
Interview coding is pattern recognition under time pressure. For each pattern below: the trigger that should make you reach for it, the approach, the complexity, and reference code. Write your own version before reading the code.
The problem-solving script
Say this out loud in the room — communication is scored:
- Restate the problem and confirm constraints, input ranges, duplicates, empty input, sortedness.
- Walk one small example by hand.
- State the brute force and its Big-O — always, even if trivial.
- Name the pattern and why it applies.
- Sketch the approach in two or three sentences and get a nod before coding.
- Code in small pieces, narrating intent.
- Dry-run on your example, then edge cases: empty, single element, all duplicates, negatives, already sorted, overflow.
- State the final time and space complexity unprompted.
Hashing
Trigger: “have I seen this before?”, counting frequencies, grouping, complement lookups. Turns an O(n²) scan into O(n).
function twoSum(nums, target) { const seen = new Map(); // 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 [];}O(n) time, O(n) space. Related: group anagrams (key by sorted string), subarray sum equals k (prefix-sum counts in a map).
Prefix sum
Trigger: “sum of a subarray / range”, “subarray with sum k”, counts over ranges, or a sliding window that breaks because values can be negative. prefix[j] - prefix[i] is the sum of (i, j].
function subarraySumK(nums, k) { const count = new Map([[0, 1]]); // prefix sum -> times seen let sum = 0, res = 0; for (const x of nums) { sum += x; res += count.get(sum - k) ?? 0; // earlier prefix that leaves exactly k count.set(sum, (count.get(sum) ?? 0) + 1); } return res;}O(n) time, O(n) space. Seed the map with 0 → 1 so subarrays starting at index 0 count. Also: zero-sum subarray, product of array except self (prefix and suffix products), range sum query, 2-D prefix sums for submatrix sums.
Two pointers
Trigger: a sorted array, or comparing from both ends; find a pair or triplet, reverse in place, partition, dedupe.
function pairSumSorted(a, target) { let l = 0, r = a.length - 1; while (l < r) { const s = a[l] + a[r]; if (s === target) return [l, r]; s < target ? l++ : r--; } return [];}O(n) after an O(n log n) sort. Three-sum: fix one element, two-pointer the rest, skip duplicates.
Cyclic sort / index marking
Trigger: an array of n numbers in the range 1..n (or 0..n) — find the missing, duplicate, or first missing positive in O(1) extra space. Each value has a “home” index.
function firstMissingPositive(nums) { const n = nums.length; for (let i = 0; i < n; i++) { // keep swapping nums[i] to its home (value v lives at index v - 1) while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] !== nums[i]) { const j = nums[i] - 1; [nums[i], nums[j]] = [nums[j], nums[i]]; } } for (let i = 0; i < n; i++) if (nums[i] !== i + 1) return i + 1; return n + 1;}O(n) — every swap places one value permanently. Alternative when you may mutate: flip nums[v - 1] negative to mark v as seen. Also: find all duplicates, set mismatch (repeating and missing), missing number (or XOR / sum formula).
Sliding window
Trigger: longest, shortest, or count of a contiguous subarray or substring meeting a constraint. Expand the right edge; shrink the left while the window is invalid.
function longestUniqueSubstring(s) { const last = new Map(); let start = 0, best = 0; for (let i = 0; i < s.length; i++) { const c = s[i]; if (last.has(c) && last.get(c) >= start) start = last.get(c) + 1; last.set(c, i); best = Math.max(best, i - start + 1); } return best;}O(n) time. Also: minimum window substring, max sum subarray of size k, longest with at most k distinct.
Binary search
Trigger: sorted-array lookup, or a monotonic predicate over a numeric range (“smallest capacity such that we finish in D days”).
// leftmost index where pred(x) is true; pred must be false...false, true...truefunction lowerBound(lo, hi, pred) { while (lo < hi) { const mid = lo + ((hi - lo) >> 1); if (pred(mid)) hi = mid; else lo = mid + 1; } return lo;}O(log n), times an O(n) check when searching the answer space. Keep one template; define the predicate carefully; use the shift or Math.floor to avoid mid overflow.
Binary search on the answer
When the question asks for the minimum maximum or maximum minimum of something (capacity, speed, pages, distance) and “can we do it with X?” is monotonic in X, binary-search X and write a greedy O(n) feasibility check.
function minEatingSpeed(piles, h) { const canFinish = (k) => piles.reduce((t, p) => t + Math.ceil(p / k), 0) <= h; return lowerBound(1, Math.max(...piles), canFinish);}O(n log range). Same shape: ship packages within D days, split array largest sum, book allocation, painter’s partition, aggressive cows (maximise the minimum gap — flip the predicate), median of two sorted arrays (search the partition).
Stack
Trigger: matching pairs, “most recent unresolved thing”, or “next greater / smaller element” (a monotonic stack).
function nextGreater(nums) { const res = Array(nums.length).fill(-1); const st = []; // indices, values decreasing for (let i = 0; i < nums.length; i++) { while (st.length && nums[i] > nums[st[st.length - 1]]) { res[st.pop()] = nums[i]; } st.push(i); } return res;}O(n) — each index is pushed and popped once.
Monotonic deque
Trigger: max or min of every window of size k, or DP where you need the best value over a sliding range. A deque of indices whose values stay monotonic; the front is always the answer for the current window.
function maxSlidingWindow(nums, k) { const dq = [], res = []; // indices, values decreasing for (let i = 0; i < nums.length; i++) { if (dq.length && dq[0] <= i - k) dq.shift(); // out of window while (dq.length && nums[dq[dq.length - 1]] <= nums[i]) dq.pop(); // dominated dq.push(i); if (i >= k - 1) res.push(nums[dq[0]]); } return res;}O(n) amortised (use a real deque or a head index in production; shift is O(n) in JS). The monotonic stack above covers next greater / smaller, largest rectangle in a histogram, daily temperatures, stock span, and trapping rain water.
Fast and slow pointers
Trigger: linked-list cycle detection, finding the middle, reordering.
function hasCycle(head) { let slow = head, fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false;}O(n) time, O(1) space. The middle is slow when fast reaches the end. Use a dummy head node for insert- or delete-heavy problems.
Linked list in-place reversal
Trigger: reverse the whole list, a sub-range, or every group of k; palindrome check; reorder list. Three pointers — prev, curr, next — and a dummy head when the head can change.
function reverseList(head) { let prev = null, curr = head; while (curr) { const next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev;}O(n) time, O(1) space. Reverse in groups of k: check k nodes exist, reverse them, reconnect the previous group’s tail. Palindrome: find the middle (fast/slow), reverse the second half, compare. Reorder list: middle + reverse + merge alternately.
Trees — BFS and DFS
Trigger: anything hierarchical. Level-order needs a queue; depth, diameter, path sum, and validation are recursive.
function levelOrder(root) { if (!root) return []; const res = [], q = [root]; while (q.length) { const level = []; for (let n = q.length; n > 0; n--) { const node = q.shift(); level.push(node.val); if (node.left) q.push(node.left); if (node.right) q.push(node.right); } res.push(level); } return res;}O(n) time. A binary-search-tree in-order traversal yields sorted order. Know: validate a BST by passing down min and max bounds; lowest common ancestor.
Graphs
Trigger: nodes and edges, or a grid treated as a graph. BFS for shortest path in an unweighted graph; DFS for connectivity and cycles; Kahn’s algorithm or DFS colours for topological sort.
function numIslands(grid) { const R = grid.length, C = grid[0].length; let count = 0; const sink = (r, c) => { if (r < 0 || c < 0 || r >= R || c >= C || grid[r][c] !== '1') return; grid[r][c] = '0'; sink(r + 1, c); sink(r - 1, c); sink(r, c + 1); sink(r, c - 1); }; for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) if (grid[r][c] === '1') { count++; sink(r, c); } return count;}O(V + E).
Topological sort
Trigger: dependencies, prerequisites, build order, “can all tasks finish?”, ordering derived from comparisons (alien dictionary). Only exists for a DAG — if you cannot order every node, there is a cycle.
function courseOrder(n, prereqs) { // prereqs: [course, needs] const adj = Array.from({ length: n }, () => []); const indeg = Array(n).fill(0); for (const [c, p] of prereqs) { adj[p].push(c); indeg[c]++; } const q = [], order = []; for (let i = 0; i < n; i++) if (indeg[i] === 0) q.push(i); for (let h = 0; h < q.length; h++) { const u = q[h]; order.push(u); for (const v of adj[u]) if (--indeg[v] === 0) q.push(v); } return order.length === n ? order : []; // [] means a cycle}O(V + E) (Kahn’s algorithm). DFS alternative: post-order, then reverse; three colours detect a cycle. Also: minimum time per job in a DAG (process in topological order), longest path in a DAG.
Union-Find (disjoint set)
Trigger: “are these connected?”, number of components as edges arrive, redundant connection, grouping equivalent items (accounts merge, similar strings), Kruskal’s MST.
class DSU { constructor(n) { this.parent = [...Array(n).keys()]; this.size = Array(n).fill(1); } find(x) { while (this.parent[x] !== x) { this.parent[x] = this.parent[this.parent[x]]; // path halving x = this.parent[x]; } return x; } union(a, b) { let ra = this.find(a), rb = this.find(b); if (ra === rb) return false; // already connected → cycle if (this.size[ra] < this.size[rb]) [ra, rb] = [rb, ra]; this.parent[rb] = ra; this.size[ra] += this.size[rb]; return true; }}Near O(1) per operation with path compression and union by size. Prefer it over DFS when edges arrive over time or you only need connectivity, not paths.
Trie
Trigger: many prefix queries, autocomplete, “word starts with”, searching many words in a grid at once (word search II), shortest unique prefix, maximum XOR of two numbers (a bit trie).
class Trie { constructor() { this.root = {}; } insert(word) { let node = this.root; for (const ch of word) node = node[ch] ??= {}; node.end = true; } #walk(s) { let node = this.root; for (const ch of s) if (!(node = node[ch])) return null; return node; } search(word) { return !!this.#walk(word)?.end; } startsWith(prefix) { return this.#walk(prefix) !== null; }}O(L) per operation for a word of length L; space is total characters stored. For word search II, build a trie of the words and DFS the grid once, pruning when no child matches.
Heap / priority queue
Trigger: “k largest or smallest”, “kth”, streaming order statistics. JavaScript has no built-in heap — carry a small class or keep a sorted array for tiny k.
// k largest: keep a min-heap of size k; the root is the kth largest.// Running median: a max-heap for the low half, a min-heap for the high half.Top-k is O(n log k).
Top-K, K-way merge and two heaps
Three recurring heap shapes — recognise which one before coding:
- Top-K — “k largest / most frequent / closest”: a min-heap of size k (pop when size exceeds k). O(n log k). For a one-off query, quickselect is O(n) average; for frequencies, bucket sort by count is O(n).
- K-way merge — “merge k sorted lists / arrays”, “kth smallest in a sorted matrix”, “smallest range covering k lists”: push the head of each list, pop the smallest, push its successor. O(N log k).
- Two heaps — “running median”, “sliding window median”: a max-heap for the lower half, a min-heap for the upper half; rebalance so sizes differ by at most one. O(log n) per insert.
class MinHeap { constructor(cmp = (a, b) => a - b) { this.a = []; this.cmp = cmp; } get size() { return this.a.length; } peek() { return this.a[0]; } push(x) { const a = this.a; a.push(x); for (let i = a.length - 1, p; i > 0 && this.cmp(a[i], a[p = (i - 1) >> 1]) < 0; i = p) [a[i], a[p]] = [a[p], a[i]]; } pop() { const a = this.a, top = a[0], last = a.pop(); if (a.length) { a[0] = last; for (let i = 0; ;) { const l = 2 * i + 1, r = l + 1; let m = i; if (l < a.length && this.cmp(a[l], a[m]) < 0) m = l; if (r < a.length && this.cmp(a[r], a[m]) < 0) m = r; if (m === i) break; [a[i], a[m]] = [a[m], a[i]]; i = m; } } return top; }}Carry this class into JavaScript interviews — pass a comparator to get a max-heap or to order [value, listIndex] pairs.
Backtracking
Trigger: generate all subsets, permutations, or combinations; constraint satisfaction.
function subsets(nums) { const res = [], path = []; const bt = (start) => { res.push([...path]); for (let i = start; i < nums.length; i++) { path.push(nums[i]); bt(i + 1); path.pop(); // undo } }; bt(0); return res;}Shape: choose, recurse, un-choose. Permutations track a used array; combination sum allows reuse by recursing on i rather than i + 1. Exponential by nature — that is expected.
Dynamic programming
Trigger: “number of ways”, “min or max cost”, “can we reach”, with overlapping subproblems and choices at each step.
Steps: define the state (what dp[i] means), the recurrence, the base case, the fill order, the answer cell. Then reduce space if only the last few states are needed.
function coinChange(coins, amount) { const dp = Array(amount + 1).fill(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] === Infinity ? -1 : dp[amount];}O(amount × coins). House robber: dp[i] = max(dp[i-1], dp[i-2] + nums[i]), reducible to two variables.
Greedy
Trigger: a locally best choice that never needs undoing — scheduling the most non-overlapping activities, jump game, gas station, minimum platforms, assign cookies, task scheduler. Usually “sort by something, then sweep”.
function canJump(nums) { let reach = 0; for (let i = 0; i < nums.length; i++) { if (i > reach) return false; // stuck before i reach = Math.max(reach, i + nums[i]); } return true;}O(n), or O(n log n) with a sort. Justify it — interviewers ask “why is greedy correct?”. Use an exchange argument: swapping any optimal solution’s choice for the greedy one never makes it worse. If you cannot argue that, it is probably DP (coin change with arbitrary coins is the classic trap).
Intervals
Trigger: overlapping ranges, scheduling, “can attend all”, “rooms needed”. Almost always: sort by start first.
function merge(intervals) { intervals.sort((a, b) => a[0] - b[0]); const res = [intervals[0]]; for (let i = 1; i < intervals.length; i++) { const last = res[res.length - 1]; if (intervals[i][0] <= last[1]) last[1] = Math.max(last[1], intervals[i][1]); else res.push(intervals[i]); } return res;}O(n log n). Minimum meeting rooms: sort starts and ends separately and sweep, or use a min-heap of end times.
Bit manipulation
Trigger: “every element appears twice except one”, O(1)-space tricks, power of two, subsets of a small set (n ≤ 20), toggling flags.
const singleNumber = (nums) => nums.reduce((x, n) => x ^ n, 0); // a ^ a = 0, a ^ 0 = aconst isPowerOfTwo = (n) => n > 0 && (n & (n - 1)) === 0;function countBits(n) { let c = 0; while (n) { n &= n - 1; c++; } return c; } // KernighanKnow: x & (x - 1) clears the lowest set bit, x & -x isolates it, 1 << i builds a mask, and enumerating mask from 0 to (1 << n) - 1 walks every subset. JavaScript bitwise operators work on 32-bit signed integers — use >>> 0 for unsigned, and BigInt beyond 32 bits.
Matrix traversal
Trigger: 2-D grids — spiral order, rotate by 90°, set matrix zeroes, search a sorted matrix, and grid-as-graph problems (islands, rotting oranges, shortest path in a maze).
function spiralOrder(m) { const res = []; 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;}O(R × C). Rotate 90° clockwise in place: transpose, then reverse each row. Sorted rows and columns: start at the top-right corner and step left or down (O(R + C)). For grid BFS/DFS, keep a dirs = [[1,0],[-1,0],[0,1],[0,-1]] array; multi-source BFS (all rotten oranges in the queue at once) gives distances from the nearest source.
When you do not recognise the problem
Look for structure: sorted implies binary search or two pointers; contiguous implies sliding window; “seen before” implies a hash; a tree or graph implies BFS or DFS; optimal over choices with overlap implies DP; “k of something” implies a heap; dependencies imply topological sort; “connected as edges arrive” implies union-find; many prefixes imply a trie; values in 1..n imply cyclic sort; “minimise the maximum” implies binary search on the answer; range sums with negatives imply prefix sums. Pick the most promising, sketch it in words, get a nod, then code.