The 15 patterns that cover the large majority of software
engineering interview problems. Recognize the pattern from the problem
signals, then fit the template to the specifics.
How to use this page
Do not read top-to-bottom. Pick a recent problem you solved, identify which
pattern it matched, and check that your solution looks like the template
here. Gaps between your code and the template are your study list.
Array & String Traversal
Two Pointers
Time O(n)
·
Space O(1)
Signals in the problem
Input is sorted or can be sorted cheaply
Looking for a pair or triple with a property (sum, difference)
"Remove duplicates in-place" / "partition" / "reverse" on an array or string
When to use
Two indices walking the array — either from opposite ends (sorted pair-sum) or in the same direction (partition / dedupe). Replaces a nested loop with a single pass.
Forgetting to remove the left element from the auxiliary counter when shrinking
Confusing fixed-size windows (always width K) with variable-size (width varies by invariant)
Canonical problems
Longest Substring Without Repeating Characters
(LC 3)
Minimum Window Substring
(LC 76)
Longest Substring with At Most K Distinct Characters
(LC 340)
Maximum Sum Subarray of Size K
— fixed window
Fast / Slow Pointers
Time O(n)
·
Space O(1)
Signals in the problem
Linked-list cycle detection
"Find the middle / Nth-from-end" of a linked list in one pass
Happy-number / sequence-cycle problems on functional graphs
When to use
Two pointers on the same structure moving at different speeds. The fast one laps the slow one iff there is a cycle; when the fast one reaches the end the slow one is at the midpoint.
Checking `fast` without also checking `fast.next` — null-deref on odd-length lists
Claiming a cycle exists when what you actually detected is a meet point inside one
Canonical problems
Linked List Cycle
(LC 141)
Linked List Cycle II — find cycle start
(LC 142)
Middle of the Linked List
(LC 876)
Happy Number
(LC 202)
Divide & Conquer
Binary Search
Time O(log n)
·
Space O(1)
Signals in the problem
Sorted input + O(log n) target
"Smallest / largest X such that predicate(X) is true" — even when the array is not sorted
Search on the answer space (min capacity, min days, min speed)
When to use
Any time a monotone predicate splits the search space into no / … / no / yes / … / yes. Use the "first true" form — it generalizes cleaner than the classic low/high/mid with three branches.
Template
Python
# "First true" — generalized binary search on a monotone predicate.
# Works for the classic case and for "answer space" problems.
deffirst_true(lo: int, hi: int, pred) -> int:
# Invariant: pred(hi) must be True; result is in [lo, hi].
while lo < hi:
mid = lo + (hi - lo) //2# overflow-safe form
if pred(mid):
hi = mid
else:
lo = mid +1
return lo
TypeScript
// "First true" — generalized binary search on a monotone predicate.
Forgetting to mark nodes as seen at enqueue time — same node goes on the queue multiple times and memory explodes
Using on weighted graphs — BFS solves unweighted shortest path only. Weighted → Dijkstra
Canonical problems
Binary Tree Level Order Traversal
(LC 102)
Word Ladder
(LC 127)
Rotting Oranges
(LC 994)
Shortest Path in Binary Matrix
(LC 1091)
Depth-First Search
Time O(V + E)
·
Space O(V) — recursion stack or explicit stack
Signals in the problem
Tree / graph traversal where depth matters or you need full exploration
"Count connected components" / "is there a path from A to B?"
Tree recursion (subtree properties, path sums)
When to use
Explore as deep as possible before backtracking. Recursive form is concise but watch the call-stack depth; iterative with an explicit stack is safer for large inputs.
Template
Python
# DFS on a graph with visited set — iterative with explicit stack.
constgraph:number[][] = Array.from({ length: n }, () => []);
constindeg=newArray(n).fill(0);
for (const [u, v] of edges) {
graph[u].push(v);
indeg[v]++;
}
constq:number[] = [];
for (let i =0; i < n; i++) if (indeg[i] ===0) q.push(i);
constorder:number[] = [];
let head =0;
while (head < q.length) {
constnode= q[head++];
order.push(node);
for (constnxtof graph[node]) {
if (--indeg[nxt] ===0) q.push(nxt);
}
}
return order.length=== n ? order : [];
}
Pitfalls
Running on a graph that might not be a DAG without checking — infinite loop or wrong answer
Using DFS variant and forgetting to reverse the post-order
Canonical problems
Course Schedule
(LC 207)
Course Schedule II
(LC 210)
Alien Dictionary
(LC 269)
Minimum Height Trees
(LC 310)
Union-Find (DSU)
Time O(α(n)) per op — effectively constant
·
Space O(n)
Signals in the problem
Dynamic connectivity — "are A and B in the same group?"
Count connected components under incremental unions
Kruskal's MST, cycle detection in an undirected graph
When to use
Each element points to a parent; `find` returns the root (with path compression); `union` merges two trees (by rank). Near-O(1) per op amortized with both optimizations.
Template
Python
# Union-Find with path compression and union by rank.
if (this.rank[ra] <this.rank[rb]) [ra, rb] = [rb, ra];
this.parent[rb] = ra;
if (this.rank[ra] ===this.rank[rb]) this.rank[ra]++;
returntrue;
}
}
Pitfalls
Omitting path compression — single operations degrade to O(log n), worst-case O(n)
Reading from `parent[x]` directly instead of calling `find(x)` — gives a stale root
Canonical problems
Number of Provinces
(LC 547)
Graph Valid Tree
(LC 261)
Accounts Merge
(LC 721)
Redundant Connection
(LC 684)
Search
Backtracking
Time Exponential — O(b^d) where b = branching, d = depth
·
Space O(d) recursion depth
Signals in the problem
"Generate all combinations / permutations / subsets"
"Find all valid configurations" (N-Queens, sudoku, word break)
Constraint satisfaction over a small search space
When to use
Enumerate candidates via a recursive choose → explore → unchoose pattern. Prune aggressively; the base is usually "candidate is complete" or "candidate violates a constraint".
Template
Python
# Generate all subsets via choose / explore / unchoose.
defsubsets(nums: list[int]) -> list[list[int]]:
out: list[list[int]] = []
path: list[int] = []
defbacktrack(start: int) -> None:
out.append(path.copy())
for i inrange(start, len(nums)):
path.append(nums[i]) # choose
backtrack(i +1) # explore
path.pop() # unchoose
backtrack(0)
return out
TypeScript
// Generate all subsets via choose / explore / unchoose.
functionsubsets(nums:number[]):number[][] {
constout:number[][] = [];
constpath:number[] = [];
constbacktrack= (start:number):void=> {
out.push([...path]);
for (let i = start; i < nums.length; i++) {
path.push(nums[i]); // choose
backtrack(i +1); // explore
path.pop(); // unchoose
}
};
backtrack(0);
return out;
}
Pitfalls
Appending the mutable path to the results list instead of a copy — all outputs end up equal
Missing the unchoose step — state leaks into the next branch
No pruning — the search explodes on inputs where pruning would have been cheap
Canonical problems
Subsets
(LC 78)
Permutations
(LC 46)
Combination Sum
(LC 39)
N-Queens
(LC 51)
Stack & Heap
Monotonic Stack
Time O(n) — amortized; each index pushed and popped at most once
·
Space O(n)
Signals in the problem
"Next greater / next smaller element"
"Largest rectangle in histogram"
Problems about spans, temperatures, or visibility
When to use
Maintain a stack whose values are strictly increasing or decreasing. Pop while the incoming value breaks the invariant — the popped elements have found their answer.
Template
Python
# Next greater element for each index (-1 if none).
defnext_greater(nums: list[int]) -> list[int]:
out = [-1] *len(nums)
stack: list[int] = [] # indices with strictly-decreasing values
for i, x inenumerate(nums):
while stack and nums[stack[-1]] < x:
out[stack.pop()] = x
stack.append(i)
return out
TypeScript
// Next greater element for each index (-1 if none).
functionnextGreater(nums:number[]):number[] {
constout=newArray(nums.length).fill(-1);
conststack:number[] = []; // indices with strictly-decreasing values
for (let i =0; i < nums.length; i++) {
while (stack.length&& nums[stack[stack.length-1]] < nums[i]) {
out[stack.pop()!] = nums[i];
}
stack.push(i);
}
return out;
}
Pitfalls
Storing values instead of indices when indices are what you need for span / distance questions
Using `<` vs `<=` incorrectly — strict vs non-strict changes the behavior on duplicates
Canonical problems
Daily Temperatures
(LC 739)
Next Greater Element I
(LC 496)
Largest Rectangle in Histogram
(LC 84)
Trapping Rain Water
(LC 42)
Top-K / Heap
Time O(n log k)
·
Space O(k)
Signals in the problem
"K largest / smallest / most frequent"
Streaming median
"Merge K sorted lists"
When to use
Min-heap of size K for K largest (invariant: heap holds the K biggest seen so far). Two heaps for a streaming median (max-heap of lower half, min-heap of upper half).
Template
Python
# K largest elements — min-heap of size K.
import heapq
deftop_k(nums: list[int], k: int) -> list[int]:
heap: list[int] = []
for x in nums:
iflen(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x)
return heap # K smallest-of-the-top, unordered
TypeScript
// K largest elements via an array-backed min-heap — O(n log k).
classMinHeap {
h:number[] = [];
size() { returnthis.h.length; }
peek() { returnthis.h[0]; }
push(x:number) {
this.h.push(x);
for (let i =this.h.length-1; i >0; ) {
constp= (i -1) >>1;
if (this.h[p] <=this.h[i]) break;
[this.h[p], this.h[i]] = [this.h[i], this.h[p]];
i = p;
}
}
pop():number {
consttop=this.h[0];
constlast=this.h.pop()!;
if (this.h.length) {
this.h[0] = last;
for (let i =0; ; ) {
constl=2* i +1, r= l +1;
let m = i;
if (l <this.h.length&&this.h[l] <this.h[m]) m = l;
if (r <this.h.length&&this.h[r] <this.h[m]) m = r;
Using a full sort when a size-K heap would be O(n log k) — pointing this out signals seniority
Forgetting that Python's `heapq` is a min-heap — invert signs for a max-heap
Canonical problems
Top K Frequent Elements
(LC 347)
Kth Largest Element in an Array
(LC 215)
Merge k Sorted Lists
(LC 23)
Find Median from Data Stream
(LC 295)
Sort & Sweep
Interval Merge / Sweep
Time O(n log n) — dominated by the sort
·
Space O(n)
Signals in the problem
Inputs are ranges / intervals / events
"Merge overlapping"
"Minimum rooms / CPUs / resources"
When to use
Sort by start time, then iterate with a running accumulator. For resource-count problems, process start and end events separately as a two-pointer sweep.
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
else merged.push([start, end]);
}
return merged;
}
Pitfalls
Forgetting to sort first — correctness collapses
Treating touching intervals [1, 2] and [2, 3] as non-overlapping when the problem wants them merged
Canonical problems
Merge Intervals
(LC 56)
Insert Interval
(LC 57)
Meeting Rooms II
(LC 253)
Non-overlapping Intervals
(LC 435)
Greedy
Time O(n log n) — usually dominated by a sort
·
Space O(1)
Signals in the problem
You can justify a local choice that is always safe
Sorting the input unlocks an obvious rule (earliest deadline, smallest cost)
Interval scheduling / activity selection
When to use
Prove an exchange argument: swapping a non-greedy choice for the greedy one never makes the solution worse. Without the proof, greedy is a guess — mention you would verify with small cases.
Template
Python
# Meeting-room count: the classic greedy-over-sorted-events pattern.
conststarts= intervals.map((iv) => iv[0]).sort((a, b) => a - b);
constends= intervals.map((iv) => iv[1]).sort((a, b) => a - b);
let rooms =0, maxRooms =0, j =0;
for (constsof starts) {
if (s < ends[j]) {
rooms++;
maxRooms = Math.max(maxRooms, rooms);
} else {
j++;
}
}
return maxRooms;
}
Pitfalls
Applying greedy where DP is required — counterexample exists but is not obvious on small inputs
Sorting by the wrong key (end time vs start time) — interval scheduling needs end-time sort
Canonical problems
Assign Cookies
(LC 455)
Jump Game
(LC 55)
Gas Station
(LC 134)
Task Scheduler
(LC 621)
Dynamic Programming
Dynamic Programming
Time O(n · W) for knapsack-shape; O(n²) for LIS/LCS
·
Space O(W) rolling array
Signals in the problem
"Count the number of ways" / "Minimum cost to ..." / "Longest ..."
Optimal substructure + overlapping subproblems
Recursion with repeated subtrees
When to use
Identify the state (inputs that uniquely determine the subproblem), then the transition. Two canonical shapes: 0/1 knapsack (rolling 1-D array, iterate reverse) and LIS/LCS (2-D table or patience-sort trick).
Template
Python
# 0/1 knapsack with a 1-D rolling array.
# Key trick: iterate capacity in REVERSE so each item is used at most once.
Forward iteration in 0/1 knapsack — an item gets picked multiple times (becomes unbounded knapsack)
Getting the base case wrong — dp[0] for "at most 0 capacity" is not the same as dp[0] for "exactly 0"
Top-down memoization without hashing all state variables — wrong answers from cache collisions
Canonical problems
Climbing Stairs
(LC 70)
Coin Change
(LC 322)
Longest Increasing Subsequence
(LC 300)
Partition Equal Subset Sum
(LC 416)
— 0/1 knapsack
Edit Distance
(LC 72)
— LCS-shape
Bit Manipulation
Bit Manipulation
Time O(n) or O(n · 2ⁿ) for subset DP
·
Space O(1) or O(2ⁿ)
Signals in the problem
"Every element appears twice except one" — XOR
Subset enumeration via bitmask (n ≤ ~20)
Packing / unpacking small integers into a single machine word
When to use
When the state fits in a word and the problem has a natural XOR or bitmask structure. Bit tricks are rare in general SWE interviews but appear in systems-leaning rounds.
Template
Python
# Essential bit idioms.
defcount_bits(x: int) -> int: # popcount
c =0
while x:
x &= x -1# clears lowest set bit
c +=1
return c
deflowest_bit(x: int) -> int: # isolate lowest set bit
return x &-x
defsingle_number(nums: list[int]) -> int: # every value appears twice except one
out =0
for x in nums:
out ^= x
return out
TypeScript
// Essential bit idioms.
functioncountBits(x:number):number { // popcount
let c =0;
while (x) { x &= x -1; c++; } // clears lowest set bit
return c;
}
functionlowestBit(x:number):number { // isolate lowest set bit
return x &-x;
}
functionsingleNumber(nums:number[]):number { // every value appears twice except one
let out =0;
for (constxof nums) out ^= x;
return out;
}
Pitfalls
Mixing signed and unsigned arithmetic — right shift differs (arithmetic vs logical)
Forgetting operator precedence — `(x & mask) == y` needs parens in C-family languages