TL;DR: Most coding problems are not new problems. They are one of about ten patterns wearing a costume. This post covers the ten I reach for most often. For each one you get the signals that give it away, an interactive visualization of it running, a classic problem with a tested Python solution, and why it works, with the complexity.
Every sketch is live: watch it run, then click it to get fresh input.
Table of Contents
- Why patterns beat memorising problems
- Which pattern? A cheat sheet
- Pattern 1: Two pointers
- Pattern 2: Sliding window
- Pattern 3: Fast and slow pointers
- Pattern 4: Prefix sum and hash map
- Pattern 5: Binary search on the answer
- Pattern 6: Monotonic stack
- Pattern 7: BFS by levels
- Pattern 8: Backtracking
- Pattern 9: Top K with a heap
- Pattern 10: Dynamic programming
- Complexity at a glance
- Honourable mentions
- How to practise
Why patterns beat memorising problems
There are thousands of problems on LeetCode and only a handful of ideas underneath them. "Longest substring without repeating characters", "minimum window substring" and "max consecutive ones III" look different, but they are the same algorithm: a window that grows on the right and shrinks on the left.
So the useful skill isn't remembering 500 solutions. It's reading a problem statement and noticing the signal: contiguous, sorted, k-th largest, all combinations, minimum number of steps. Each one points at a pattern, and each pattern comes with a template you've already debugged.
Every pattern below follows the same shape:
- Signals: the words in a problem that should make you think of it.
- The idea: the one insight that makes it work.
- Visualization: a live sketch you can click.
- Sample problem: a classic, with a Python solution.
- Why it's correct and how fast it is.
All ten sample solutions in this post were run against LeetCode's own examples and thousands of random inputs checked by brute force, so you can trust them as templates.
Which pattern? A cheat sheet
Diagram from the original.
Pattern 1: Two pointers
Signals: the array is sorted, and you're looking for a pair (or triplet) that meets a target. Also: reversing in place, partitioning, or merging two sorted arrays.
The idea: put one pointer at each end. Because the array is sorted, comparing the current sum with the target tells you exactly which pointer to move. If the sum is too small, only moving the left pointer right can make it bigger. If it's too big, only moving the right pointer left can shrink it. Every comparison throws away one element for good, which turns an O(n²) pair search into O(n).
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Two Sum II (LeetCode 167)
Given a 1-indexed, sorted array
numbersand atarget, return the indices of the two numbers that add up totarget. Exactly one solution exists, and you may not use the same element twice.
numbers = [2, 7, 11, 15], target = 9→[1, 2]
def two_sum(numbers, target):
lo, hi = 0, len(numbers) - 1
while lo < hi:
s = numbers[lo] + numbers[hi]
if s == target:
return [lo + 1, hi + 1] # the problem is 1-indexed
if s < target:
lo += 1 # need a bigger sum: only lo can grow it
else:
hi -= 1 # need a smaller sum: only hi can shrink it
return []
Why it's correct: when numbers[lo] + numbers[hi] < target, pairing numbers[lo] with any element left of hi gives an even smaller sum, so numbers[lo] can't be in the answer. Discarding it is safe. The same argument mirrors for hi. O(n) time, O(1) space.
Practise: 3Sum (15), Container With Most Water (11), Valid Palindrome (125).
Pattern 2: Sliding window
Signals: contiguous subarray or substring, plus longest, shortest, at most k, or without repeating.
The idea: keep a window [L, R) that is always valid. Grow it on the right. When adding a character breaks the rule, shrink it from the left until it's valid again. Both pointers only ever move right, so the total work is linear, even though there's a loop inside a loop.
A frame from the interactive sketch in the original.
Sample problem: Longest Substring Without Repeating Characters (LeetCode 3)
Given a string
s, find the length of the longest substring without repeating characters.
"abcabcbb"→3("abc"),"pwwkew"→3("wke")
The sketch shrinks one character at a time to make the idea visible. In real code you can jump L straight past the duplicate by remembering where you last saw each character:
def length_of_longest_substring(s):
last = {} # char -> index where we last saw it
best = left = 0
for right, ch in enumerate(s):
if ch in last and last[ch] >= left:
left = last[ch] + 1 # jump past the duplicate in one move
last[ch] = right
best = max(best, right - left + 1)
return best
Why it's linear: each index enters the window once (when right reaches it) and leaves at most once (when left passes it). That's at most
pointer moves in total, so O(n) time, and O(alphabet) space for the map.
Practise: Minimum Window Substring (76), Longest Repeating Character Replacement (424), Max Consecutive Ones III (1004).
Pattern 3: Fast and slow pointers
Signals: a linked list (or anything with a next function) where you need to find a cycle, where it starts, or the middle, using O(1) extra memory.
The idea: move a slow pointer one step at a time and a fast pointer two. If there's no cycle, fast falls off the end. If there is one, fast gains one node per step inside the cycle, so it must eventually land on slow. That's Floyd's algorithm. The clever bit is phase 2: put one pointer back at the head, move both one step at a time, and they meet exactly where the cycle begins.
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Linked List Cycle II (LeetCode 142)
Given the
headof a linked list, return the node where the cycle begins, orNoneif there is no cycle. Use O(1) memory.
def detect_cycle(head):
slow = fast = head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
if slow is fast: # phase 1: they met somewhere inside the cycle
slow = head # phase 2: one pointer back to the head
while slow is not fast:
slow, fast = slow.next, fast.next
return slow # where they meet again is the cycle's entry
return None
Why phase 2 works: say the tail has nodes, the cycle has , and they meet steps past the cycle's entry. Slow walked steps and fast walked twice that. The extra distance fast covered is whole laps of the cycle:
$$
2(\mu + a) - (\mu + a) = \mu + a = m\lambda \quad\Longrightarrow\quad \mu = m\lambda - a
$$
So walking more steps from the meeting point is full laps minus , which lands exactly on the entry. A pointer walking steps from the head lands there too. O(n) time, O(1) space.
Practise: Middle of the Linked List (876), Happy Number (202), Find the Duplicate Number (287).
Pattern 4: Prefix sum and hash map
Signals: count (or find) subarrays whose sum equals k, especially when the array has negative numbers. Sliding window breaks here, because adding a negative can shrink a sum, so a window can't know which way to move.
The idea: a running prefix sum turns "sum of a range" into "difference of two prefixes":
$$
\text{sum}(i..j) = P_j - P_{i-1} = k \quad\Longleftrightarrow\quad P_{i-1} = P_j - k
$$
So as you scan, the number of subarrays ending at j with sum k is just how many earlier prefixes equal
. A hash map of prefix counts answers that in O(1).
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Subarray Sum Equals K (LeetCode 560)
Given an integer array
nums(which may contain negatives) and an integerk, return the total number of contiguous subarrays whose sum equalsk.
nums = [1, 1, 1], k = 2→2
from collections import defaultdict
def subarray_sum(nums, k):
seen = defaultdict(int)
seen[0] = 1 # the empty prefix: lets a subarray start at index 0
prefix = count = 0
for x in nums:
prefix += x
count += seen[prefix - k] # every earlier prefix p with prefix - p == k
seen[prefix] += 1
return count
The one line people forget is seen[0] = 1. Without it, a subarray that starts at index 0 has no earlier prefix to match against and is never counted. O(n) time, O(n) space.
Practise: Continuous Subarray Sum (523), Contiguous Array (525), Range Sum Query: Immutable (303).
Pattern 5: Binary search on the answer
Signals: a sorted array, obviously. But also, and more usefully: find the **minimum* speed, capacity, or size such that something is possible*. If a value works and every larger value also works, the answer space is monotonic, and you can binary search it even though nothing is sorted.
The idea: every binary search is really one question: where does a boolean flip from false to true? Write feasible(x), then shrink [lo, hi] by half each step until lo == hi. That's at most
probes instead of
.
In the sketch, each cell is a candidate eating speed k. Red means too slow and green means fast enough. You never need to check most of them:
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Koko Eating Bananas (LeetCode 875)
Koko has
pilesof bananas andhhours. Each hour she picks one pile and eatskbananas from it (or the whole pile if it has fewer). Return the minimum integerkthat lets her finish withinhhours.
piles = [3, 6, 7, 11], h = 8→4
def min_eating_speed(piles, h):
def hours(k):
return sum((p + k - 1) // k for p in piles) # ceil without floats
lo, hi = 1, max(piles)
while lo < hi:
mid = (lo + hi) // 2
if hours(mid) <= h:
hi = mid # mid works: the answer is mid or smaller
else:
lo = mid + 1 # mid is too slow: the answer is bigger
return lo
Why it's correct:
can only go down as
goes up, so feasible is false, false, …, true, true. The loop keeps the first true inside [lo, hi] at every step. O(n log m) time, where m is the largest pile.
The template to memorise is the lo < hi, hi = mid / lo = mid + 1 form. It never loops forever and it always lands on the first true.
Practise: Capacity To Ship Packages Within D Days (1011), Search in Rotated Sorted Array (33), Split Array Largest Sum (410).
Pattern 6: Monotonic stack
Signals: the next greater element, the next smaller element, how many days until warmer, the largest rectangle. Anything that asks each element about the nearest element to its right (or left) that beats it.
The idea: keep a stack of indices that are still waiting for an answer. Their values are always in decreasing order, which is what makes it "monotonic". When a new element is bigger than the top of the stack, it is the answer for the top, so pop it and record the distance. Keep popping while it's still bigger, then push the new element so it can wait too.
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Daily Temperatures (LeetCode 739)
Given daily
temperatures, return an array whereanswer[i]is the number of days after dayiuntil a warmer temperature, or0if there is none.
[73, 74, 75, 71, 69, 72, 76, 73]→[1, 1, 4, 2, 1, 1, 0, 0]
def daily_temperatures(temps):
ans = [0] * len(temps)
stack = [] # indices still waiting, temperatures decreasing
for i, t in enumerate(temps):
while stack and temps[stack[-1]] < t:
j = stack.pop()
ans[j] = i - j
stack.append(i)
return ans
Why it's O(n): there's a while inside a for, but every index is pushed once and popped at most once. That's at most
stack operations in total, so O(n) time, O(n) space, instead of the O(n²) "scan right from every day" approach.
Practise: Next Greater Element I (496), Largest Rectangle in Histogram (84), Online Stock Span (901).
Pattern 7: BFS by levels
Signals: the fewest steps, minimum moves or shortest path in a grid or a graph where every edge costs the same. Also anything that spreads one ring at a time: fire, infection, rotting.
The idea: a queue processes nodes in order of distance. Process the queue one whole level at a time (for _ in range(len(q))) and the level counter is the distance. Several starting points? Put them all in the queue at the start. That's a multi-source BFS, and it costs nothing extra.
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Rotting Oranges (LeetCode 994)
In a grid,
0is empty,1is a fresh orange and2is a rotten one. Every minute, each fresh orange next to a rotten one (up, down, left or right) becomes rotten. Return the minutes until no fresh orange is left, or-1if that's impossible.
[[2,1,1],[1,1,0],[0,1,1]]→4
from collections import deque
def oranges_rotting(grid):
rows, cols = len(grid), len(grid[0])
q, fresh = deque(), 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
q.append((r, c)) # every source starts in the queue
elif grid[r][c] == 1:
fresh += 1
minutes = 0
while q and fresh:
for _ in range(len(q)): # one whole BFS level = one minute
r, c = q.popleft()
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2
fresh -= 1
q.append((nr, nc))
minutes += 1
return -1 if fresh else minutes
Why BFS and not DFS: DFS dives down one path and finds a path, not the shortest one. BFS visits everything at distance 1 before anything at distance 2, so the first time it reaches a cell is via a shortest path. O(rows × cols) time and space.
Practise: Number of Islands (200), Shortest Path in Binary Matrix (1091), Word Ladder (127).
Pattern 8: Backtracking
Signals: return all subsets, permutations, combinations or partitions; place N queens; solve a sudoku. Any problem where the answer is every valid arrangement rather than a count of them.
The idea: every answer is a sequence of choices, so the search space is a decision tree. Walk it depth-first with one shared path: choose (append), recurse, un-choose (pop). The un-choose step is the "backtrack". It restores the state so the sibling branch starts clean. Pruning, i.e. skipping branches that can't lead to a valid answer, is what makes the harder versions fast.
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Subsets (LeetCode 78)
Given an array of unique integers
nums, return all possible subsets (the power set), in any order.
[1, 2, 3]→[[1,2,3], [1,2], [1,3], [1], [2,3], [2], [3], []]
def subsets(nums):
out, path = [], []
def dfs(i):
if i == len(nums):
out.append(path[:]) # a leaf: record a copy
return
path.append(nums[i]) # choose
dfs(i + 1)
path.pop() # un-choose
dfs(i + 1) # skip
dfs(0)
return out
Note the path[:]. Appending path itself would store the same list object eight times, and since it ends empty, you'd get eight empty lists. O(n · 2ⁿ) time: there are
subsets and copying each costs up to
. That's optimal, because the output itself is that big.
Practise: Permutations (46), Combination Sum (39), N-Queens (51).
Pattern 9: Top K with a heap
Signals: the k-th largest or smallest, the top k frequent, the k closest points, or a median of a stream. Anything that needs the best few without fully sorting.
The idea: to keep the k largest, use a min-heap of size k. Its root is the smallest of the k largest, which is exactly the k-th largest. A new number only matters if it beats the root: if it does, evict the root and push the new one; if not, ignore it. The direction feels backwards the first time: largest needs a min-heap, because the root is the gatekeeper.
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Kth Largest Element in an Array (LeetCode 215)
Given an integer array
numsand an integerk, return the k-th largest element (in sorted order, not the k-th distinct one).
nums = [3, 2, 1, 5, 6, 4], k = 2→5
import heapq
def find_kth_largest(nums, k):
heap = [] # min-heap of the k largest so far
for x in nums:
if len(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x) # pop the smallest, push x
return heap[0]
Why it beats sorting: sorting is O(n log n). The heap never holds more than k items, so each push or replace costs O(log k), for O(n log k) time and O(k) space. When k is small and n is huge, or the data is a stream you can't sort at all, that difference is the whole point.
Practise: Top K Frequent Elements (347), K Closest Points to Origin (973), Find Median from Data Stream (295).
Pattern 10: Dynamic programming
Signals: the fewest, the most, or the number of ways, where a choice now leaves a smaller version of the same problem, and those smaller problems overlap (the naive recursion keeps solving the same one again).
The idea: define what one cell means, in plain words, before writing any code. Here, dp[x] is the fewest coins that make x. Then write how a cell is built from smaller ones, and fill the table in an order where those smaller cells already exist:
$$
dp[0] = 0, \qquad dp[x] = 1 + \min_{c \,\in\, \text{coins},\ c \le x} dp[x - c]
$$
Watch each cell try every coin, look back at an earlier cell (the arc) and keep the best. When it finishes, the green arcs below trace the coins actually used:
A frame from the interactive sketch in the original — it does not render here.
Sample problem: Coin Change (LeetCode 322)
Given coin denominations
coinsand anamount, return the fewest coins that make up that amount, or-1if it can't be done. You have unlimited coins of each kind.
coins = [1, 2, 5], amount = 11→3(5 + 5 + 1)
def coin_change(coins, amount):
INF = amount + 1 # more coins than could ever be needed
dp = [0] + [INF] * amount # dp[x] = fewest coins that make x
for x in range(1, amount + 1):
for c in coins:
if c <= x:
dp[x] = min(dp[x], dp[x - c] + 1)
return dp[amount] if dp[amount] != INF else -1
Why not just be greedy? With coins [1, 3, 4] and amount 6, greedy grabs the biggest coin first: 4 + 1 + 1, three coins. The real answer is 3 + 3, two coins. Greedy decides too early. DP tries every last coin and keeps the best, so it can't be fooled. The sketch prints the greedy count whenever greedy would have lost; click until coins [1, 3, 4] come up. O(amount × coins) time, O(amount) space.
Practise: Climbing Stairs (70), House Robber (198), Longest Common Subsequence (1143).
Complexity at a glance
| # | Pattern | Classic problem | Time | Extra space |
|---|---|---|---|---|
| 1 | Two pointers | Two Sum II (167) | O(n) | O(1) |
| 2 | Sliding window | Longest Substring Without Repeating (3) | O(n) | O(alphabet) |
| 3 | Fast and slow pointers | Linked List Cycle II (142) | O(n) | O(1) |
| 4 | Prefix sum + hash map | Subarray Sum Equals K (560) | O(n) | O(n) |
| 5 | Binary search on the answer | Koko Eating Bananas (875) | O(n log m) | O(1) |
| 6 | Monotonic stack | Daily Temperatures (739) | O(n) | O(n) |
| 7 | BFS by levels | Rotting Oranges (994) | O(rows × cols) | O(rows × cols) |
| 8 | Backtracking | Subsets (78) | O(n · 2ⁿ) | O(n) + output |
| 9 | Heap (top K) | Kth Largest Element (215) | O(n log k) | O(k) |
| 10 | Dynamic programming | Coin Change (322) | O(amount × coins) | O(amount) |
Notice how many rows say O(n) where the brute force is O(n²). That's the common thread. Each pattern is a reason why most of the pairs, windows or states you'd naively check can never be the answer, and a way to skip them.
Honourable mentions
Ten covers most problems, but these show up often enough to know by name:
- Merge intervals: sort by start, then merge each interval into the last one if they overlap. Merge Intervals (56), Insert Interval (57).
- Topological sort: Kahn's algorithm, BFS over in-degrees, for "order these tasks with dependencies". Course Schedule (207).
- Union-find: near-O(1) "are these connected?" with path compression. Number of Connected Components (323), Redundant Connection (684).
- Trie: a prefix tree for "starts with" and word-search problems. Implement Trie (208).
- Two heaps: a max-heap and a min-heap balanced around the middle, for a running median. Find Median from Data Stream (295).
- Bit manipulation: XOR tricks, masks and subsets as integers. Single Number (136).
How to practise
- Learn the pattern, not the problem. For each pattern, do the sample problem above, then the three practice problems without looking anything up. If they feel like the same problem by the third one, you've got it.
- Name the signal before you code. Read the statement and say out loud which word gave it away: "contiguous", "sorted", "k-th", "all combinations", "fewest steps".
- Start from the template. Each solution above is small enough to rewrite from memory in a couple of minutes. In an interview, a correct template you trust beats a clever idea you have to debug.
- State the complexity and the reason. "O(n), because each index enters and leaves the window once" is the sentence interviewers listen for.
- Play with the sketches. Click them until the behaviour stops surprising you. Being able to predict the next step is what understanding feels like.
Happy problem solving! 🧠
Cover photo: "Swiss Army Knife" Abacus by Jccsvq, CC0, via Wikimedia Commons.


Top comments (0)