Patterns beat memorising problems
There are thousands of coding interview questions and you cannot memorise them. You do not need to. Most questions are variations on a small set of patterns. Once you can recognise the pattern, you already know the shape of the solution, and you spend your energy on the specific details instead of starting from nothing.
The skill to train is recognition. When you read a problem, ask what category it falls into before you write code. "Find a pair that sums to a target" should trigger the two-pointer or hash-map pattern instantly. "Longest substring without repeats" should trigger sliding window. Curated problem banks bear this out: Sean Prashad's collection groups 179 interview questions into a short set of recurring patterns, which is exactly why training the trigger beats grinding the questions one by one. The patterns below cover a large share of what interviewers ask, and the second half of this guide is about how to actually use them under pressure, where most candidates lose points.
A useful mental model: a pattern is not a memorised solution, it is a question you ask the problem. "Is this contiguous?" points at sliding window. "Is the answer monotonic?" points at binary search on the answer. Train the questions, not the answers.
Here is the quick map most screens draw from, with the signal that should fire in your head and the cost you are trading for.
| Pattern | Fires when you see | Typical complexity |
|---|---|---|
| Two pointers | Sorted input, pair or triplet, palindrome, in-place | O(n) time, O(1) space |
| Sliding window | Contiguous subarray or substring, longest or shortest | O(n) time, O(k) space |
| Hash map | "Have we seen", counts, group by, complement lookup | O(n) time, O(n) space |
| BFS or DFS | Tree, graph, grid, reachability, levels | O(V + E) |
| Binary search | Sorted data, or "smallest value such that" | O(log n) per check |
| Dynamic programming | "How many ways", "minimum cost", overlapping subproblems | O(states x transitions) |
Two pointers
Use this when the input is sorted or can be sorted, and you are looking for a pair or a triplet, or you are shrinking a range from both ends. It turns many nested-loop solutions into a single pass, dropping you from O(n squared) to O(n).
Signal phrases: "sorted array", "find two numbers that", "is it a palindrome", "remove duplicates in place".
function twoSumSorted(nums: number[], target: number): [number, number] | null {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const sum = nums[left] + nums[right];
if (sum === target) return [left, right];
if (sum < target) left++;
else right--;
}
return null;
}
console.log(twoSumSorted([2, 7, 11, 15], 9)); // [0, 1]The mental hook is that moving a pointer inward is a decision you can never regret, because the array is sorted. If the sum is too small, the smallest element cannot pair with anything to its left, so you discard it for good. That irreversibility is what makes a single pass correct. Triplet problems such as 3-sum are the same idea with one fixed index and two pointers sweeping the rest, after sorting and skipping duplicates.
Sliding window
Use this for problems about a contiguous subarray or substring where you want the longest, shortest, or a window that meets some condition. You expand the window to include more, and shrink it from the left when a constraint breaks. The invariant is what makes it work: at the top of every loop iteration the window is valid, so the moment it stops being valid you shrink until it is valid again.
Signal phrases: "longest substring", "maximum sum of size k", "smallest window containing".
function longestUniqueSubstring(s: string): number {
const seen = new Map<string, number>();
let start = 0;
let best = 0;
for (let end = 0; end < s.length; end++) {
const c = s[end];
if (seen.has(c) && seen.get(c)! >= start) {
start = seen.get(c)! + 1;
}
seen.set(c, end);
best = Math.max(best, end - start + 1);
}
return best;
}
console.log(longestUniqueSubstring("abcabcbb")); // 3There are two flavours worth separating. A fixed window of size k slides one step at a time and you add the new element and remove the old one. A variable window grows and shrinks based on a condition, which is what the example above does. The trap is forgetting that start should never move backwards, which is why the check is seen.get(c)! >= start rather than just seen.has(c). A repeated character that fell out of the window on the left should not drag the window back.
Hash map for lookups and counting
Use a hash map whenever you need fast membership checks, frequency counts, or to remember what you have already seen. Many "do this in one pass" questions are really asking you to trade memory for time with a map: a hash lookup averages O(1) time for O(n) space, and naming that trade out loud is what tells the interviewer you chose the structure deliberately. This is the most quietly common pattern in interviews because it hides inside other patterns: sliding window leans on a frequency map, graph traversal leans on a visited set.
Signal phrases: "have we seen", "count occurrences", "group by", "first non-repeating".
The classic unsorted two-sum is the canonical example: store each value as you go and check whether the complement is already in the map. The subtle point candidates miss is ordering. You check for the complement before inserting the current element, otherwise a single value can match itself when the target is double that value.
function twoSum(nums: number[], target: number): [number, number] | null {
const seen = new Map<number, number>();
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 null;
}
console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]Breadth-first and depth-first search
Use BFS and DFS for anything that is a tree or graph, or that can be modelled as one, such as a grid. BFS finds shortest paths in unweighted graphs and explores level by level, because the first time it reaches a node is along a path with the fewest edges. DFS suits exhaustive exploration, connectivity, and problems about paths or components.
Signal phrases: "shortest path", "number of islands", "connected components", "levels of a tree", "can you reach".
function numIslands(grid: string[][]): number {
let count = 0;
const flood = (r: number, c: number) => {
if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length) return;
if (grid[r][c] !== "1") return;
grid[r][c] = "0";
flood(r + 1, c);
flood(r - 1, c);
flood(r, c + 1);
flood(r, c - 1);
};
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < grid[0].length; c++) {
if (grid[r][c] === "1") {
count++;
flood(r, c);
}
}
}
return count;
}
console.log(numIslands([["1", "1", "0"], ["0", "1", "0"], ["0", "0", "1"]])); // 2Choosing between the two is usually about what the question rewards. If it asks for the shortest number of steps in an unweighted setting, reach for BFS with a queue, because the first time you touch a node is along a shortest path. If it asks whether something is reachable, or to enumerate all paths, or to count regions, DFS with recursion or an explicit stack is cleaner. Always carry a visited set on a general graph, or you will loop forever on a cycle. On a grid you can either keep a separate visited structure or mutate the grid in place as the flood-fill above does, which is fine when the interviewer is happy for you to destroy the input.
Binary search beyond sorted arrays
Everyone knows binary search on a sorted array. The harder version is binary search on the answer. If you can write a function that says "is a candidate value good enough", and goodness is monotonic, meaning once a value works every larger value also works, you can binary search the space of candidate answers instead of the array itself. The technique generalises binary search to any monotonic predicate, not just a sorted list of values.
Signal phrases: "minimum capacity to", "smallest value such that", "split into k groups so the largest is minimised".
The trick is to stop looking for a target inside the array and start asking which answer is feasible. Here is the shape, deciding the smallest ship capacity that can deliver all packages within days.
function shipWithinDays(weights: number[], days: number): number {
const feasible = (cap: number): boolean => {
let used = 1;
let load = 0;
for (const w of weights) {
if (load + w > cap) {
used++;
load = 0;
}
load += w;
}
return used <= days;
};
let lo = Math.max(...weights);
let hi = weights.reduce((a, b) => a + b, 0);
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (feasible(mid)) hi = mid;
else lo = mid + 1;
}
return lo;
}
console.log(shipWithinDays([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5)); // 15The two judgement calls are the bounds and the loop invariant. The lower bound is the largest single item, because no capacity smaller than that can ever ship it. The upper bound is the total, which trivially works in one day. The lo < hi loop with hi = mid converges on the smallest feasible answer without an off-by-one. Getting the boundary update wrong is the single most common binary search bug, so state your invariant out loud before you code it.
Dynamic programming, kept practical
Dynamic programming intimidates people, but most interview DP comes down to one idea: the answer to a problem is built from answers to smaller versions of the same problem, and you store those to avoid recomputing. Start by writing the plain recursion, then add memoisation, then convert to a table if you have time. That progression also doubles as your narration in the room, which reassures the interviewer you understand why the table works rather than having memorised it.
Signal phrases: "in how many ways", "minimum cost to reach", "can you make up this amount", "longest increasing".
function coinChange(coins: number[], amount: number): number {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let a = 1; a <= amount; a++) {
for (const coin of coins) {
if (coin <= a) dp[a] = Math.min(dp[a], dp[a - coin] + 1);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
console.log(coinChange([1, 2, 5], 11)); // 3To get unstuck on a DP problem, answer three questions in order. What is the state, meaning the smallest set of variables that fully describes a subproblem? What is the transition, meaning how a state is built from smaller states? And what is the base case? For coin change the state is the amount remaining, the transition is "take one coin then solve the smaller amount", and the base case is that zero needs zero coins. If you can say those three things aloud, you can almost always write the code.
A few more worth knowing
- Heaps and a priority queue for "top k", "k closest", or merging sorted streams.
- Stacks for matching brackets, parsing, and "next greater element".
- Prefix sums for repeated range-sum queries.
- Backtracking for permutations, combinations, and constraint puzzles like N-queens.
- Intervals for merging or scheduling, almost always after sorting by start.
- Union-find for connectivity questions where edges arrive over time.
You do not need all of these on day one. The first five sections cover the majority of screens, and the list above is what separates a comfortable mid-level pass from a senior one.
What strong recognition looks like versus weak
The difference between candidates is rarely raw coding speed. It is how quickly and how cleanly they classify the problem. Here is the contrast in practice.
| Weak | Strong |
|---|---|
| Starts coding within thirty seconds, then rewrites twice | Spends two minutes naming the pattern and stating complexity before typing |
| Says "I think I can use a loop" | Says "this is a variable sliding window because we want the longest contiguous run under a constraint" |
| Discovers edge cases when the code breaks | Lists empty input, single element, all duplicates, no match before running |
| Goes silent while thinking | Narrates the trade-off: "a hash map costs O(n) space but buys O(n) time" |
| Treats a wrong first guess as failure | Treats it as a checkpoint and pivots when the interviewer hints |
A short worked dialogue
Picture a forty-five minute screen. The prompt is: given an array of integers and a value k, return the length of the longest contiguous subarray whose sum is at most k, where all numbers are positive.
A strong candidate does not type yet. They say: "All numbers are positive, and we want the longest contiguous run under a sum constraint. That is a variable sliding window. As I extend the right edge the sum only grows, and when it exceeds k I shrink from the left until it is valid again. Because everything is positive, the window monotonically behaves, so one pass is enough, O(n) time and O(1) space. Let me confirm the edge cases: an empty array returns zero, and a single element larger than k also returns zero."
Only then do they write it.
function longestUnderK(nums: number[], k: number): number {
let start = 0;
let sum = 0;
let best = 0;
for (let end = 0; end < nums.length; end++) {
sum += nums[end];
while (sum > k && start <= end) {
sum -= nums[start];
start++;
}
best = Math.max(best, end - start + 1);
}
return best;
}
console.log(longestUnderK([2, 1, 5, 1, 3], 8)); // 3After coding they trace [2, 1, 5, 1, 3] with k of 8 by hand, landing on the window [1, 5, 1] of length three, then note the one assumption that breaks everything: "If negatives were allowed, the sliding window stops working because adding an element can shrink the sum, and I would switch to prefix sums with a different structure." That single sentence about the assumption is often what tips a "hire" over a "leaning hire", because it shows you know the boundary of your own solution.
Where candidates lose points even with the right pattern
- Pattern-matching too fast and forcing the wrong tool, then refusing to abandon it when it stops fitting.
- Writing code before stating the approach, so the interviewer cannot redirect you early and has to watch you walk into a wall.
- Going silent. Interviewers score communication, and an unspoken correct idea reads the same as no idea.
- Ignoring complexity until asked. State time and space up front, then again at the end if you optimised.
- Off-by-one errors in binary search and window boundaries, usually because the loop invariant was never made explicit.
- Forgetting the visited set on graphs, which turns a clean traversal into an infinite loop.
- Not testing by hand. Trace one small example through your finished code before you declare it done.
When two patterns fit the same problem
Recognition is rarely a single clean match. Several problems trip the same first instinct, and the skill that separates a fast pass from a slow one is knowing which signal wins when two patterns both look plausible. The collisions below are the ones I see candidates lose the most time to.
| The problem says | The pattern people reach for | The pattern that actually fits | The tell |
|---|---|---|---|
| Find two numbers that sum to a target | Two pointers | Hash map, unless the input is already sorted | Sorted input is the only thing that earns the O(1)-space pointer sweep. Unsorted, sorting costs O(n log n) and a one-pass map is cheaper. |
| Longest subarray under a sum limit, with negatives allowed | Sliding window | Prefix sums with a hash map | A window only works while extending the right edge can only grow the sum. One negative number breaks that monotonicity. |
| For each element, find the next larger one to its right | Nested loops or two pointers | Monotonic stack | You must remember earlier unresolved elements and settle them out of order, which a stack does and a moving pair cannot. |
| Fewest steps across an unweighted grid | DFS, because it is quick to write | BFS | The first time BFS reaches a cell is along a shortest path. DFS can arrive the long way first and overcount. |
Work the third row, because it is the one people misclassify most. The prompt: given daily temperatures, for each day output how many days you wait until a warmer one, or zero if none comes. The two-pointer instinct fails immediately, because the answer for an early day can depend on a value far to its right that you have not reached yet. The move is a stack that holds the indices of days still waiting for a warmer reading, kept so their temperatures decrease from the bottom up. Each new day pops every colder day it resolves.
function daysUntilWarmer(temps: number[]): number[] {
const answer = new Array(temps.length).fill(0);
const waiting: number[] = []; // indices, temperatures decreasing down the stack
for (let today = 0; today < temps.length; today++) {
while (waiting.length && temps[today] > temps[waiting[waiting.length - 1]]) {
const past = waiting.pop()!;
answer[past] = today - past;
}
waiting.push(today);
}
return answer;
}
console.log(daysUntilWarmer([73, 74, 75, 71, 69, 72, 76, 73])); // [1, 1, 4, 2, 1, 1, 0, 0]Each index is pushed once and popped at most once, so despite the inner while the whole pass is O(n). The recognition lesson generalises: when a problem needs you to hold onto earlier items and resolve them later in the order they get beaten, reach for a stack before you reach for a second pointer. Naming why the obvious pattern loses is worth as much in the room as naming the one that wins.
A four-week practice framework
Reps beat cramming, but unstructured reps waste time. A schedule that works for most people:
- Week one: two pointers, sliding window, hash map. The highest-frequency patterns, and they reinforce each other. Do three or four problems per pattern, and after each write one sentence on why that pattern fitted.
- Week two: BFS and DFS on both trees and grids, then binary search including binary search on the answer. Do at least two "smallest value such that" problems, because that variant is where the points are.
- Week three: dynamic programming and backtracking. Always write the recursion first, then memoise, then tabulate. Do not jump straight to a table you half remember.
- Week four: mixed sets under a timer, plus heaps, stacks, intervals, and prefix sums to round out coverage. Simulate the real format, talking out loud throughout.
The goal of the final week is not new knowledge, it is making recognition automatic so that under stress your first minute is calm rather than frantic.
FAQ
How many problems do I actually need to do? Closer to eighty solved with understanding beats three hundred rushed. The metric that matters is whether you can name the pattern of an unseen problem within the first minute, not how many you have ground through.
Should I memorise solutions? No. Memorise the recognition triggers and the skeleton of each pattern, then derive the specifics live. Memorised solutions collapse the moment the interviewer twists the constraints, which they often do on purpose.
Which language should I use? Whichever you write most fluently. Interviewers care about correct, readable code and clear reasoning, not your language. Pick one and know its standard library well, especially maps, sets, sorting, and the heap or priority queue.
What if I do not recognise the pattern at all? Fall back to brute force out loud, get a working baseline, then ask what is making it slow. The bottleneck points at the pattern: repeated lookups suggest a hash map, repeated recomputation suggests DP, a sorted-and-scanning feel suggests two pointers.
Is this still relevant given AI tools? Some companies are shifting toward debugging and system reasoning, but algorithmic screens remain standard for most roles. Pattern recognition is the transferable skill anyway, since it is about decomposing a problem, which no autocomplete does for you in the room.
Sources
- Sean Prashad, LeetCode Patterns: a curated set of interview problems grouped by pattern, the evidence that a short list of recognitions covers most questions.
- Big-O Cheat Sheet: average and worst-case time and space complexity for the data structures referenced above, including the O(1)-average hash lookup.
- Breadth-first search, Wikipedia: the shortest-path-by-edge-count property that decides BFS over DFS on unweighted graphs.
- Binary search on a monotonic predicate, CP-Algorithms: the formal basis for binary searching the answer rather than the array.
Where to take these patterns next
Turn recognition into reps in the tools and roadmaps built around these exact patterns:
- Coding interview patterns hub: worked problem sets for each pattern above.
- NeetCode 150 roadmap: an ordered path that introduces the patterns in a sensible sequence.
- Code practice tool: run and test solutions in the browser while you drill the recognition triggers.