Designs by Duhart All writing

·7 min read·softwareengineering · dsa · algorithms · datastructures · codinginterview · python · leetcode · learntocode · programming

15 patterns to make SWE & DSA easier, with a free practice repo

When to reach for each pattern, the shape of the code, and 45 practice problems with tests and solutions.

Cover slide: 15 patterns to make SWE and DSA easier. Lists the fifteen patterns: two pointers, sliding window, binary search, frequency counting, matrix traversal, monotonic stack, prefix sum, overlapping intervals, greedy, top k elements, backtracking, binary tree traversal, depth-first search, breadth-first search and dynamic programming. A green box announces a free practice repo with 45 problems, tests and solutions.

15 patterns, and now a place to practice them

A while back I posted a list of 15 patterns that make data structures and algorithms easier. People saved it, which is nice, but saving a list is not the same as being able to use it. So this version comes with a practice repo.

Here's the idea behind the list. Memorising hundreds of problems doesn't scale. Recognising about fifteen shapes does. When I read a new problem now, the first question I ask is which pattern it is, not whether I've seen that exact one before. That's also true at work, where nobody hands you a problem with a name on it.

The practice repo: gitlab.com/j.michaelduhart/dsa-patterns. Free, MIT licensed, Python.

Cover slide: 15 patterns to make SWE and DSA easier, listing all fifteen pattern names and announcing a free practice repo with 45 problems, tests and solutions.
The carousel version. Each slide has the cue that tells you to reach for the pattern, and the code shape.

All 15 in 42 seconds

The quick version: every pattern, its cue and its code shape.
Cover slide: 15 patterns to make SWE and DSA easier. Lists the fifteen patterns: two pointers, sliding window, binary search, frequency counting, matrix traversal, monotonic stack, prefix sum, overlapping intervals, greedy, top k elements, backtracking, binary tree traversal, depth-first search, breadth-first search and dynamic programming. A green box announces a free practice repo with 45 problems, tests and solutions.
The quick version: every pattern, its cue and its code shape. Watch the video: https://designsbyduhart.org/blog/swe-dsa-15-patterns/

How the repo works

Every pattern gets its own folder with four things in it:

  • a README with when to use it, the template, the complexity and a worked example
  • practice.py, three problems as function stubs, where the docstring is the problem
  • tests, which run against your practice file
  • solutions/, for when you're properly stuck

The problems are written for the repo. Plenty of them are classic interview questions in plain words, so what you learn carries over. Run the tests and you get a table of what you've solved.

Getting started

bash
git clone https://gitlab.com/j.michaelduhart/dsa-patterns.git
cd dsa-patterns
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

pytest                              # every pattern, all failing until you solve them
pytest patterns/01-two-pointers     # one pattern
DSA_IMPL=solutions pytest           # the same tests against the answers

1. Two Pointers

Use it when: Sorted input and you need a pair. Start at both ends and move the one that helps.

Two indexes, one at each end, walking toward each other. On sorted data every move rules out a whole set of pairs, so a nested O(n^2) loop becomes one O(n) pass. It also covers palindromes and in-place work like removing duplicates.

Cost: O(n) time, O(1) space. Practice it: patterns/01-two-pointers

Two Pointers: the shape

python
lo, hi = 0, len(a) - 1
while lo < hi:
    s = a[lo] + a[hi]
    if s == target: return lo, hi
    if s < target: lo += 1
    else: hi -= 1

2. Sliding Window

Use it when: Best contiguous run. Grow the right edge, shrink the left when it breaks.

Keep a window over the array and update it as it slides instead of recomputing every subarray. Add what enters on the right, remove what leaves on the left. Each element goes in once and out once, which is why it's linear even with a loop inside a loop.

Cost: O(n) time. Practice it: patterns/02-sliding-window

Sliding Window: the shape

python
left = 0
for right, x in enumerate(a):
    add(x)
    while not valid():
        remove(a[left]); left += 1
    best = max(best, right - left + 1)

3. Binary Search

Use it when: Sorted data, or a yes/no answer that flips once. Halve the space every step.

Everyone knows it for finding a value in a sorted list. The version that gets you hired is searching the answer space: if a speed of 10 works, 11 works too, so binary search the smallest speed that works. I write every binary search as "find the first index where the condition turns true" and most off-by-one bugs go away.

Cost: O(log n) steps. Practice it: patterns/03-binary-search

Brute force against the patterns

