- Published on
The Complete Guide to LeetCode Problem-Solving Patterns
- Authors

- Name
- Yinhuan Yuan
The Complete Guide to LeetCode Problem-Solving Patterns
Introduction: Why Patterns Matter
LeetCode problems often seem overwhelming at first, but the secret is that most problems are variations of well-established patterns. Once you internalize these patterns, you can quickly identify which approach to use and adapt it to the specific problem. This guide covers the essential patterns you need to master.
Core Problem-Solving Patterns
1. Two Pointers Pattern
When to Use:
- Working with sorted arrays or linked lists
- Finding pairs with specific properties
- Removing duplicates in-place
- Palindrome problems
Core Concept: Use two pointers moving through the data structure, often from opposite ends or at different speeds.
Variations:
a) Opposite Direction Two Pointers
def two_sum_sorted(arr, target):
left, right = 0, len(arr) - 1
while left < right:
current_sum = arr[left] + arr[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1
else:
right -= 1
return []
b) Fast and Slow Pointers (Floyd's Algorithm)
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
Key Problems:
- Container With Most Water
- 3Sum/4Sum
- Remove Nth Node From End
- Linked List Cycle Detection
- Find the Duplicate Number
2. Sliding Window Pattern
When to Use:
- Contiguous subarrays/substrings
- Finding optimal windows with specific properties
- String manipulation problems
Core Concept: Maintain a window that expands and contracts based on conditions.
Fixed Size Window:
def max_sum_subarray(arr, k):
window_sum = sum(arr[:k])
max_sum = window_sum
for i in range(k, len(arr)):
window_sum = window_sum - arr[i-k] + arr[i]
max_sum = max(max_sum, window_sum)
return max_sum
Variable Size Window:
def longest_substring_k_distinct(s, k):
window_start = 0
max_length = 0
char_frequency = {}
for window_end in range(len(s)):
right_char = s[window_end]
char_frequency[right_char] = char_frequency.get(right_char, 0) + 1
while len(char_frequency) > k:
left_char = s[window_start]
char_frequency[left_char] -= 1
if char_frequency[left_char] == 0:
del char_frequency[left_char]
window_start += 1
max_length = max(max_length, window_end - window_start + 1)
return max_length
Key Problems:
- Longest Substring Without Repeating Characters
- Minimum Window Substring
- Fruit Into Baskets
- Permutation in String
3. Binary Search Pattern
When to Use:
- Sorted arrays
- Finding boundaries
- Optimization problems with monotonic properties
- Search in rotated arrays
Core Template:
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
Advanced: Binary Search on Answer Space
def min_eating_speed(piles, h):
def can_eat_all(speed):
hours = 0
for pile in piles:
hours += (pile + speed - 1) // speed
return hours <= h
left, right = 1, max(piles)
while left < right:
mid = left + (right - left) // 2
if can_eat_all(mid):
right = mid
else:
left = mid + 1
return left
Key Problems:
- Search in Rotated Sorted Array
- Find Peak Element
- Koko Eating Bananas
- Capacity to Ship Packages
4. Tree Traversal Patterns
When to Use:
- Binary tree problems
- Path finding
- Tree construction/validation
DFS Patterns:
a) Preorder (Root → Left → Right)
def preorder(root):
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
b) Bottom-up DFS (for diameter, height problems)
def tree_diameter(root):
def dfs(node):
if not node:
return 0
left_height = dfs(node.left)
right_height = dfs(node.right)
# Update global diameter
self.diameter = max(self.diameter, left_height + right_height)
# Return height
return 1 + max(left_height, right_height)
self.diameter = 0
dfs(root)
return self.diameter
BFS Pattern:
def level_order(root):
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(current_level)
return result
5. Dynamic Programming Patterns
When to Use:
- Optimization problems (min/max)
- Count distinct ways
- Decision making problems
- Problems with overlapping subproblems
a) Linear DP
def climb_stairs(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
b) 2D DP (Grid Problems)
def unique_paths(m, n):
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
c) DP with State Compression
def rob(nums):
if not nums:
return 0
if len(nums) == 1:
return nums[0]
prev2 = nums[0]
prev1 = max(nums[0], nums[1])
for i in range(2, len(nums)):
current = max(prev1, prev2 + nums[i])
prev2 = prev1
prev1 = current
return prev1
d) Knapsack Pattern
def can_partition(nums):
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for j in range(target, num - 1, -1):
dp[j] = dp[j] or dp[j - num]
return dp[target]
6. Backtracking Pattern
When to Use:
- Generate all combinations/permutations
- Find all solutions
- Constraint satisfaction problems
Core Template:
def backtrack(path, remaining, result):
if is_solution(path):
result.append(path[:]) # Make a copy
return
for choice in get_choices(remaining):
if is_valid(choice):
# Make choice
path.append(choice)
# Recurse
backtrack(path, update_remaining(remaining, choice), result)
# Undo choice
path.pop()
Permutations Example:
def permute(nums):
result = []
def backtrack(path, remaining):
if not remaining:
result.append(path[:])
return
for i in range(len(remaining)):
path.append(remaining[i])
backtrack(path, remaining[:i] + remaining[i+1:])
path.pop()
backtrack([], nums)
return result
N-Queens Example:
def solve_n_queens(n):
def is_valid(board, row, col):
# Check column
for i in range(row):
if board[i][col] == 'Q':
return False
# Check upper-left diagonal
i, j = row - 1, col - 1
while i >= 0 and j >= 0:
if board[i][j] == 'Q':
return False
i -= 1
j -= 1
# Check upper-right diagonal
i, j = row - 1, col + 1
while i >= 0 and j < n:
if board[i][j] == 'Q':
return False
i -= 1
j += 1
return True
def backtrack(board, row):
if row == n:
result.append([''.join(row) for row in board])
return
for col in range(n):
if is_valid(board, row, col):
board[row][col] = 'Q'
backtrack(board, row + 1)
board[row][col] = '.'
result = []
board = [['.' for _ in range(n)] for _ in range(n)]
backtrack(board, 0)
return result
7. Graph Patterns
When to Use:
- Network problems
- Connected components
- Shortest path problems
- Cycle detection
BFS for Shortest Path:
def 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
DFS for Connected Components:
def count_components(n, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
visited = set()
components = 0
for i in range(n):
if i not in visited:
dfs(i)
components += 1
return components
Union-Find Pattern:
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
# Union by rank
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
8. Heap/Priority Queue Pattern
When to Use:
- K-th largest/smallest problems
- Merge K sorted lists
- Schedule/interval problems
- Dijkstra's algorithm
Top K Elements:
def find_kth_largest(nums, k):
# Min heap of size k
heap = nums[:k]
heapq.heapify(heap)
for num in nums[k:]:
if num > heap[0]:
heapq.heappushpop(heap, num)
return heap[0]
Merge K Sorted Lists:
def merge_k_lists(lists):
heap = []
# Initialize heap with first element from each list
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst.val, i, lst))
dummy = ListNode()
current = dummy
while heap:
val, idx, node = heapq.heappop(heap)
current.next = node
current = current.next
if node.next:
heapq.heappush(heap, (node.next.val, idx, node.next))
return dummy.next
9. Monotonic Stack Pattern
When to Use:
- Next greater/smaller element
- Largest rectangle problems
- Temperature/stock span problems
Next Greater Element:
def next_greater_element(nums):
result = [-1] * len(nums)
stack = [] # Stores 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
Largest Rectangle in Histogram:
def largest_rectangle(heights):
stack = []
max_area = 0
for i, h in enumerate(heights):
start = i
while stack and stack[-1][1] > h:
idx, height = stack.pop()
max_area = max(max_area, height * (i - idx))
start = idx
stack.append((start, h))
for idx, h in stack:
max_area = max(max_area, h * (len(heights) - idx))
return max_area
10. Bit Manipulation Pattern
When to Use:
- Problems involving powers of 2
- XOR properties
- Finding single/missing numbers
- Subset generation
Key Operations:
# Check if bit is set
def is_bit_set(num, pos):
return (num & (1 << pos)) != 0
# Set bit
def set_bit(num, pos):
return num | (1 << pos)
# Clear bit
def clear_bit(num, pos):
return num & ~(1 << pos)
# Toggle bit
def toggle_bit(num, pos):
return num ^ (1 << pos)
# Count set bits (Brian Kernighan's algorithm)
def count_bits(n):
count = 0
while n:
n &= n - 1
count += 1
return count
Single Number (XOR Property):
def single_number(nums):
result = 0
for num in nums:
result ^= num
return result
Pattern Recognition Strategy
Step 1: Identify the Problem Type
- Array/String: Consider two pointers, sliding window, or prefix sum
- Tree/Graph: Think traversal (DFS/BFS) or dynamic programming on trees
- Optimization: Consider DP, greedy, or binary search on answer
- Combinations/Permutations: Use backtracking
- Interval/Scheduling: Think greedy or heap
Step 2: Look for Keywords
- "Subarray/Substring" → Sliding Window or Prefix Sum
- "K-th largest/smallest" → Heap or Quick Select
- "All possible ways" → Backtracking or DP
- "Shortest/Longest path" → BFS or DP
- "Maximum/Minimum" → DP, Greedy, or Binary Search
- "Sorted array" → Binary Search or Two Pointers
Step 3: Consider Constraints
- Small input (n < 20) → Brute force or backtracking possible
- Medium input (20 < n < 10^4) → O(n²) solutions work
- Large input (n > 10^5) → Need O(n) or O(n log n) solution
Advanced Pattern Combinations
Two Pointers + Sliding Window
Used in problems like "Longest Substring with At Most K Distinct Characters"
Binary Search + Greedy
Common in optimization problems like "Minimum Days to Make M Bouquets"
DP + Bit Manipulation
Used in subset problems with special constraints
BFS + Backtracking
Path finding with multiple valid solutions
Practice Strategy
Phase 1: Pattern Mastery (Weeks 1-4)
- Focus on one pattern per week
- Solve 5-10 easy/medium problems per pattern
- Implement the pattern from scratch each time
Phase 2: Pattern Recognition (Weeks 5-8)
- Mixed problem sets
- Focus on identifying which pattern to use
- Time yourself on pattern identification (aim for < 5 minutes)
Phase 3: Advanced Applications (Weeks 9-12)
- Hard problems combining multiple patterns
- Contest participation
- System design problems using these patterns
Daily Routine
- Warm-up (15 min): One easy problem from a familiar pattern
- Learning (45 min): One new medium problem, implement without looking at solution
- Review (30 min): Understand optimal solution, identify pattern variations
- Reflection (10 min): Document pattern insights in your notes
Common Pitfalls and How to Avoid Them
Pitfall 1: Jumping to Code Too Quickly
Solution: Spend 10-15 minutes understanding the problem and identifying patterns
Pitfall 2: Not Considering Edge Cases
Solution: Always test with:
- Empty input
- Single element
- All same elements
- Sorted/reverse sorted input
Pitfall 3: Overcomplicating Solutions
Solution: Start with brute force, then optimize. Most problems don't need complex data structures.
Pitfall 4: Not Learning from Failed Attempts
Solution: Keep a mistake journal. Document why your approach failed and what pattern would have worked.
Conclusion
Mastering these patterns transforms LeetCode from a daunting challenge into a systematic skill. Remember:
- Patterns are tools, not rules - Adapt them to specific problems
- Practice recognition - The faster you identify patterns, the more time for implementation
- Understand trade-offs - Know when to use which pattern based on constraints
- Build incrementally - Start with basic implementations, then add optimizations
With consistent practice using this pattern-based approach, you'll find that even unfamiliar problems become variations of patterns you already know. The key is deliberate practice with a focus on understanding why each pattern works, not just memorizing solutions.