DEV Community

Cover image for 10 Algorithm Patterns That Crack Most Coding Problems, Visualized
Ravi Kishan
Ravi Kishan

Posted on Originally published at ravikishan.me

10 Algorithm Patterns That Crack Most Coding Problems, Visualized

TL;DR: Most coding problems are not new problems. They are one of about ten patterns wearing a costume. This post covers the ten I reach for most often. For each one you get the signals that give it away, an interactive visualization of it running, a classic problem with a tested Python solution, and why it works, with the complexity.

Every sketch is live: watch it run, then click it to get fresh input.


Table of Contents

  1. Why patterns beat memorising problems
  2. Which pattern? A cheat sheet
  3. Pattern 1: Two pointers
  4. Pattern 2: Sliding window
  5. Pattern 3: Fast and slow pointers
  6. Pattern 4: Prefix sum and hash map
  7. Pattern 5: Binary search on the answer
  8. Pattern 6: Monotonic stack
  9. Pattern 7: BFS by levels
  10. Pattern 8: Backtracking
  11. Pattern 9: Top K with a heap
  12. Pattern 10: Dynamic programming
  13. Complexity at a glance
  14. Honourable mentions
  15. How to practise

Why patterns beat memorising problems

There are thousands of problems on LeetCode and only a handful of ideas underneath them. "Longest substring without repeating characters", "minimum window substring" and "max consecutive ones III" look different, but they are the same algorithm: a window that grows on the right and shrinks on the left.

So the useful skill isn't remembering 500 solutions. It's reading a problem statement and noticing the signal: contiguous, sorted, k-th largest, all combinations, minimum number of steps. Each one points at a pattern, and each pattern comes with a template you've already debugged.

Every pattern below follows the same shape:

  • Signals: the words in a problem that should make you think of it.
  • The idea: the one insight that makes it work.
  • Visualization: a live sketch you can click.
  • Sample problem: a classic, with a Python solution.
  • Why it's correct and how fast it is.

All ten sample solutions in this post were run against LeetCode's own examples and thousands of random inputs checked by brute force, so you can trust them as templates.

Which pattern? A cheat sheet

mermaid diagram

Diagram from the original.

Pattern 1: Two pointers

Signals: the array is sorted, and you're looking for a pair (or triplet) that meets a target. Also: reversing in place, partitioning, or merging two sorted arrays.

The idea: put one pointer at each end. Because the array is sorted, comparing the current sum with the target tells you exactly which pointer to move. If the sum is too small, only moving the left pointer right can make it bigger. If it's too big, only moving the right pointer left can shrink it. Every comparison throws away one element for good, which turns an O(n²) pair search into O(n).

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Two Sum II (LeetCode 167)

Given a 1-indexed, sorted array numbers and a target, return the indices of the two numbers that add up to target. Exactly one solution exists, and you may not use the same element twice.

numbers = [2, 7, 11, 15], target = 9 → [1, 2]

def two_sum(numbers, target):
    lo, hi = 0, len(numbers) - 1
    while lo < hi:
        s = numbers[lo] + numbers[hi]
        if s == target:
            return [lo + 1, hi + 1]  # the problem is 1-indexed
        if s < target:
            lo += 1  # need a bigger sum: only lo can grow it
        else:
            hi -= 1  # need a smaller sum: only hi can shrink it
    return []
Enter fullscreen mode Exit fullscreen mode

Why it's correct: when numbers[lo] + numbers[hi] < target, pairing numbers[lo] with any element left of hi gives an even smaller sum, so numbers[lo] can't be in the answer. Discarding it is safe. The same argument mirrors for hi. O(n) time, O(1) space.

Practise: 3Sum (15), Container With Most Water (11), Valid Palindrome (125).

Pattern 2: Sliding window

Signals: contiguous subarray or substring, plus longest, shortest, at most k, or without repeating.

The idea: keep a window [L, R) that is always valid. Grow it on the right. When adding a character breaks the rule, shrink it from the left until it's valid again. Both pointers only ever move right, so the total work is linear, even though there's a loop inside a loop.

p5 visualisation

A frame from the interactive sketch in the original.