Exact step counts as n grows: every pair (n(n-1)/2), two pointers (n), binary search for one value (log2 n).
Final frame of the animated chart. Animated line chart of worst-case steps against input size n up to 40: checking every pair grows as n times (n - 1) / 2 and reaches 780; two pointers grows as n and reaches 40; binary search grows as log2 n and stays near 5.
Exact step counts as n grows: every pair (n(n-1)/2), two pointers (n), binary search for one value (log2 n). Watch the video: https://designsbyduhart.org/blog/swe-dsa-15-patterns/

Binary Search: the shape

python
lo, hi = 0, n
while lo < hi:
    mid = (lo + hi) // 2
    if ok(mid): hi = mid
    else: lo = mid + 1
return lo

4. Frequency Counting

Use it when: Anagrams, duplicates, first unique. Count once into a map, then ask.

Count once into a dictionary, then answer from the counts. Anagrams, duplicates, the first unique character, grouping words by their letters. Write the counting loop by hand once so you see it's just a dict, then use collections.Counter forever.

Cost: O(n) time, O(k) space for k distinct items. Practice it: patterns/04-frequency-counting

Frequency Counting: the shape

python
counts = Counter(items)
for i, x in enumerate(items):
    if counts[x] == 1:
        return i
return -1

5. Matrix Traversal

Use it when: Grids. Name your four directions and check the bounds in one place.

Most grid bugs are boundary bugs. Put the four directions in one list and the bounds check in one place, and the rest of the code stops caring about edges. For spirals, keep four walls and pull one in after each side.

Cost: O(rows x cols). Practice it: patterns/05-matrix-traversal

Matrix Traversal: the shape

python
DIRS = [(0, 1), (1, 0), (0, -1), (-1, 0)]
for dr, dc in DIRS:
    nr, nc = r + dr, c + dc
    if 0 <= nr < R and 0 <= nc < C:
        visit(nr, nc)

6. Monotonic Stack

Use it when: Next bigger or smaller element. Every pop has just found its answer.

A stack whose values only go one direction. When a new number breaks the order, everything it pops has just found its next bigger element. It looks quadratic because of the inner while, but each index is pushed once and popped once.

Cost: O(n) time. Practice it: patterns/06-monotonic-stack

Monotonic Stack: the shape

python
stack = []
for i, x in enumerate(a):
    while stack and a[stack[-1]] < x:
        ans[stack.pop()] = x
    stack.append(i)

7. Prefix Sum

Use it when: Lots of range-sum questions. Build running totals once, then subtract.

Build running totals once and every range sum is one subtraction. Pair it with a hash map and you can count subarrays that add up to k, even with negative numbers, in a single pass. The leading 0 in the array is not decoration. It's what makes ranges that start at index 0 work.

Cost: O(n) build, O(1) per query. Practice it: patterns/07-prefix-sum

Prefix Sum: the shape

python
pre = [0]
for x in a:
    pre.append(pre[-1] + x)
# sum of a[i..j] in O(1)
total = pre[j + 1] - pre[i]

8. Overlapping Intervals

Use it when: Meetings, bookings, ranges. Sort by start and only neighbours can overlap.

Meetings, bookings, time windows. Sort by start and overlaps can only happen between neighbours, so one pass merges everything. Add a min-heap of end times and the same idea tells you how many rooms you need.

Cost: O(n log n) for the sort. Practice it: patterns/08-overlapping-intervals

Overlapping Intervals: the shape

python
out = []
for s, e in sorted(spans):
    if out and s <= out[-1][1]:
        out[-1][1] = max(out[-1][1], e)
    else:
        out.append([s, e])

9. Greedy

Use it when: The best choice now never hurts later. Prove it with a swap, or use DP.

Take the best-looking choice at each step. It's the simplest code in this list and the easiest to get wrong. Before you trust a greedy answer, make the swap argument: replacing any other first choice with yours never makes things worse. If you can't, look for a counterexample.

Cost: usually O(n log n). Practice it: patterns/09-greedy

Greedy: the shape

python
talks.sort(key=lambda t: t[1])
free, count = float("-inf"), 0
for s, e in talks:
    if s >= free:
        count += 1
        free = e

10. Top K Elements

Use it when: The k biggest, closest or most frequent. Keep a heap of size k.

For the k biggest, closest or most frequent items, keep a heap that never grows past k. It feels backwards: to keep the largest k you use a min-heap, because the item you want to throw out is the smallest of them.

