LeetCode patterns are the reason two people who have solved the same number of problems can walk into the same interview with completely different odds. One has a catalogue of solutions and hopes the interviewer picks from it; the other has about twenty reusable techniques and spends three minutes deciding which one the constraints are asking for. This is a map of the second thing: twenty-two patterns, each with its trigger, a short Python template, and three problems to practise on.
Why the pattern is the unit of study#
A problem you have seen before is worth almost nothing in an interview, because the interviewer will change something. A pattern survives the change. It also changes what you can say when you are stuck: "I don't recognise this" is a dead end, while "this wants the longest contiguous stretch satisfying a condition and the constraints rule out O(n²), so it is a window" is a plan the interviewer can hint against.
How to read this map#
Each entry starts with its trigger — what in the statement or the constraints should make you reach for it. Read the constraints first, and ask for the bound on n out loud if it is missing; it is the cheapest hint in the interview. When two patterns fit, name both and pick the one whose correctness you can argue. A condensed lookup version lives in the coding interview cheat sheet.
The LeetCode patterns for arrays and strings#
These seven cover more interview problems than the other fifteen combined.
Hash map#
Trigger: a nested loop asking "is there another element such that…", or any counting, grouping or de-duplication.
def group_by_signature(words):
groups = {}
for word in words:
key = "".join(sorted(word))
groups.setdefault(key, []).append(word)
return list(groups.values())The skill is choosing a key that collides exactly when two inputs belong together. O(n·k log k) for n words of length k.
Practise: Two Sum · Group Anagrams · Longest Consecutive Sequence · more
Two pointers#
Trigger: the input is sorted or can be, and you want a pair, a partition point, or an in-place rewrite in O(1) space.
def remove_duplicates(nums):
if not nums:
return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[write - 1]:
nums[write] = nums[read]
write += 1
return writeThe comparison says which pointer cannot be in the answer, so moving it discards a whole family of candidates. O(n) time, O(1) space.
Practise: Valid Palindrome · 3Sum · Container With Most Water · more
Sliding window#
Trigger: the answer is a contiguous stretch, and the brute force recomputes overlapping ranges.
def max_sum_of_length_k(nums, k):
window = sum(nums[:k])
best = window
for right in range(k, len(nums)):
window += nums[right] - nums[right - k]
best = max(best, window)
return bestThe running summary updates in O(1) as an index moves, which is what collapses the nested loop. O(n).
Practise: Longest Substring Without Repeating Characters · Longest Repeating Character Replacement · Minimum Window Substring · more
Prefix sum#
Trigger: many range queries over an array that does not change, or counting subarrays that hit a target.
def build_prefix(nums):
prefix = [0]
for value in nums:
prefix.append(prefix[-1] + value)
return prefix
def range_sum(prefix, left, right):
return prefix[right + 1] - prefix[left]Build once, then every range query is a subtraction. Build O(n), query O(1).
Practise: Subarray Sum Equals K · Range Sum Query - Immutable · Contiguous Array · more
Binary search#
Trigger: a sorted array, or a numeric answer in a known range where checking a candidate is cheaper than computing it.
def lower_bound(nums, target):
low, high = 0, len(nums)
while low < high:
mid = (low + high) // 2
if nums[mid] < target:
low = mid + 1
else:
high = mid
return lowSearch for a boundary, not a value, and the off-by-one errors disappear: the loop ends when the bounds meet. O(log n).
Practise: Binary Search · Search in Rotated Sorted Array · Koko Eating Bananas · more
Sorting#
Trigger: adjacency in sorted order makes the answer visible — overlaps, duplicates, closest pairs — or a greedy choice needs an order.
def reconstruct_queue(people):
people.sort(key=lambda person: (-person[0], person[1]))
queue = []
for person in people:
queue.insert(person[1], person)
return queueThe insight is in the key, never the sort. O(n log n), plus whatever the pass costs.
Practise: Merge Intervals · Largest Number · Queue Reconstruction by Height · more
Greedy#
Trigger: a local choice can be shown never to rule out an optimal completion — usually by an exchange argument.
def can_reach_end(nums):
furthest = 0
for i, jump in enumerate(nums):
if i > furthest:
return False
furthest = max(furthest, i + jump)
return TrueThe code is three lines; the work is the proof. If the exchange argument fails, this is dynamic programming in disguise. O(n).
Practise: Jump Game · Jump Game II · Partition Labels · more
Patterns for stacks, queues and lists#
Stack#
Trigger: the input nests, or a new element resolves against the most recent unmatched one.
from operator import add, sub, mul
OPS = {"+": add, "-": sub, "*": mul, "/": lambda a, b: int(a / b)}
def eval_rpn(tokens):
stack = []
for token in tokens:
if token in OPS:
right, left = stack.pop(), stack.pop()
stack.append(OPS[token](left, right))
else:
stack.append(int(token))
return stack[-1]Both failure modes matter: a close on an empty stack, and items left at the end. O(n).
Practise: Valid Parentheses · Evaluate Reverse Polish Notation · Simplify Path · more
Monotonic stack#
Trigger: the question is about the nearest larger or smaller element, or a span between a position and the first position that beats it.
def days_until_warmer(temperatures):
answer = [0] * len(temperatures)
stack = []
for i, degrees in enumerate(temperatures):
while stack and temperatures[stack[-1]] < degrees:
previous = stack.pop()
answer[previous] = i - previous
stack.append(i)
return answerEvery pop resolves one answer. Each index is pushed and popped once, so it is O(n) despite the inner loop.
Practise: Daily Temperatures · Largest Rectangle in Histogram · Trapping Rain Water · more
Linked list#
Trigger: nodes must be reordered in place, or you need a midpoint, a cycle check, or the nth node from the end in one pass.
def middle_node(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slowTwo habits remove most bugs: a dummy node when the head can change, and saving next before overwriting it. O(n) time, O(1) space.
Practise: Reverse Linked List · Linked List Cycle · Reorder List · more
Patterns for trees and graphs#
Tree traversal#
Trigger: a binary tree, plus a decision about when the node is processed relative to its children.
def height(node):
if node is None:
return 0
return 1 + max(height(node.left), height(node.right))Postorder when a node's answer depends on its subtrees, inorder for sorted order on a BST, preorder when information flows down from the root. O(n).
Practise: Binary Tree Inorder Traversal · Validate Binary Search Tree · Diameter of Binary Tree · more
Depth-first search#
Trigger: you need everything reachable from a node — a component, a region, a set of paths.
def count_components(graph):
seen = set()
def visit(node):
seen.add(node)
for neighbour in graph[node]:
if neighbour not in seen:
visit(neighbour)
components = 0
for node in graph:
if node not in seen:
components += 1
visit(node)
return componentsMark nodes on discovery, never on pop. Directed-graph cycle detection needs three states: finished is not the same as on the current path. O(V + E).
Practise: Clone Graph · Pacific Atlantic Water Flow · Binary Tree Maximum Path Sum · more
Breadth-first search#
Trigger: the minimum number of steps on an unweighted graph, or an answer that is per level.
from collections import deque
def level_order(root):
if root is None:
return []
levels, queue = [], deque([root])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
for child in (node.left, node.right):
if child:
queue.append(child)
levels.append(level)
return levelsDraining exactly len(queue) nodes separates one level from the next. First arrival is the shortest path. O(V + E).
Practise: Binary Tree Level Order Traversal · Rotting Oranges · 01 Matrix · more
Matrix and grid#
Trigger: cells connected to their neighbours, or pure index mechanics — rotate, spiral, transpose.
def rotate_in_place(matrix):
n = len(matrix)
for r in range(n):
for c in range(r + 1, n):
matrix[r][c], matrix[c][r] = matrix[c][r], matrix[r][c]
for row in matrix:
row.reverse()A grid is a graph nobody built: write the neighbour offsets as a list and DFS, BFS and union-find apply unchanged. O(n²) here.
Practise: Number of Islands · Spiral Matrix · Rotate Image · more
Topological sort#
Trigger: prerequisites, dependencies, build order — or a question about whether a directed graph has a cycle.
def topological_order(graph):
"""Assumes a DAG. Cycle detection needs a third node state."""
seen, order = set(), []
def visit(node):
seen.add(node)
for neighbour in graph[node]:
if neighbour not in seen:
visit(neighbour)
order.append(node)
for node in graph:
if node not in seen:
visit(node)
return order[::-1]Appending after the descendants finish, then reversing, gives a valid order; cycles need a third state — see topological sort explained. O(V + E).
Practise: Course Schedule · Course Schedule II · Alien Dictionary · more
Union-find#
Trigger: edges arrive one at a time and you need connectivity, component counts, or "does this edge close a cycle".
def find(parent, x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(parent, a, b):
root_a, root_b = find(parent, a), find(parent, b)
if root_a == root_b:
return False
parent[root_b] = root_a
return Trueunion returning False means "already connected", which is the cycle test. Path compression keeps the trees flat; adding union by size gives the inverse-Ackermann bound.
Practise: Number of Provinces · Redundant Connection · Accounts Merge · more
Trie#
Trigger: queries about prefixes rather than whole words, or a search that should be pruned by a dictionary.
END = "$"
def build_trie(words):
root = {}
for word in words:
node = root
for char in word:
node = node.setdefault(char, {})
node[END] = True
return rootLookup costs the length of the word, not the size of the dictionary. O(total characters) to build.
Practise: Implement Trie (Prefix Tree) · Design Add and Search Words Data Structure · Word Search II · more
Patterns for search, state and numbers#
Heap and priority queue#
Trigger: the k largest, smallest or closest, or a loop that always takes the cheapest item.
from heapq import heappush, heappushpop
def k_closest_to_origin(points, k):
heap = []
for x, y in points:
item = (-(x * x + y * y), x, y)
if len(heap) < k:
heappush(heap, item)
else:
heappushpop(heap, item)
return [[x, y] for _, x, y in heap]A max-heap in Python is a min-heap of negated keys. Capping at k gives O(n log k), not O(n log n).
Practise: Kth Largest Element in an Array · K Closest Points to Origin · Find Median from Data Stream · more
Backtracking#
Trigger: the problem asks for the arrangements themselves — subsets, permutations, partitions, placements — and n is small.
def permutations(nums):
result, path, used = [], [], [False] * len(nums)
def explore():
if len(path) == len(nums):
result.append(path[:])
return
for i, value in enumerate(nums):
if used[i]:
continue
used[i] = True
path.append(value)
explore()
path.pop()
used[i] = False
explore()
return resultChoose, explore, un-choose — and copy the path when recording it, or later mutations corrupt the answer. Prune before the recursive call, not at the leaf.
Practise: Subsets · Permutations · Combination Sum · more
Dynamic programming#
Trigger: a count, maximum, minimum or feasibility question where greedy is provably wrong and the naive recursion repeats itself.
def coin_change(coins, amount):
best = [0] + [float("inf")] * amount
for value in range(1, amount + 1):
for coin in coins:
if coin <= value:
best[value] = min(best[value], best[value - coin] + 1)
return -1 if best[amount] == float("inf") else best[amount]All the work is in choosing the state. Write it as cached recursion first, convert to a table once it works. O(amount · len(coins)).
Practise: Climbing Stairs · Coin Change · Longest Common Subsequence · more
Bit manipulation#
Trigger: every element appears a fixed number of times except one, or n is at most about twenty and you need to iterate over subsets.
def all_subsets(nums):
result = []
for mask in range(1 << len(nums)):
subset = [nums[i] for i in range(len(nums)) if mask >> i & 1]
result.append(subset)
return resultAn integer's bits are a set. x & (x - 1) clears the lowest set bit, x & -x isolates it, XOR cancels pairs. O(2^n · n) here.
Practise: Single Number · Number of 1 Bits · Counting Bits · more
Math and number theory#
Trigger: constraints far too large to iterate, or a statement about digits, divisors, primes or remainders.
def primes_below(limit):
if limit < 3:
return []
sieve = [True] * limit
sieve[0] = sieve[1] = False
for candidate in range(2, int(limit ** 0.5) + 1):
if sieve[candidate]:
for multiple in range(candidate * candidate, limit, candidate):
sieve[multiple] = False
return [i for i, is_prime in enumerate(sieve) if is_prime]Compute the first six answers by hand and look for the closed form or the invariant. O(n log log n).
Practise: Pow(x, n) · Happy Number · Count Primes · more
The order to learn them in#
Not the order above — learn them in the order that makes the next one easier. First the array and string seven: most common, easiest to check by hand, and hash map and two pointers turn up inside everything later. Second stack, linked list and tree traversal, all of which are pointer and order discipline, and where tree recursion becomes the on-ramp to graphs. Third the graph family — DFS, BFS, grids, topological sort, union-find, trie — starting with grids, the friendliest graphs because you can draw them. Last backtracking and dynamic programming, with heap, bit manipulation and math alongside; attempting those two first is the most common reason people stall.
Three to five problems per pattern, spread over weeks, is enough for recognition to become automatic. For a ready-made sequence the Blind 75 walks most of this map in roughly that order, the NeetCode 150 covers the same ground with more repetition, and the full problem index is filterable if you would rather assemble your own set.
If you want a second pair of eyes during the real thing, Stealth Interview is a desktop app for macOS and Windows that reads the problem from a screenshot and works through it with you — the pattern, the code, and the time and space complexity — while staying invisible to screen sharing.
Sources
[1] Stealth Interview — LeetCode Patterns: The 22 That Cover the Problem Set
Republished from the publisher’s Stealth Interview blog. Original publication: 2026-08-13. Product references in the article describe the source site.
Practice for your next interview.
Discuss your goals and build a training plan around the questions you need to answer.
Apply for a consultation