After grinding through hundreds of LeetCode problems, patterns emerge. Most algorithmic interview questions are variations of about a dozen core techniques. Learn to recognise which pattern applies, and you’re halfway to the solution.
Two Pointers
When to use: Sorted arrays, finding pairs, palindromes, in-place operations.
The idea: maintain two indices that move toward each other (or in the same direction) based on some condition.
# Find two numbers that sum to target (sorted array)
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
current = nums[left] + nums[right]
if current == target:
return [left, right]
elif current < target:
left += 1
else:
right -= 1
return []
Recognise it when: “Find a pair”, “in-place”, “sorted array”, “palindrome”.
Classic problems: Two Sum II, Container With Most Water, Trapping Rain Water, Valid Palindrome.
Sliding Window
When to use: Contiguous subarrays/substrings, finding optimal windows.
Maintain a window [left, right] and expand/contract based on conditions. Avoids O(n²) brute force.
# Longest substring without repeating characters
def length_of_longest_substring(s):
seen = {}
left = max_len = 0
for right, char in enumerate(s):
if char in seen and seen[char] >= left:
left = seen[char] + 1
seen[char] = right
max_len = max(max_len, right - left + 1)
return max_len
Recognise it when: “Contiguous subarray”, “substring”, “window of size k”, “maximum/minimum length”.
Classic problems: Maximum Sum Subarray of Size K, Longest Substring Without Repeating Characters, Minimum Window Substring.
Binary Search
When to use: Sorted data, finding boundaries, search space reduction.
Not just for “find element in sorted array” - binary search works whenever you can eliminate half the search space.
# Find first bad version
def first_bad_version(n):
left, right = 1, n
while left < right:
mid = left + (right - left) // 2
if is_bad(mid):
right = mid # bad version could be mid or earlier
else:
left = mid + 1 # first bad is after mid
return left
Recognise it when: “Sorted”, “find minimum/maximum that satisfies condition”, “search space”, “O(log n) required”.
Classic problems: Search in Rotated Sorted Array, Find Peak Element, Koko Eating Bananas, Capacity to Ship Packages.
Depth-First Search (DFS)
When to use: Trees, graphs, exploring all paths, backtracking, connected components.
Go deep before going wide. Usually implemented with recursion or an explicit stack.
# Number of islands
def num_islands(grid):
if not grid:
return 0
def dfs(i, j):
if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]):
return
if grid[i][j] != '1':
return
grid[i][j] = '0' # mark visited
dfs(i+1, j)
dfs(i-1, j)
dfs(i, j+1)
dfs(i, j-1)
count = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
dfs(i, j)
count += 1
return count
Recognise it when: “All paths”, “connected components”, “tree traversal”, “explore all possibilities”.
Classic problems: Number of Islands, Clone Graph, Path Sum, Word Search.
Breadth-First Search (BFS)
When to use: Shortest path (unweighted), level-order traversal, nearest neighbour.
Explore level by level using a queue. Guarantees shortest path in unweighted graphs.
from collections import deque
# Shortest path in binary matrix
def shortest_path(grid):
if grid[0][0] == 1:
return -1
n = len(grid)
queue = deque([(0, 0, 1)]) # (row, col, distance)
grid[0][0] = 1 # mark visited
directions = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
while queue:
row, col, dist = queue.popleft()
if row == n-1 and col == n-1:
return dist
for dr, dc in directions:
r, c = row + dr, col + dc
if 0 <= r < n and 0 <= c < n and grid[r][c] == 0:
grid[r][c] = 1
queue.append((r, c, dist + 1))
return -1
Recognise it when: “Shortest path”, “minimum steps”, “level by level”, “nearest”.
Classic problems: Binary Tree Level Order Traversal, Rotting Oranges, Word Ladder, Shortest Path in Binary Matrix.
Dynamic Programming
When to use: Overlapping subproblems, optimal substructure, counting ways, optimization.
Break the problem into smaller subproblems, cache results. Usually involves a recurrence relation.
# Coin change - minimum coins to make amount
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for x in range(coin, amount + 1):
dp[x] = min(dp[x], dp[x - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
The key questions:
- What are the subproblems? (state)
- How do subproblems relate? (transition)
- What’s the base case?
- What order to fill the table?
Recognise it when: “Number of ways”, “minimum/maximum”, “can you reach”, “longest/shortest sequence”.
Classic problems: Climbing Stairs, Coin Change, Longest Common Subsequence, House Robber, Knapsack.
Backtracking
When to use: Generate all combinations/permutations, constraint satisfaction, puzzles.
Try a choice, recurse, undo the choice (backtrack), try the next option.
# Generate all subsets
def subsets(nums):
result = []
def backtrack(start, current):
result.append(current[:]) # add a copy
for i in range(start, len(nums)):
current.append(nums[i])
backtrack(i + 1, current)
current.pop() # backtrack
backtrack(0, [])
return result
Recognise it when: “Generate all”, “all combinations”, “all permutations”, “N-Queens”, “Sudoku”.
Classic problems: Subsets, Permutations, Combination Sum, N-Queens, Word Search.
Hash Map / Set
When to use: O(1) lookup, counting, finding duplicates, two-sum style problems.
Trade space for time. When you need to check “have I seen this before?” - use a hash.
# Two Sum (unsorted)
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 []
Recognise it when: “Find duplicate”, “count occurrences”, “O(1) lookup needed”, “anagram”.
Classic problems: Two Sum, Group Anagrams, Longest Consecutive Sequence, Subarray Sum Equals K.
Monotonic Stack
When to use: Next greater/smaller element, histogram problems, maintaining order.
Stack where elements are always increasing or decreasing. Useful for “next greater element” patterns.
# Next greater element
def next_greater(nums):
result = [-1] * len(nums)
stack = [] # indices
for i, num in enumerate(nums):
while stack and nums[stack[-1]] < num:
idx = stack.pop()
result[idx] = num
stack.append(i)
return result
Recognise it when: “Next greater/smaller”, “previous greater/smaller”, “histogram”, “temperatures”.
Classic problems: Daily Temperatures, Largest Rectangle in Histogram, Trapping Rain Water.
Heap / Priority Queue
When to use: K-th largest/smallest, merge sorted lists, scheduling.
Efficient access to min or max element. Use when you repeatedly need the extreme value.
import heapq
# K-th largest element
def kth_largest(nums, k):
heap = []
for num in nums:
heapq.heappush(heap, num)
if len(heap) > k:
heapq.heappop(heap)
return heap[0]
Recognise it when: “K-th largest/smallest”, “merge K sorted”, “top K”, “median stream”.
Classic problems: Kth Largest Element, Merge K Sorted Lists, Find Median from Data Stream, Top K Frequent Elements.
Union-Find (Disjoint Set)
When to use: Connected components, cycle detection, grouping.
Efficiently track which elements belong to the same group.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # path compression
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
return True
Recognise it when: “Connected components”, “group by”, “cycle in undirected graph”, “redundant connection”.
Classic problems: Number of Connected Components, Redundant Connection, Accounts Merge.
Greedy
When to use: Local optimal choice leads to global optimal, interval scheduling.
Make the best choice at each step. Only works when the problem has greedy-choice property.
# Jump Game - can you reach the end?
def can_jump(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
Recognise it when: “Minimum number of”, “interval scheduling”, “activity selection”, often involves sorting first.
Classic problems: Jump Game, Meeting Rooms II, Task Scheduler, Gas Station.
Pattern Recognition Cheat Sheet
| Problem says… | Think… |
|---|---|
| Sorted array | Binary Search, Two Pointers |
| Contiguous subarray | Sliding Window |
| Tree/Graph traversal | DFS/BFS |
| Shortest path (unweighted) | BFS |
| All combinations/permutations | Backtracking |
| Overlapping subproblems | Dynamic Programming |
| K-th largest/smallest | Heap |
| Next greater element | Monotonic Stack |
| Connected components | Union-Find, DFS |
| O(1) lookup needed | Hash Map |
The Meta-Pattern
- Read the problem twice. What’s actually being asked?
- Identify constraints. Array size, value ranges - these hint at expected complexity.
- Match to a pattern. Which technique fits?
- Work through an example. Trace your algorithm by hand.
- Code it. Keep it clean, handle edge cases.
- Test. Empty input, single element, duplicates, large values.
The patterns become second nature with practice. You stop thinking “this is a sliding window problem” and just… see it. That’s when you know you’re ready.