Cost: O(n log k) time, O(k) space. Practice it: patterns/10-top-k-elements

Top K Elements: the shape

python
heap = []
for x in a:
    heappush(heap, x)
    if len(heap) > k:
        heappop(heap)
return sorted(heap, reverse=True)

11. Backtracking

Use it when: Every subset, order or combination. Choose, explore, un-choose.

When the question says all, every or generate, build a candidate one choice at a time and undo the last choice on the way back up. Choose, explore, un-choose. The bug almost everyone writes once is appending path instead of a copy of it.

Cost: exponential, because the output is. Practice it: patterns/11-backtracking

Backtracking: the shape

python
def go(path, start):
    out.append(path[:])
    for i in range(start, len(a)):
        path.append(a[i])
        go(path, i + 1)
        path.pop()

12. Binary Tree Traversal

Use it when: Any tree question. Pick the order first: pre, in, post or level.

Every tree question starts with choosing the order you visit nodes. Pre-order copies a tree, in-order reads a search tree in sorted order, post-order is for anything that needs the children's answers first, like height. Validating a search tree is the classic trap: checking only a node's direct children is not enough.

Cost: O(n) time, O(h) stack. Practice it: patterns/12-binary-tree-traversal

Binary Tree Traversal: the shape

python
def walk(node):
    if node is None:
        return 0
    left = walk(node.left)
    right = walk(node.right)
    return 1 + max(left, right)

13. Depth-First Search

Use it when: Reachable, connected, islands, cycles. Go deep with a stack.

Go as deep as you can, then back up. Use it for reachability, counting islands and finding cycles in dependencies. Python stops at about 1,000 nested calls, so on a big grid I use an explicit stack instead of recursion.

Cost: O(V + E). Practice it: patterns/13-depth-first-search

Depth-First Search: the shape

python
stack, seen = [start], set()
while stack:
    v = stack.pop()
    if v in seen: continue
    seen.add(v)
    stack.extend(graph[v])

14. Breadth-First Search

Use it when: Shortest path when every step costs the same. Explore in rings.

Explore in rings: everything one step away, then everything two steps away. The first time you reach the goal is the shortest route, as long as every step costs the same. Use collections.deque. list.pop(0) quietly turns BFS quadratic.

Cost: O(V + E). Practice it: patterns/14-breadth-first-search

Breadth-First Search: the shape

python
q, seen = deque([(start, 0)]), {start}
while q:
    v, d = q.popleft()
    if v == goal: return d
    for w in graph[v]:
        if w not in seen:
            seen.add(w); q.append((w, d + 1))

15. Dynamic Programming

Use it when: Count the ways or find the best, and subproblems repeat. Store each answer once.

The subproblems repeat, so solve each once and store it. I answer four questions before writing a loop: what does dp[i] mean, how is it built from smaller states, what are the base cases, and in what order do I fill it. Coin change is the classic proof that greedy can fail: with coins 1, 3 and 4, greedy makes 6 as 4 + 1 + 1, DP finds 3 + 3.

Cost: states x work per state. Practice it: patterns/15-dynamic-programming

Dynamic Programming: the shape

python
best = [0] + [INF] * amount
for x in range(1, amount + 1):
    for c in coins:
        if c <= x:
            best[x] = min(best[x], best[x - c] + 1)

The pairs worth studying together

Two pairs trip people up more than any single pattern.

Greedy and dynamic programming. Both answer "what's the best I can do?". Greedy is faster and shorter, and it's only right when you can argue that the local choice never hurts. The moment you find a counterexample, switch to DP. Doing patterns 9 and 15 back to back makes that click.

DFS and BFS. Both visit everything reachable. If the question says shortest and every step costs the same, it's BFS. If you need to go all the way down a path, detect a cycle or just mark what's connected, DFS is simpler.

Knowing when a pattern does NOT fit is most of the skill.

How I'd use this

Read a pattern's README and type the template out by hand once. Don't paste it. Solve the first problem and run its tests. Before the second and third, say out loud what told you this pattern fits. A week later, pick a random problem and write the pattern name before you write any code.

If something in the repo is wrong or unclear, open an issue on GitLab. I'd rather fix it than have someone learn it wrong.

Which of the 15 gives you the most trouble?

More: LinkedIn · Instagram. Portfolio and case studies: designsbyduhart.org.

If any of this saved you an afternoon, Buy me a coffee.