Sample problem: Longest Substring Without Repeating Characters (LeetCode 3)

Given a string s, find the length of the longest substring without repeating characters.

"abcabcbb" → 3 ("abc"), "pwwkew" → 3 ("wke")

The sketch shrinks one character at a time to make the idea visible. In real code you can jump L straight past the duplicate by remembering where you last saw each character:

def length_of_longest_substring(s):
    last = {}  # char -> index where we last saw it
    best = left = 0
    for right, ch in enumerate(s):
        if ch in last and last[ch] >= left:
            left = last[ch] + 1  # jump past the duplicate in one move
        last[ch] = right
        best = max(best, right - left + 1)
    return best
Enter fullscreen mode Exit fullscreen mode

Why it's linear: each index enters the window once (when right reaches it) and leaves at most once (when left passes it). That's at most 2n2n pointer moves in total, so O(n) time, and O(alphabet) space for the map.

Practise: Minimum Window Substring (76), Longest Repeating Character Replacement (424), Max Consecutive Ones III (1004).

Pattern 3: Fast and slow pointers

Signals: a linked list (or anything with a next function) where you need to find a cycle, where it starts, or the middle, using O(1) extra memory.

The idea: move a slow pointer one step at a time and a fast pointer two. If there's no cycle, fast falls off the end. If there is one, fast gains one node per step inside the cycle, so it must eventually land on slow. That's Floyd's algorithm. The clever bit is phase 2: put one pointer back at the head, move both one step at a time, and they meet exactly where the cycle begins.

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Linked List Cycle II (LeetCode 142)

Given the head of a linked list, return the node where the cycle begins, or None if there is no cycle. Use O(1) memory.

def detect_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:  # phase 1: they met somewhere inside the cycle
            slow = head  # phase 2: one pointer back to the head
            while slow is not fast:
                slow, fast = slow.next, fast.next
            return slow  # where they meet again is the cycle's entry
    return None
Enter fullscreen mode Exit fullscreen mode

Why phase 2 works: say the tail has μ\mu nodes, the cycle has λ\lambda , and they meet aa steps past the cycle's entry. Slow walked μ+a\mu + a steps and fast walked twice that. The extra distance fast covered is whole laps of the cycle:

$$
2(\mu + a) - (\mu + a) = \mu + a = m\lambda \quad\Longrightarrow\quad \mu = m\lambda - a
$$

So walking μ\mu more steps from the meeting point is mm full laps minus aa , which lands exactly on the entry. A pointer walking μ\mu steps from the head lands there too. O(n) time, O(1) space.

Practise: Middle of the Linked List (876), Happy Number (202), Find the Duplicate Number (287).

Pattern 4: Prefix sum and hash map

Signals: count (or find) subarrays whose sum equals k, especially when the array has negative numbers. Sliding window breaks here, because adding a negative can shrink a sum, so a window can't know which way to move.

The idea: a running prefix sum turns "sum of a range" into "difference of two prefixes":

$$
\text{sum}(i..j) = P_j - P_{i-1} = k \quad\Longleftrightarrow\quad P_{i-1} = P_j - k
$$

So as you scan, the number of subarrays ending at j with sum k is just how many earlier prefixes equal Pj−kP_j - k . A hash map of prefix counts answers that in O(1).

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Subarray Sum Equals K (LeetCode 560)

Given an integer array nums (which may contain negatives) and an integer k, return the total number of contiguous subarrays whose sum equals k.

nums = [1, 1, 1], k = 2 → 2

from collections import defaultdict

def subarray_sum(nums, k):
    seen = defaultdict(int)
    seen[0] = 1  # the empty prefix: lets a subarray start at index 0
    prefix = count = 0
    for x in nums:
        prefix += x
        count += seen[prefix - k]  # every earlier prefix p with prefix - p == k
        seen[prefix] += 1
    return count
Enter fullscreen mode Exit fullscreen mode

The one line people forget is seen[0] = 1. Without it, a subarray that starts at index 0 has no earlier prefix to match against and is never counted. O(n) time, O(n) space.

Practise: Continuous Subarray Sum (523), Contiguous Array (525), Range Sum Query: Immutable (303).

Pattern 5: Binary search on the answer

