Skip to content

Design Patterns for LeetCode-Style Problems

Posted on:February 18, 2026
· 8 min read

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.

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:

  1. What are the subproblems? (state)
  2. How do subproblems relate? (transition)
  3. What’s the base case?
  4. 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 arrayBinary Search, Two Pointers
Contiguous subarraySliding Window
Tree/Graph traversalDFS/BFS
Shortest path (unweighted)BFS
All combinations/permutationsBacktracking
Overlapping subproblemsDynamic Programming
K-th largest/smallestHeap
Next greater elementMonotonic Stack
Connected componentsUnion-Find, DFS
O(1) lookup neededHash Map

The Meta-Pattern

  1. Read the problem twice. What’s actually being asked?
  2. Identify constraints. Array size, value ranges - these hint at expected complexity.
  3. Match to a pattern. Which technique fits?
  4. Work through an example. Trace your algorithm by hand.
  5. Code it. Keep it clean, handle edge cases.
  6. 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.