Here's a secret that LeetCode power users know: there are only about 20 distinct patterns that cover the vast majority of coding interview questions. Once you recognize the pattern, the problem is half solved.
Stop grinding 500 random problems. Learn these 20 patterns, and you'll be able to tackle almost anything an interviewer throws at you.
How to Use This Guide
For each pattern, I'll cover:
- What it is - The core idea
- When to use it - The signals that tell you this pattern applies
- Template - The code skeleton you can adapt
- Classic problems - Where to practice
Pattern 1: Two Pointers
What it is: Use two pointers that move through an array or string, typically one from each end or both from the start at different speeds.
When to use it:
- Sorted array or linked list
- Finding pairs that satisfy a condition
- Removing duplicates in-place
- Comparing elements from both ends
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1
else:
right -= 1
return [-1, -1]
Classic problems: Two Sum II, Container With Most Water, Trapping Rain Water, Valid Palindrome, 3Sum
Pattern 2: Sliding Window
What it is: Maintain a window of elements that slides through an array or string, expanding and contracting to find optimal subarrays/substrings.
When to use it:
- "Longest/shortest substring with condition X"
- "Maximum sum subarray of size K"
- Contiguous sequence problems
def max_subarray_of_size_k(arr, k):
window_sum = sum(arr[:k])
max_sum = window_sum
for i in range(k, len(arr)):
window_sum += arr[i] - arr[i - k]
max_sum = max(max_sum, window_sum)
return max_sum
def longest_substring_without_repeating(s):
char_set = set()
left = 0
max_len = 0
for right in range(len(s)):
while s[right] in char_set:
char_set.remove(s[left])
left += 1
char_set.add(s[right])
max_len = max(max_len, right - left + 1)
return max_len
Classic problems: Longest Substring Without Repeating Characters, Minimum Window Substring, Max Consecutive Ones III, Fruit Into Baskets
Pattern 3: Binary Search
What it is: Divide the search space in half each iteration. Not just for sorted arrays - works on any monotonic condition.
When to use it:
- Sorted array lookup
- "Find minimum/maximum that satisfies condition"
- Search space can be divided in half
- O(log n) is expected
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# Binary search on answer space
def min_capacity_to_ship(weights, days):
left, right = max(weights), sum(weights)
while left < right:
mid = left + (right - left) // 2
if can_ship(weights, days, mid):
right = mid
else:
left = mid + 1
return left
Classic problems: Search in Rotated Sorted Array, Find Minimum in Rotated Sorted Array, Koko Eating Bananas, Capacity To Ship Packages
Pattern 4: BFS (Breadth-First Search)
What it is: Explore nodes level by level using a queue. Guarantees shortest path in unweighted graphs.
When to use it:
- Shortest path in unweighted graph
- Level-order traversal
- "Minimum number of steps/moves"
- Spreading/infection problems (rotten oranges, etc.)
from collections import deque
def bfs_shortest_path(graph, start, end):
queue = deque([(start, 0)])
visited = {start}
while queue:
node, distance = queue.popleft()
if node == end:
return distance
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, distance + 1))
return -1
def level_order_traversal(root):
if not root:
return []
result = []
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
Classic problems: Binary Tree Level Order Traversal, Rotting Oranges, Word Ladder, Shortest Path in Binary Matrix
Pattern 5: DFS (Depth-First Search)
What it is: Explore as deep as possible before backtracking. Use stack (or recursion) to track the path.
When to use it:
- Tree/graph traversal
- Path finding (all paths, any path)
- Connected components
- Detecting cycles
def dfs_recursive(graph, node, visited=None):
if visited is None:
visited = set()
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
return visited
def count_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == '0':
return
grid[r][c] = '0'
dfs(r+1, c); dfs(r-1, c); dfs(r, c+1); dfs(r, c-1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count
Classic problems: Number of Islands, Clone Graph, Path Sum, Course Schedule (cycle detection)
Pattern 6: Dynamic Programming (Bottom-Up)
What it is: Build solutions to larger problems from solutions to smaller subproblems. Store results to avoid recomputation.
When to use it:
- "Count the number of ways"
- "Find the minimum/maximum cost"
- Optimal substructure + overlapping subproblems
- Can't use greedy (no local optimal = global optimal)
# 1D DP: Climbing stairs
def climb_stairs(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1], dp[2] = 1, 2
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
# 2D DP: Longest Common Subsequence
def lcs(text1, text2):
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
Classic problems: Climbing Stairs, Coin Change, Longest Common Subsequence, Edit Distance, House Robber, 0/1 Knapsack
Pattern 7: Backtracking
What it is: Build candidates incrementally, abandoning a candidate ("backtracking") as soon as you determine it can't lead to a valid solution.
When to use it:
- Generate all permutations/combinations
- Solve constraint satisfaction (Sudoku, N-Queens)
- "Find all possible..." problems
def permutations(nums):
result = []
def backtrack(current, remaining):
if not remaining:
result.append(current[:])
return
for i in range(len(remaining)):
current.append(remaining[i])
backtrack(current, remaining[:i] + remaining[i+1:])
current.pop()
backtrack([], nums)
return result
def combination_sum(candidates, target):
result = []
def backtrack(start, current, remaining):
if remaining == 0:
result.append(current[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break
current.append(candidates[i])
backtrack(i, current, remaining - candidates[i])
current.pop()
candidates.sort()
backtrack(0, [], target)
return result
Classic problems: Permutations, Combination Sum, Subsets, N-Queens, Word Search, Palindrome Partitioning
Pattern 8: Heap / Priority Queue
What it is: A data structure that gives O(1) access to the min or max element and O(log n) insertion/deletion.
When to use it:
- "Find the K largest/smallest"
- Merge K sorted lists/arrays
- Continuous median (two heaps)
- Task scheduling by priority
import heapq
def k_largest(nums, k):
# Min-heap of size k
heap = nums[:k]
heapq.heapify(heap)
for num in nums[k:]:
if num > heap[0]:
heapq.heapreplace(heap, num)
return sorted(heap, reverse=True)
def merge_k_sorted_lists(lists):
heap = []
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst[0].val, i, lst[0]))
dummy = current = ListNode(0)
while heap:
val, i, node = heapq.heappop(heap)
current.next = node
current = current.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
Classic problems: Kth Largest Element, Merge K Sorted Lists, Find Median from Data Stream, Top K Frequent Elements
Pattern 9: Monotonic Stack
What it is: A stack that maintains elements in increasing or decreasing order. Elements are popped when they violate the monotonic property.
When to use it:
- "Next greater/smaller element"
- Stock span problems
- Largest rectangle in histogram
- Temperature/weather problems
def next_greater_element(nums):
result = [-1] * len(nums)
stack = [] # stores indices
for i in range(len(nums)):
while stack and nums[i] > nums[stack[-1]]:
result[stack.pop()] = nums[i]
stack.append(i)
return result
def daily_temperatures(temperatures):
result = [0] * len(temperatures)
stack = []
for i, temp in enumerate(temperatures):
while stack and temp > temperatures[stack[-1]]:
prev = stack.pop()
result[prev] = i - prev
stack.append(i)
return result
Classic problems: Daily Temperatures, Next Greater Element, Largest Rectangle in Histogram, Trapping Rain Water (stack approach)
Pattern 10: Hash Map Patterns
What it is: Use hash maps for O(1) lookups, frequency counting, and grouping.
When to use it:
- Two Sum (unsorted)
- Frequency counting / anagram detection
- Grouping by key
- Subarray sum equals K (prefix sum + hash map)
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
def group_anagrams(strs):
groups = defaultdict(list)
for s in strs:
key = tuple(sorted(s))
groups[key].append(s)
return list(groups.values())
def subarray_sum_equals_k(nums, k):
count = 0
prefix_sum = 0
seen = {0: 1}
for num in nums:
prefix_sum += num
count += seen.get(prefix_sum - k, 0)
seen[prefix_sum] = seen.get(prefix_sum, 0) + 1
return count
Classic problems: Two Sum, Group Anagrams, Subarray Sum Equals K, Longest Consecutive Sequence
Pattern 11: Union Find (Disjoint Set)
What it is: A data structure for tracking elements partitioned into disjoint sets. Supports near-O(1) union and find operations.
When to use it:
- Connected components in a graph
- "Are these two nodes connected?"
- Kruskal's MST algorithm
- Accounts merge / friend circles
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.components = n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
self.components -= 1
return True
Classic problems: Number of Connected Components, Redundant Connection, Accounts Merge, Longest Consecutive Sequence
Pattern 12: Trie (Prefix Tree)
What it is: A tree-like data structure for storing strings, where each node represents a character. Shared prefixes share the same path.
When to use it:
- Autocomplete / prefix matching
- Word search in a grid
- Longest common prefix
- Spell checker
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def search(self, word):
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end
def starts_with(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
Classic problems: Implement Trie, Word Search II, Design Add and Search Words, Replace Words
Pattern 13: Intervals
What it is: Problems involving ranges/intervals that may overlap, be merged, or need scheduling.
When to use it:
- Merge overlapping intervals
- Insert into sorted intervals
- Meeting room scheduling
- "Minimum number of intervals to cover"
def merge_intervals(intervals):
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
def min_meeting_rooms(intervals):
starts = sorted(i[0] for i in intervals)
ends = sorted(i[1] for i in intervals)
rooms = 0
end_ptr = 0
for start in starts:
if start < ends[end_ptr]:
rooms += 1
else:
end_ptr += 1
return rooms
Classic problems: Merge Intervals, Insert Interval, Meeting Rooms I & II, Non-overlapping Intervals, Minimum Number of Arrows
Pattern 14: Topological Sort
What it is: Linear ordering of vertices in a DAG such that for every directed edge (u, v), u comes before v.
When to use it:
- Task/course scheduling with prerequisites
- Build order / dependency resolution
- Detecting cycles in directed graphs
from collections import deque
def topological_sort(num_courses, prerequisites):
graph = defaultdict(list)
in_degree = [0] * num_courses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return order if len(order) == num_courses else []
Classic problems: Course Schedule I & II, Alien Dictionary, Sequence Reconstruction
Pattern 15: Greedy
What it is: Make the locally optimal choice at each step, hoping it leads to a globally optimal solution.
When to use it:
- Activity selection / interval scheduling
- Huffman coding
- When local optimal provably leads to global optimal
- Fractional knapsack
def jump_game(nums):
max_reach = 0
for i, jump in enumerate(nums):
if i > max_reach:
return False
max_reach = max(max_reach, i + jump)
return True
def assign_cookies(children, cookies):
children.sort()
cookies.sort()
child = cookie = 0
while child < len(children) and cookie < len(cookies):
if cookies[cookie] >= children[child]:
child += 1
cookie += 1
return child
Classic problems: Jump Game, Gas Station, Task Scheduler, Partition Labels, Assign Cookies
Pattern 16: Linked List Techniques
What it is: Common manipulation techniques for linked lists: fast/slow pointers, reversal, merge.
When to use it:
- Cycle detection (Floyd's algorithm)
- Finding middle element
- Reversing a linked list
- Merging sorted lists
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
def reverse_list(head):
prev = None
current = head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
return prev
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
Classic problems: Reverse Linked List, Linked List Cycle, Merge Two Sorted Lists, Remove Nth Node From End, Reorder List
Pattern 17: Bit Manipulation
What it is: Use bitwise operations for efficient computation and clever tricks.
When to use it:
- Single number / find duplicate
- Power of 2 checks
- Counting bits
- XOR tricks
def single_number(nums):
result = 0
for num in nums:
result ^= num
return result
def is_power_of_two(n):
return n > 0 and (n & (n - 1)) == 0
def count_bits(n):
count = 0
while n:
count += n & 1
n >>= 1
return count
Classic problems: Single Number, Number of 1 Bits, Counting Bits, Missing Number, Reverse Bits
Pattern 18: Graph - Shortest Path (Dijkstra's)
What it is: Find shortest paths from a source to all other vertices in a weighted graph with non-negative edges.
When to use it:
- Weighted graph shortest path
- Network delay time
- Cheapest flights with K stops (modified)
import heapq
def dijkstra(graph, start, n):
dist = [float('inf')] * n
dist[start] = 0
heap = [(0, start)]
while heap:
d, node = heapq.heappop(heap)
if d > dist[node]:
continue
for neighbor, weight in graph[node]:
new_dist = d + weight
if new_dist < dist[neighbor]:
dist[neighbor] = new_dist
heapq.heappush(heap, (new_dist, neighbor))
return dist
Classic problems: Network Delay Time, Cheapest Flights Within K Stops, Path with Minimum Effort, Swim in Rising Water
Pattern 19: Prefix Sum
What it is: Precompute cumulative sums to answer range sum queries in O(1).
When to use it:
- Range sum queries
- Subarray sum problems
- 2D matrix sum queries
- Finding equilibrium index
def range_sum(nums):
prefix = [0] * (len(nums) + 1)
for i in range(len(nums)):
prefix[i + 1] = prefix[i] + nums[i]
# Sum of nums[i:j+1] = prefix[j+1] - prefix[i]
return prefix
def pivot_index(nums):
total = sum(nums)
left_sum = 0
for i, num in enumerate(nums):
if left_sum == total - left_sum - num:
return i
left_sum += num
return -1
Classic problems: Range Sum Query, Product of Array Except Self, Subarray Sum Equals K, Contiguous Array
Pattern 20: Tree DFS Patterns
What it is: Specific DFS patterns on binary trees - inorder, preorder, postorder, and common tree problems.
When to use it:
- Tree traversals
- Tree validation (BST)
- Lowest common ancestor
- Maximum depth / diameter
def max_depth(root):
if not root:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
def is_valid_bst(root, min_val=float('-inf'), max_val=float('inf')):
if not root:
return True
if root.val <= min_val or root.val >= max_val:
return False
return (is_valid_bst(root.left, min_val, root.val) and
is_valid_bst(root.right, root.val, max_val))
def lowest_common_ancestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
if left and right:
return root
return left or right
Classic problems: Maximum Depth, Validate BST, Lowest Common Ancestor, Binary Tree Maximum Path Sum, Serialize and Deserialize Binary Tree
Study Plan: 6-Week Pattern Mastery
| Week | Patterns | Problems/Day |
|---|---|---|
| 1 | Two Pointers, Sliding Window, Binary Search | 2-3 easy |
| 2 | BFS, DFS, Tree DFS | 2-3 easy/medium |
| 3 | Hash Map, Prefix Sum, Monotonic Stack | 2-3 medium |
| 4 | DP, Backtracking, Greedy | 2-3 medium |
| 5 | Heap, Intervals, Topo Sort, Union Find | 2-3 medium |
| 6 | Trie, Linked List, Bit Manipulation, Dijkstra's | 2-3 medium/hard |
The Pattern Recognition Trick
When you see a new problem, ask yourself these questions in order:
- Is the input sorted? → Two Pointers or Binary Search
- Is it about a subarray/substring? → Sliding Window or Prefix Sum
- Is it a tree or graph? → BFS or DFS
- Is it about optimization? → DP or Greedy
- Is it about generating all possibilities? → Backtracking
- Does it involve intervals? → Sort by start, use Intervals pattern
- Is it "Top K" or "Kth largest"? → Heap
- Is it about ordering with dependencies? → Topological Sort
- Is it about connected groups? → Union Find or DFS
- Is it about prefix matching? → Trie
Master these patterns, and you'll walk into any coding interview with confidence.