Signals: a sorted array, obviously. But also, and more usefully: find the **minimum* speed, capacity, or size such that something is possible*. If a value works and every larger value also works, the answer space is monotonic, and you can binary search it even though nothing is sorted.

The idea: every binary search is really one question: where does a boolean flip from false to true? Write feasible(x), then shrink [lo, hi] by half each step until lo == hi. That's at most ⌈log⁡2n⌉\lceil \log_2 n \rceil probes instead of nn .

In the sketch, each cell is a candidate eating speed k. Red means too slow and green means fast enough. You never need to check most of them:

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Koko Eating Bananas (LeetCode 875)

Koko has piles of bananas and h hours. Each hour she picks one pile and eats k bananas from it (or the whole pile if it has fewer). Return the minimum integer k that lets her finish within h hours.

piles = [3, 6, 7, 11], h = 8 → 4

def min_eating_speed(piles, h):
    def hours(k):
        return sum((p + k - 1) // k for p in piles)  # ceil without floats

    lo, hi = 1, max(piles)
    while lo < hi:
        mid = (lo + hi) // 2
        if hours(mid) <= h:
            hi = mid  # mid works: the answer is mid or smaller
        else:
            lo = mid + 1  # mid is too slow: the answer is bigger
    return lo
Enter fullscreen mode Exit fullscreen mode

Why it's correct: hours(k)=∑i⌈pi/k⌉\text{hours}(k) = \sum_i \lceil p_i / k \rceil can only go down as kk goes up, so feasible is false, false, …, true, true. The loop keeps the first true inside [lo, hi] at every step. O(n log m) time, where m is the largest pile.

The template to memorise is the lo < hi, hi = mid / lo = mid + 1 form. It never loops forever and it always lands on the first true.

Practise: Capacity To Ship Packages Within D Days (1011), Search in Rotated Sorted Array (33), Split Array Largest Sum (410).

Pattern 6: Monotonic stack

Signals: the next greater element, the next smaller element, how many days until warmer, the largest rectangle. Anything that asks each element about the nearest element to its right (or left) that beats it.

The idea: keep a stack of indices that are still waiting for an answer. Their values are always in decreasing order, which is what makes it "monotonic". When a new element is bigger than the top of the stack, it is the answer for the top, so pop it and record the distance. Keep popping while it's still bigger, then push the new element so it can wait too.

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Daily Temperatures (LeetCode 739)

Given daily temperatures, return an array where answer[i] is the number of days after day i until a warmer temperature, or 0 if there is none.

[73, 74, 75, 71, 69, 72, 76, 73] → [1, 1, 4, 2, 1, 1, 0, 0]

def daily_temperatures(temps):
    ans = [0] * len(temps)
    stack = []  # indices still waiting, temperatures decreasing
    for i, t in enumerate(temps):
        while stack and temps[stack[-1]] < t:
            j = stack.pop()
            ans[j] = i - j
        stack.append(i)
    return ans
Enter fullscreen mode Exit fullscreen mode

Why it's O(n): there's a while inside a for, but every index is pushed once and popped at most once. That's at most 2n2n stack operations in total, so O(n) time, O(n) space, instead of the O(n²) "scan right from every day" approach.

Practise: Next Greater Element I (496), Largest Rectangle in Histogram (84), Online Stock Span (901).

Pattern 7: BFS by levels

Signals: the fewest steps, minimum moves or shortest path in a grid or a graph where every edge costs the same. Also anything that spreads one ring at a time: fire, infection, rotting.

The idea: a queue processes nodes in order of distance. Process the queue one whole level at a time (for _ in range(len(q))) and the level counter is the distance. Several starting points? Put them all in the queue at the start. That's a multi-source BFS, and it costs nothing extra.

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Rotting Oranges (LeetCode 994)

In a grid, 0 is empty, 1 is a fresh orange and 2 is a rotten one. Every minute, each fresh orange next to a rotten one (up, down, left or right) becomes rotten. Return the minutes until no fresh orange is left, or -1 if that's impossible.

[[2,1,1],[1,1,0],[0,1,1]] → 4

from collections import deque

def oranges_rotting(grid):
    rows, cols = len(grid), len(grid[0])
    q, fresh = deque(), 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 2:
                q.append((r, c))  # every source starts in the queue
            elif grid[r][c] == 1:
                fresh += 1
    minutes = 0
    while q and fresh:
        for _ in range(len(q)):  # one whole BFS level = one minute
            r, c = q.popleft()
            for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
                if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                    grid[nr][nc] = 2
                    fresh -= 1
                    q.append((nr, nc))
        minutes += 1
    return -1 if fresh else minutes
Enter fullscreen mode Exit fullscreen mode

Why BFS and not DFS: DFS dives down one path and finds a path, not the shortest one. BFS visits everything at distance 1 before anything at distance 2, so the first time it reaches a cell is via a shortest path. O(rows × cols) time and space.

Practise: Number of Islands (200), Shortest Path in Binary Matrix (1091), Word Ladder (127).

Pattern 8: Backtracking

Signals: return all subsets, permutations, combinations or partitions; place N queens; solve a sudoku. Any problem where the answer is every valid arrangement rather than a count of them.

The idea: every answer is a sequence of choices, so the search space is a decision tree. Walk it depth-first with one shared path: choose (append), recurse, un-choose (pop). The un-choose step is the "backtrack". It restores the state so the sibling branch starts clean. Pruning, i.e. skipping branches that can't lead to a valid answer, is what makes the harder versions fast.

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Subsets (LeetCode 78)

Given an array of unique integers nums, return all possible subsets (the power set), in any order.

[1, 2, 3] → [[1,2,3], [1,2], [1,3], [1], [2,3], [2], [3], []]

def subsets(nums):
    out, path = [], []

    def dfs(i):
        if i == len(nums):
            out.append(path[:])  # a leaf: record a copy
            return
        path.append(nums[i])  # choose
        dfs(i + 1)
        path.pop()  # un-choose
        dfs(i + 1)  # skip

    dfs(0)
    return out
Enter fullscreen mode Exit fullscreen mode

Note the path[:]. Appending path itself would store the same list object eight times, and since it ends empty, you'd get eight empty lists. O(n · 2ⁿ) time: there are 2n2^n subsets and copying each costs up to nn . That's optimal, because the output itself is that big.

Practise: Permutations (46), Combination Sum (39), N-Queens (51).

Pattern 9: Top K with a heap

Signals: the k-th largest or smallest, the top k frequent, the k closest points, or a median of a stream. Anything that needs the best few without fully sorting.

The idea: to keep the k largest, use a min-heap of size k. Its root is the smallest of the k largest, which is exactly the k-th largest. A new number only matters if it beats the root: if it does, evict the root and push the new one; if not, ignore it. The direction feels backwards the first time: largest needs a min-heap, because the root is the gatekeeper.

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Kth Largest Element in an Array (LeetCode 215)

Given an integer array nums and an integer k, return the k-th largest element (in sorted order, not the k-th distinct one).

nums = [3, 2, 1, 5, 6, 4], k = 2 → 5

import heapq

def find_kth_largest(nums, k):
    heap = []  # min-heap of the k largest so far
    for x in nums:
        if len(heap) < k:
            heapq.heappush(heap, x)
        elif x > heap[0]:
            heapq.heapreplace(heap, x)  # pop the smallest, push x
    return heap[0]
Enter fullscreen mode Exit fullscreen mode

Why it beats sorting: sorting is O(n log n). The heap never holds more than k items, so each push or replace costs O(log k), for O(n log k) time and O(k) space. When k is small and n is huge, or the data is a stream you can't sort at all, that difference is the whole point.

Practise: Top K Frequent Elements (347), K Closest Points to Origin (973), Find Median from Data Stream (295).

Pattern 10: Dynamic programming

Signals: the fewest, the most, or the number of ways, where a choice now leaves a smaller version of the same problem, and those smaller problems overlap (the naive recursion keeps solving the same one again).

The idea: define what one cell means, in plain words, before writing any code. Here, dp[x] is the fewest coins that make x. Then write how a cell is built from smaller ones, and fill the table in an order where those smaller cells already exist:

$$
dp[0] = 0, \qquad dp[x] = 1 + \min_{c \,\in\, \text{coins},\ c \le x} dp[x - c]
$$

Watch each cell try every coin, look back at an earlier cell (the arc) and keep the best. When it finishes, the green arcs below trace the coins actually used:

A frame from the interactive sketch in the original — it does not render here.

Sample problem: Coin Change (LeetCode 322)

Given coin denominations coins and an amount, return the fewest coins that make up that amount, or -1 if it can't be done. You have unlimited coins of each kind.

coins = [1, 2, 5], amount = 11 → 3 (5 + 5 + 1)

def coin_change(coins, amount):
    INF = amount + 1  # more coins than could ever be needed
    dp = [0] + [INF] * amount  # dp[x] = fewest coins that make x
    for x in range(1, amount + 1):
        for c in coins:
            if c <= x:
                dp[x] = min(dp[x], dp[x - c] + 1)
    return dp[amount] if dp[amount] != INF else -1
Enter fullscreen mode Exit fullscreen mode

Why not just be greedy? With coins [1, 3, 4] and amount 6, greedy grabs the biggest coin first: 4 + 1 + 1, three coins. The real answer is 3 + 3, two coins. Greedy decides too early. DP tries every last coin and keeps the best, so it can't be fooled. The sketch prints the greedy count whenever greedy would have lost; click until coins [1, 3, 4] come up. O(amount × coins) time, O(amount) space.

Practise: Climbing Stairs (70), House Robber (198), Longest Common Subsequence (1143).

Complexity at a glance

# Pattern Classic problem Time Extra space
1 Two pointers Two Sum II (167) O(n) O(1)
2 Sliding window Longest Substring Without Repeating (3) O(n) O(alphabet)
3 Fast and slow pointers Linked List Cycle II (142) O(n) O(1)
4 Prefix sum + hash map Subarray Sum Equals K (560) O(n) O(n)
5 Binary search on the answer Koko Eating Bananas (875) O(n log m) O(1)
6 Monotonic stack Daily Temperatures (739) O(n) O(n)
7 BFS by levels Rotting Oranges (994) O(rows × cols) O(rows × cols)
8 Backtracking Subsets (78) O(n · 2ⁿ) O(n) + output
9 Heap (top K) Kth Largest Element (215) O(n log k) O(k)
10 Dynamic programming Coin Change (322) O(amount × coins) O(amount)

Notice how many rows say O(n) where the brute force is O(n²). That's the common thread. Each pattern is a reason why most of the pairs, windows or states you'd naively check can never be the answer, and a way to skip them.

Honourable mentions

Ten covers most problems, but these show up often enough to know by name:

  • Merge intervals: sort by start, then merge each interval into the last one if they overlap. Merge Intervals (56), Insert Interval (57).
  • Topological sort: Kahn's algorithm, BFS over in-degrees, for "order these tasks with dependencies". Course Schedule (207).
  • Union-find: near-O(1) "are these connected?" with path compression. Number of Connected Components (323), Redundant Connection (684).
  • Trie: a prefix tree for "starts with" and word-search problems. Implement Trie (208).
  • Two heaps: a max-heap and a min-heap balanced around the middle, for a running median. Find Median from Data Stream (295).
  • Bit manipulation: XOR tricks, masks and subsets as integers. Single Number (136).

How to practise

  1. Learn the pattern, not the problem. For each pattern, do the sample problem above, then the three practice problems without looking anything up. If they feel like the same problem by the third one, you've got it.
  2. Name the signal before you code. Read the statement and say out loud which word gave it away: "contiguous", "sorted", "k-th", "all combinations", "fewest steps".
  3. Start from the template. Each solution above is small enough to rewrite from memory in a couple of minutes. In an interview, a correct template you trust beats a clever idea you have to debug.
  4. State the complexity and the reason. "O(n), because each index enters and leaves the window once" is the sentence interviewers listen for.
  5. Play with the sketches. Click them until the behaviour stops surprising you. Being able to predict the next step is what understanding feels like.

Happy problem solving! 🧠


Cover photo: "Swiss Army Knife" Abacus by Jccsvq, CC0, via Wikimedia Commons.

Top comments (0)