Algorithm Patterns: The 15 Essential Patterns

DFS, BFS & Backtracking

4 min read

These patterns are your toolkit for exploring trees, graphs, and generating all possible combinations. They appear in a wide range of interview problems.

DFS explores as deep as possible before backtracking. It uses a stack (or recursion's call stack).

When to Use DFS:

  • Tree traversals (preorder, inorder, postorder)
  • Path finding in graphs
  • Connected components
  • Cycle detection
# Number of islands (classic DFS)
def num_islands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == '0':
            return
        grid[r][c] = '0'  # Mark visited
        dfs(r+1, c)
        dfs(r-1, c)
        dfs(r, c+1)
        dfs(r, c-1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                dfs(r, c)
                count += 1
    return count

# Path sum in binary tree
def has_path_sum(root, target_sum):
    if not root:
        return False
    if not root.left and not root.right:
        return root.val == target_sum
    return (has_path_sum(root.left, target_sum - root.val) or
            has_path_sum(root.right, target_sum - root.val))

BFS explores level by level using a queue. It guarantees the shortest path in unweighted graphs.

When to Use BFS:

  • Shortest path (unweighted graph)
  • Level-order traversal
  • Minimum steps/moves problems
  • Nearest neighbor searches
from collections import deque

# Shortest path in binary matrix
def shortest_path(grid):
    n = len(grid)
    if grid[0][0] == 1 or grid[n-1][n-1] == 1:
        return -1

    queue = deque([(0, 0, 1)])  # (row, col, distance)
    visited = {(0, 0)}
    directions = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]

    while queue:
        r, c, dist = queue.popleft()
        if r == n-1 and c == n-1:
            return dist
        for dr, dc in directions:
            nr, nc = r + dr, c + dc
            if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0 and (nr, nc) not in visited:
                visited.add((nr, nc))
                queue.append((nr, nc, dist + 1))
    return -1

DFS vs BFS Decision

CriteriaDFSBFS
Shortest pathNo (in general)Yes (unweighted)
Memory usageO(h) heightO(w) width
ImplementationRecursion or stackQueue
Best for treesPath problemsLevel-order
Best for graphsConnected componentsShortest distance

Backtracking

Backtracking is DFS with a twist: you build a solution incrementally and abandon it ("backtrack") when it can't lead to a valid result.

The Template:

def backtrack(candidates, path, result, start):
    if is_valid_solution(path):
        result.append(path[:])  # Add copy
        return
    for i in range(start, len(candidates)):
        path.append(candidates[i])       # Choose
        backtrack(candidates, path, result, i + 1)  # Explore
        path.pop()                        # Un-choose (backtrack)

Classic Backtracking Problems:

# Subsets: generate all subsets of a set
def subsets(nums):
    result = []
    def backtrack(start, path):
        result.append(path[:])
        for i in range(start, len(nums)):
            path.append(nums[i])
            backtrack(i + 1, path)
            path.pop()
    backtrack(0, [])
    return result

# Permutations
def permutations(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
def solve_n_queens(n):
    result = []
    def backtrack(row, cols, diag1, diag2, board):
        if row == n:
            result.append([''.join(r) for r in board])
            return
        for col in range(n):
            if col in cols or (row-col) in diag1 or (row+col) in diag2:
                continue
            board[row][col] = 'Q'
            backtrack(row+1, cols|{col}, diag1|{row-col}, diag2|{row+col}, board)
            board[row][col] = '.'
    board = [['.']*n for _ in range(n)]
    backtrack(0, set(), set(), set(), board)
    return result

Interview Tip: Backtracking problems follow a "choose, explore, un-choose" pattern. If you can identify this structure, the solution writes itself.


Next, the pattern that strikes fear into most candidates: dynamic programming. :::

Quiz

Module 3 Quiz: Algorithm Patterns

Take Quiz
FREE WEEKLY NEWSLETTER

Stay on the Nerd Track

One email per week — courses, deep dives, tools, and AI experiments.

No spam. Unsubscribe anytime.