Skip to main content
Coding interview guide ยท Updated November 6, 2026

LeetCode patterns: the 18 coding interview patterns, with templates

Most coding interview problems are a known pattern in a new setting. This guide covers the 18 patterns behind most coding rounds. For each one you get the signals that give it away, a short template in Python and JavaScript, time and space complexity, three classic problem types and the mistakes that cost the most.

By the OphyAI team. Every template on this page runs as written in Python 3 and Node.js.

Method

How to use coding patterns in an interview

A pattern only helps if you can find it under pressure. Use the same five steps on every problem, in practice and in the real round.

  1. Step 1

    Restate and test

    Say the problem back in your own words and work one small example by hand. Ask about input size, duplicates, negatives and empty input.

  2. Step 2

    Read the signals

    Look for the words that give a pattern away: sorted, contiguous, top k, all combinations, fewest steps, how many ways. The table below maps signals to patterns.

  3. Step 3

    Say the brute force

    Give the slow answer and its complexity in one sentence. It proves you understand the problem and shows what the pattern has to beat.

  4. Step 4

    Apply the template

    Name the pattern, explain why it fits, then adapt the template. Talk while you code so the interviewer can follow and help.

  5. Step 5

    Test and state the cost

    Walk through your example, then the edge cases. Finish with time and space complexity, and one trade-off you would mention.

Decision table

Which pattern? Read the problem for these signals

Match the wording of the problem to a pattern, then jump to its template. If two rows fit, start with the simpler pattern and say why you would switch.

Signal in the problemTry this patternTypical target
Have I seen this value before? Count, group or remove duplicatesArrays and hashingO(n) time, O(n) space
Sorted input and you need a pair, a triplet or a palindrome checkTwo pointersO(n) time, O(1) space
Longest or shortest contiguous subarray or substring that meets a ruleSliding windowO(n)
Brackets or nesting, or the next greater or smaller item for each itemStackO(n)
Sorted input with an O(log n) target, or the smallest value that worksBinary searchO(log n) or O(n log M)
A linked list: reverse it, find its middle or a cycle, merge listsLinked listsO(n) time, O(1) space
Depth, paths, levels or a common ancestor in a treeTreesO(n)
Many words and questions about prefixesTriesO(L) per word
Top k, kth largest, merge k sorted inputs, running medianHeapsO(n log k)
Return every subset, permutation or valid arrangementBacktrackingExponential, pruned
A grid, islands, prerequisites, or fewest steps when every step costs the sameGraphsO(V + E)
Weighted edges, the cheapest route, or connecting everything at lowest costAdvanced graphsO((V + E) log V)
How many ways, minimum cost or "is it possible" along one sequence1D dynamic programmingO(n) to O(n^2)
Two strings compared together, a grid of choices, or a capacity limit2D dynamic programmingO(m * n)
An obvious best next step, often after sorting, that you can justifyGreedyO(n) or O(n log n)
Start and end times, overlaps, or how many rooms at onceIntervalsO(n log n)
Rotate or spiral a matrix, powers, digits or modulo mathMath and geometryVaries
Every value appears twice but one, bit counts, power of twoBit manipulationO(n) time, O(1) space
Cheat sheet

LeetCode patterns cheat sheet

All 18 coding patterns on one screen: the core move and the usual cost of the main template. Bookmark it for the night before.

#PatternCore moveTimeSpace
01Arrays and hashingTrade memory for O(1) lookupsO(n)O(n)
02Two pointersMove two indexes instead of nesting loopsO(n), plus O(n log n) if you sort firstO(1)
03Sliding windowGrow the right edge, shrink the leftO(n)O(1)
04StackLast in, first out for nesting and "next greater"O(n)O(n)
05Binary searchHalve a sorted or monotonic rangeO(log n)O(1)
06Linked listsRewire pointers, often with fast and slowO(n)O(1)
07TreesRecurse on subtrees, or BFS by levelO(n)O(h) for the call stack, h = height
08TriesShare prefixes in a character treeO(L) per wordO(total characters stored)
09HeapsKeep the top k, or the next smallest, cheaplyO(n log k)O(k)
10BacktrackingChoose, explore, undoO(n * 2^n)O(n) plus the output
11GraphsBFS or DFS with a visited setO(rows * cols)O(rows * cols)
12Advanced graphsWeighted paths, unions and treesO((V + E) log V)O(V + E)
131D dynamic programmingBest answer at i from answers before iO(n * number of steps)O(n)
142D dynamic programmingA table over two indexesO(m * n)O(m * n), or O(n) with one row
15GreedyTake the best local choice, then prove itO(n)O(1)
16IntervalsSort by start, then sweepO(n log n)O(n) for the output
17Math and geometryIndex math, matrix moves and fast powerO(n^2)O(1)
18Bit manipulationXOR, masks and the lowest set bitO(n)O(1)
Pattern 01 of 18

Arrays and hashing

Most array problems get faster the moment you stop searching and start remembering. A hash map or set gives you constant-time "have I seen this?" checks, counts and groupings, which turns many O(n^2) double loops into one pass.

Signals in the problem

  • You need to know if a value, or its complement, has appeared before.
  • The problem asks you to count, group or find duplicates.
  • The input is unsorted and sorting it would lose the original positions.
  • A brute force answer compares every pair of items.

Classic problem types

  • Pair with a target sum

    Store each value as you go and look up target minus the current value.

  • Group words made of the same letters

    Use the sorted letters, or a 26-letter count, as the map key.

  • Longest run of consecutive numbers in an unsorted list

    Put every value in a set, then only start counting from values with no smaller neighbor.

Common mistakes

  • Adding the current value to the map before checking for its complement, so a value pairs with itself.
  • Using a list or array where a set was needed, which makes each lookup O(n) again.
  • Forgetting that keys must be hashable: in Python, use a tuple, not a list, as a key.
Template 1 of 2

Lookup map: find a pair that adds up to a target

Python
def pair_with_sum(nums, target):
    seen = {}  # value -> index where we saw it
    for i, num in enumerate(nums):
        need = target - num
        if need in seen:
            return [seen[need], i]
        seen[num] = i
    return []
JavaScript
function pairWithSum(nums, target) {
  const seen = new Map(); // value -> index where we saw it
  for (let i = 0; i < nums.length; i++) {
    const need = target - nums[i];
    if (seen.has(need)) return [seen.get(need), i];
    seen.set(nums[i], i);
  }
  return [];
}
Template 2 of 2

Group by key: bucket words that use the same letters

Python
from collections import defaultdict

def group_by_letters(words):
    groups = defaultdict(list)
    for word in words:
        key = "".join(sorted(word))  # same letters, same key
        groups[key].append(word)
    return list(groups.values())
JavaScript
function groupByLetters(words) {
  const groups = new Map();
  for (const word of words) {
    const key = [...word].sort().join(""); // same letters, same key
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(word);
  }
  return [...groups.values()];
}
Time and space complexity for the arrays and hashing templates
TemplateTimeSpace
Lookup mapO(n)O(n)
Group by keyO(n * k log k) for words of length kO(n * k)
Practice this pattern

Open the Coding Practice library filtered to arrays and hashing problems.

Practice arrays and hashing
Pattern 02 of 18

Two pointers

The two pointer technique keeps two indexes into the same array or string and moves them by a rule, so each step rules out many candidates at once. It is the standard O(n) answer to problems that look like they need every pair.

There are two shapes. Opposite ends: start one pointer at each end and move them toward each other, which works when the data is sorted or symmetric, as with pair sums and palindromes. Same direction: a slow write pointer and a fast read pointer move left to right, which is how you filter or compact an array in place without extra memory.

The two pointer algorithm is correct because of a simple argument you should say out loud: in a sorted array, if the sum is too small, no pair using the current left value with any smaller right value can work, so moving left forward loses nothing.

Signals in the problem

  • The input is sorted, or you are allowed to sort it.
  • You are looking for a pair, a triplet or a palindrome.
  • You must modify the array in place with O(1) extra space.
  • The answer depends on comparing items from both ends.

Classic problem types

  • Does a string read the same both ways, ignoring punctuation?

    Skip non-letters from each end, then compare and move inward.

  • All unique triplets that sum to zero

    Sort, fix one value, then run opposite-end pointers on the rest and skip repeats.

  • Two walls that hold the most water

    Start at both ends and always move the shorter wall: no narrower pair that keeps it can hold more.

Common mistakes

  • Using opposite-end pointers on unsorted data, where the move rule no longer holds.
  • Writing while left <= right when the two pointers must stay distinct, so an item pairs with itself.
  • Not skipping duplicate values in triplet problems, which returns the same answer more than once.
Template 1 of 2

Opposite ends: pair sum in a sorted array

Python
def pair_sum_sorted(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        total = nums[left] + nums[right]
        if total == target:
            return [left, right]
        if total < target:
            left += 1   # need a bigger sum
        else:
            right -= 1  # need a smaller sum
    return []
JavaScript
function pairSumSorted(nums, target) {
  let left = 0;
  let right = nums.length - 1;
  while (left < right) {
    const total = nums[left] + nums[right];
    if (total === target) return [left, right];
    if (total < target) left++; // need a bigger sum
    else right--; // need a smaller sum
  }
  return [];
}
Template 2 of 2

Same direction: read and write pointers, in place

Python
def remove_duplicates_sorted(nums):
    if not nums:
        return 0
    write = 1
    for read in range(1, len(nums)):
        if nums[read] != nums[write - 1]:
            nums[write] = nums[read]
            write += 1
    return write  # nums[:write] now holds the unique values
JavaScript
function removeDuplicatesSorted(nums) {
  if (nums.length === 0) return 0;
  let write = 1;
  for (let read = 1; read < nums.length; read++) {
    if (nums[read] !== nums[write - 1]) {
      nums[write] = nums[read];
      write++;
    }
  }
  return write; // nums.slice(0, write) now holds the unique values
}
Time and space complexity for the two pointers templates
TemplateTimeSpace
Opposite endsO(n), plus O(n log n) if you sort firstO(1)
Read and write pointersO(n)O(1)
Practice this pattern

Open the Coding Practice library filtered to two pointers problems.

Practice two pointers
Pattern 03 of 18

Sliding window

The sliding window algorithm finds the best contiguous run in an array or string in one pass. You keep a window between two indexes, add the item on the right, and drop items on the left whenever the window breaks the rule. Each item enters and leaves at most once, so the whole scan is O(n).

Fixed size windows have a set length k: add the new item, remove the one that fell out, and update the answer. Variable size windows grow and shrink. For "longest valid" problems, shrink while the window is invalid and record the answer after. For "shortest valid" problems, shrink while the window is still valid and record the answer inside the loop.

The window only works when growing it moves the condition in one direction. A sum of non-negative numbers only grows as the window grows, so you know when to shrink. With negative numbers that is no longer true, and you need prefix sums with a hash map instead.

Signals in the problem

  • The problem says subarray or substring, meaning a contiguous run.
  • You want the longest, shortest or best run that meets a condition.
  • There is a window size k, or a limit such as "at most k distinct" or "sum at least target".
  • A brute force answer checks every start and end index.

Classic problem types

  • Longest substring with no repeating characters

    Count characters in the window and shrink from the left while any count is above 1.

  • Smallest run of numbers whose sum reaches a target

    Grow until the sum is big enough, then shrink while it still is.

  • Every place where a rearranged copy of a short word appears

    A fixed window the length of the word, comparing letter counts as it slides.

Common mistakes

  • Shrinking with if instead of while, so the window can stay invalid.
  • Recording the answer at the wrong moment: after shrinking for "longest", inside the shrink loop for "shortest".
  • Using a sum-based window on arrays with negative numbers.
  • Rebuilding the window from scratch at each step, which quietly turns O(n) into O(n * k).
Template 1 of 3

Fixed size: best sum of k values in a row

Python
def max_sum_of_k(nums, k):
    if k <= 0 or k > len(nums):
        return None
    window = sum(nums[:k])
    best = window
    for right in range(k, len(nums)):
        window += nums[right] - nums[right - k]  # add the new value, drop the oldest
        best = max(best, window)
    return best
JavaScript
function maxSumOfK(nums, k) {
  if (k <= 0 || k > nums.length) return null;
  let window = 0;
  for (let i = 0; i < k; i++) window += nums[i];
  let best = window;
  for (let right = k; right < nums.length; right++) {
    window += nums[right] - nums[right - k]; // add the new value, drop the oldest
    best = Math.max(best, window);
  }
  return best;
}
Template 2 of 3

Variable size, longest: longest run with no repeated character

Python
def longest_unique_run(s):
    counts = {}
    left = best = 0
    for right, ch in enumerate(s):
        counts[ch] = counts.get(ch, 0) + 1
        while counts[ch] > 1:      # the window broke the rule: shrink it
            counts[s[left]] -= 1
            left += 1
        best = max(best, right - left + 1)
    return best
JavaScript
function longestUniqueRun(s) {
  const counts = new Map();
  let left = 0;
  let best = 0;
  for (let right = 0; right < s.length; right++) {
    const ch = s[right];
    counts.set(ch, (counts.get(ch) || 0) + 1);
    while (counts.get(ch) > 1) { // the window broke the rule: shrink it
      counts.set(s[left], counts.get(s[left]) - 1);
      left++;
    }
    best = Math.max(best, right - left + 1);
  }
  return best;
}
Template 3 of 3

Variable size, shortest: shortest run with a sum of at least target

Python
def shortest_run_at_least(nums, target):
    # Works because every value is non-negative: growing the window never lowers the sum.
    left = total = 0
    best = float("inf")
    for right, num in enumerate(nums):
        total += num
        while total >= target:     # valid: record it, then try to shrink
            best = min(best, right - left + 1)
            total -= nums[left]
            left += 1
    return 0 if best == float("inf") else best
JavaScript
function shortestRunAtLeast(nums, target) {
  // Works because every value is non-negative: growing the window never lowers the sum.
  let left = 0;
  let total = 0;
  let best = Infinity;
  for (let right = 0; right < nums.length; right++) {
    total += nums[right];
    while (total >= target) { // valid: record it, then try to shrink
      best = Math.min(best, right - left + 1);
      total -= nums[left];
      left++;
    }
  }
  return best === Infinity ? 0 : best;
}
Time and space complexity for the sliding window templates
TemplateTimeSpace
Fixed sizeO(n)O(1)
Longest valid runO(n)O(k) for k distinct characters
Shortest valid runO(n)O(1)
Practice this pattern

Open the Coding Practice library filtered to sliding window problems.

Practice sliding window
Pattern 04 of 18

Stack

A stack handles anything nested or anything where the most recent open item is the one that matters next. A monotonic stack, kept in increasing or decreasing order, answers "next greater" or "next smaller" questions for every item in one pass.

Signals in the problem

  • Brackets, tags or other things that open and close.
  • For each item, find the next or previous item that is bigger or smaller.
  • Undo, back, or evaluate-an-expression behavior.
  • Nested structure such as decoding "3[ab]" style strings.

Classic problem types

  • Are the brackets in a string balanced?

    Push openers, and pop and compare on every closer.

  • For each day, how many days until a warmer one

    Keep indexes of days still waiting, and pop them when a warmer day arrives.

  • Largest rectangle under a bar chart

    A stack of increasing heights, closing each bar's rectangle when a shorter bar arrives.

Common mistakes

  • Popping from an empty stack. Check that it is non-empty first.
  • Returning true without checking that the stack is empty at the end.
  • Storing values in a monotonic stack when you need indexes to measure distances.
Template 1 of 2

Matching pairs: are the brackets balanced?

Python
def balanced(s):
    pairs = {")": "(", "]": "[", "}": "{"}
    stack = []
    for ch in s:
        if ch in "([{":
            stack.append(ch)
        elif ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
    return not stack
JavaScript
function balanced(s) {
  const pairs = { ")": "(", "]": "[", "}": "{" };
  const stack = [];
  for (const ch of s) {
    if ("([{".includes(ch)) {
      stack.push(ch);
    } else if (ch in pairs) {
      if (stack.length === 0 || stack.pop() !== pairs[ch]) return false;
    }
  }
  return stack.length === 0;
}
Template 2 of 2

Monotonic stack: days until a warmer day

Python
def days_until_warmer(temps):
    answer = [0] * len(temps)
    stack = []  # indices whose warmer day we have not found yet
    for i, t in enumerate(temps):
        while stack and temps[stack[-1]] < t:
            j = stack.pop()
            answer[j] = i - j
        stack.append(i)
    return answer
JavaScript
function daysUntilWarmer(temps) {
  const answer = new Array(temps.length).fill(0);
  const stack = []; // indices whose warmer day we have not found yet
  for (let i = 0; i < temps.length; i++) {
    while (stack.length && temps[stack[stack.length - 1]] < temps[i]) {
      const j = stack.pop();
      answer[j] = i - j;
    }
    stack.push(i);
  }
  return answer;
}
Time and space complexity for the stack templates
TemplateTimeSpace
Matching pairsO(n)O(n)
Monotonic stackO(n): each index is pushed and popped onceO(n)
Practice this pattern

Open the Coding Practice library filtered to stack problems.

Practice stack
Pattern 06 of 18

Linked lists

Linked list problems test careful pointer handling more than clever ideas. Three tools cover most of them: reversing links in place, a dummy head node so the first item is not a special case, and fast and slow pointers to find a middle or a cycle.

Signals in the problem

  • The input is a linked list and you must do it in place or in O(1) space.
  • Find the middle, the kth from the end, or whether a cycle exists.
  • Reverse all or part of a list, or reorder it.
  • Merge or split lists.

Classic problem types

  • Reverse a singly linked list

    Three pointers: previous, current and a saved next.

  • Merge two sorted lists into one

    A dummy head and a tail pointer that always takes the smaller node.

  • Remove the nth node from the end in one pass

    Move a lead pointer n steps ahead, then move both until the lead hits the end.

Common mistakes

  • Overwriting next before saving it, which loses the rest of the list.
  • Special-casing the head instead of using a dummy node.
  • Reading fast.next.next without checking that fast.next exists.
Template 1 of 2

Reverse in place, and fast and slow pointers

Python
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverse(head):
    prev = None
    while head:
        nxt = head.next  # save it before you overwrite the link
        head.next = prev
        prev, head = head, nxt
    return prev

def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False
JavaScript
class ListNode {
  constructor(val = 0, next = null) {
    this.val = val;
    this.next = next;
  }
}

function reverse(head) {
  let prev = null;
  while (head) {
    const next = head.next; // save it before you overwrite the link
    head.next = prev;
    prev = head;
    head = next;
  }
  return prev;
}

function hasCycle(head) {
  let slow = head;
  let fast = head;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) return true;
  }
  return false;
}
Template 2 of 2

Dummy head: merge two sorted lists

Python
def merge_sorted(a, b):
    dummy = tail = ListNode()  # uses the ListNode class above
    while a and b:
        if a.val <= b.val:
            tail.next, a = a, a.next
        else:
            tail.next, b = b, b.next
        tail = tail.next
    tail.next = a or b  # attach whatever is left
    return dummy.next
JavaScript
function mergeSorted(a, b) {
  const dummy = new ListNode(); // uses the ListNode class above
  let tail = dummy;
  while (a && b) {
    if (a.val <= b.val) {
      tail.next = a;
      a = a.next;
    } else {
      tail.next = b;
      b = b.next;
    }
    tail = tail.next;
  }
  tail.next = a || b; // attach whatever is left
  return dummy.next;
}
Time and space complexity for the linked lists templates
TemplateTimeSpace
Reverse, cycle checkO(n)O(1)
Merge with a dummy headO(n + m)O(1)
Practice this pattern

Open the Coding Practice library filtered to linked lists problems.

Practice linked lists
Pattern 07 of 18

Trees

Most tree problems are a depth-first search that asks each subtree a question and combines the answers, or a breadth-first search that walks the tree one level at a time. Decide what each recursive call returns before you write it.

Signals in the problem

  • The input is a binary tree or a binary search tree.
  • The answer is about depth, paths, levels or a common ancestor.
  • "Level by level", "left side view" or "zigzag" point to BFS.
  • Sorted order or "kth smallest" in a BST points to an in-order walk.

Classic problem types

  • Maximum depth of a tree

    One plus the deeper of the two subtrees.

  • Is this a valid binary search tree?

    Pass the allowed low and high bounds down, not just a parent comparison.

  • Lowest common ancestor of two nodes

    Return a node up the tree when it, or something below it, is one of the targets.

Common mistakes

  • Checking only that each child is on the right side of its parent when validating a BST.
  • Forgetting the empty tree base case.
  • Deep recursion on a skewed tree: Python's default limit is about 1,000 frames, so say you would switch to an explicit stack.
Template 1 of 2

DFS for depth, BFS for levels

Python
from collections import deque

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def max_depth(root):  # depth-first
    if root is None:
        return 0
    return 1 + max(max_depth(root.left), max_depth(root.right))

def level_order(root):  # breadth-first, one level at a time
    levels = []
    queue = deque([root] if root else [])
    while queue:
        level = []
        for _ in range(len(queue)):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        levels.append(level)
    return levels
JavaScript
class TreeNode {
  constructor(val = 0, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function maxDepth(root) { // depth-first
  if (root === null) return 0;
  return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}

function levelOrder(root) { // breadth-first, one level at a time
  const levels = [];
  let queue = root ? [root] : [];
  while (queue.length) {
    levels.push(queue.map((node) => node.val));
    const next = [];
    for (const node of queue) {
      if (node.left) next.push(node.left);
      if (node.right) next.push(node.right);
    }
    queue = next;
  }
  return levels;
}
Template 2 of 2

Pass bounds down: is it a valid binary search tree?

Python
def is_valid_bst(root, low=float("-inf"), high=float("inf")):
    if root is None:
        return True
    if not (low < root.val < high):
        return False
    return (is_valid_bst(root.left, low, root.val)
            and is_valid_bst(root.right, root.val, high))
JavaScript
function isValidBst(root, low = -Infinity, high = Infinity) {
  if (root === null) return true;
  if (!(low < root.val && root.val < high)) return false;
  return isValidBst(root.left, low, root.val) && isValidBst(root.right, root.val, high);
}
Time and space complexity for the trees templates
TemplateTimeSpace
DFS (depth)O(n)O(h) for the call stack, h = height
BFS (levels)O(n)O(w), w = widest level
BST check with boundsO(n)O(h)
Practice this pattern

Open the Coding Practice library filtered to trees problems.

Practice trees
Pattern 08 of 18

Tries

A trie, or prefix tree, stores words one character per level, so words that share a start share a path. Lookups cost O(L) for a word of length L, no matter how many words are stored, and prefix questions become a single walk.

Signals in the problem

  • Many words, and the question is about prefixes.
  • Autocomplete, spell check or "starts with" queries.
  • Searching for many dictionary words at once, for example on a letter grid.
  • Wildcard search where one character can be anything.

Classic problem types

  • Build a prefix tree with insert, search and starts-with

    A node holds a map of children and an end-of-word flag.

  • A dictionary that supports "." as any letter

    Walk normally, and branch into every child when you hit a dot.

  • Find every dictionary word hidden on a letter grid

    Put the words in a trie, then DFS the grid and stop as soon as no word has that prefix.

Common mistakes

  • Treating a prefix as a full word because there is no end-of-word flag.
  • Using a sentinel key such as "$" that could collide with a real character.
  • Not pruning finished words in grid search, which repeats work and duplicates answers.
Template 1 of 1

Prefix tree with insert, search and prefix lookup

Python
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_word = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for ch in word:
            node = node.children.setdefault(ch, TrieNode())
        node.is_word = True

    def _walk(self, prefix):
        node = self.root
        for ch in prefix:
            node = node.children.get(ch)
            if node is None:
                return None
        return node

    def search(self, word):
        node = self._walk(word)
        return node is not None and node.is_word

    def starts_with(self, prefix):
        return self._walk(prefix) is not None
JavaScript
class TrieNode {
  constructor() {
    this.children = new Map();
    this.isWord = false;
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  insert(word) {
    let node = this.root;
    for (const ch of word) {
      if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
      node = node.children.get(ch);
    }
    node.isWord = true;
  }

  walk(prefix) {
    let node = this.root;
    for (const ch of prefix) {
      node = node.children.get(ch);
      if (!node) return null;
    }
    return node;
  }

  search(word) {
    const node = this.walk(word);
    return node !== null && node.isWord;
  }

  startsWith(prefix) {
    return this.walk(prefix) !== null;
  }
}
Time and space complexity for the tries templates
TemplateTimeSpace
Insert, search, prefixO(L) per wordO(total characters stored)
Practice this pattern

Open the Coding Practice library filtered to tries problems.

Practice tries
Pattern 09 of 18

Heaps

A heap, or priority queue, gives you the smallest (or largest) item in O(1) and adds or removes items in O(log n). Use it when you need the best few items, the next event in time order, or a running median.

Python's heapq is a min-heap only: push negated values to get a max-heap. JavaScript has no built-in heap, so interviewers expect you to write a small one, like the class below, or to say clearly that you would use a library.

Signals in the problem

  • Top k, kth largest or k closest.
  • Merge k sorted lists or streams.
  • Always process the next smallest or most urgent item.
  • A running median over a stream (two heaps).

Classic problem types

  • The kth largest value in a stream

    Keep a min-heap of size k. Its top is the answer.

  • Merge k sorted lists into one

    A heap of the current head of each list.

  • Running median as numbers arrive

    A max-heap for the low half and a min-heap for the high half, kept balanced.

Common mistakes

  • Sorting the whole input, O(n log n), when a size-k heap is O(n log k).
  • Forgetting that Python's heapq is a min-heap.
  • Pushing tuples with values that cannot be compared on ties. Add an index as a tie breaker.
Template 1 of 1

Top k: keep a min-heap of size k

Python
import heapq

def k_largest(nums, k):
    heap = []  # min-heap holding the k largest values seen so far
    for num in nums:
        heapq.heappush(heap, num)
        if len(heap) > k:
            heapq.heappop(heap)  # drop the smallest
    return sorted(heap, reverse=True)
JavaScript
// JavaScript has no built-in heap, so interviews expect a small one like this.
class MinHeap {
  constructor(less = (a, b) => a < b) {
    this.items = [];
    this.less = less;
  }

  size() {
    return this.items.length;
  }

  push(value) {
    const a = this.items;
    a.push(value);
    let i = a.length - 1;
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (!this.less(a[i], a[parent])) break;
      [a[i], a[parent]] = [a[parent], a[i]];
      i = parent;
    }
  }

  pop() {
    const a = this.items;
    const top = a[0];
    const last = a.pop();
    if (a.length > 0) {
      a[0] = last;
      let i = 0;
      for (;;) {
        const l = 2 * i + 1;
        const r = l + 1;
        let m = i;
        if (l < a.length && this.less(a[l], a[m])) m = l;
        if (r < a.length && this.less(a[r], a[m])) m = r;
        if (m === i) break;
        [a[i], a[m]] = [a[m], a[i]];
        i = m;
      }
    }
    return top;
  }
}

function kLargest(nums, k) {
  const heap = new MinHeap();
  for (const num of nums) {
    heap.push(num);
    if (heap.size() > k) heap.pop(); // drop the smallest
  }
  return heap.items.sort((a, b) => b - a);
}
Time and space complexity for the heaps templates
TemplateTimeSpace
Top k with a size-k heapO(n log k)O(k)
Practice this pattern

Open the Coding Practice library filtered to heaps problems.

Practice heaps
Pattern 10 of 18

Backtracking

Backtracking builds answers one choice at a time and steps back when a path cannot work. It is how you list every subset, permutation or valid arrangement. The skill is pruning: stopping early on paths that cannot lead to an answer.

Signals in the problem

  • "Return all" subsets, combinations, permutations or arrangements.
  • Placement puzzles with rules, such as queens on a board or filling a grid.
  • Small input limits (n up to about 15-20) that hint at exponential time.
  • Searching a grid for a word without reusing cells.

Classic problem types

  • Every subset of a set of numbers

    At each index, record the path, then try adding each later number.

  • Every combination that adds up to a target, with reuse

    Sort first so you can stop once a number is too big.

  • Place n queens so none attack each other

    One row at a time, with sets for used columns and diagonals.

Common mistakes

  • Saving the live path list instead of a copy, so every saved answer changes later.
  • Forgetting to undo the choice after the recursive call.
  • Not sorting and skipping equal neighbors when the input has duplicates.
Template 1 of 2

Choose or skip: every subset

Python
def subsets(nums):
    result, path = [], []

    def backtrack(start):
        result.append(path[:])  # save a copy, not the live list
        for i in range(start, len(nums)):
            path.append(nums[i])  # choose
            backtrack(i + 1)      # explore
            path.pop()            # undo

    backtrack(0)
    return result
JavaScript
function subsets(nums) {
  const result = [];
  const path = [];
  const backtrack = (start) => {
    result.push([...path]); // save a copy, not the live array
    for (let i = start; i < nums.length; i++) {
      path.push(nums[i]); // choose
      backtrack(i + 1); // explore
      path.pop(); // undo
    }
  };
  backtrack(0);
  return result;
}
Template 2 of 2

Prune early: combinations that add up to a target

Python
def combination_sum(candidates, target):
    nums = sorted(candidates)
    result, path = [], []

    def backtrack(start, remaining):
        if remaining == 0:
            result.append(path[:])
            return
        for i in range(start, len(nums)):
            if nums[i] > remaining:
                break  # sorted, so nothing after this fits either
            path.append(nums[i])
            backtrack(i, remaining - nums[i])  # i, not i + 1: reuse is allowed
            path.pop()

    backtrack(0, target)
    return result
JavaScript
function combinationSum(candidates, target) {
  const nums = [...candidates].sort((a, b) => a - b);
  const result = [];
  const path = [];
  const backtrack = (start, remaining) => {
    if (remaining === 0) {
      result.push([...path]);
      return;
    }
    for (let i = start; i < nums.length; i++) {
      if (nums[i] > remaining) break; // sorted, so nothing after this fits either
      path.push(nums[i]);
      backtrack(i, remaining - nums[i]); // i, not i + 1: reuse is allowed
      path.pop();
    }
  };
  backtrack(0, target);
  return result;
}
Time and space complexity for the backtracking templates
TemplateTimeSpace
Every subsetO(n * 2^n)O(n) plus the output
Combinations with pruningExponential, cut down by pruningO(target / smallest value) depth
Practice this pattern

Open the Coding Practice library filtered to backtracking problems.

Practice backtracking
Pattern 11 of 18

Graphs

Graph problems often hide as grids, prerequisites or "friends of friends". Model the nodes and edges, then use BFS for shortest steps in an unweighted graph, DFS for reachability and components, and topological sort for ordering with dependencies.

Signals in the problem

  • A grid where you move up, down, left and right.
  • Prerequisites or dependencies between tasks.
  • Connected groups, islands or regions.
  • Fewest steps or moves, with every step costing the same.

Classic problem types

  • Count the islands on a grid of land and water

    Start a BFS or DFS from each unvisited land cell.

  • Can every course be finished, given its prerequisites?

    Topological sort. A cycle means no.

  • How long until a spread reaches every cell

    Multi-source BFS: start with every source in the queue at once.

Common mistakes

  • Marking a cell visited when you pop it instead of when you push it, so it is queued many times.
  • Using DFS when the question asks for the fewest steps.
  • Forgetting the bounds check, or mixing up rows and columns.
  • Using queue.shift() in JavaScript, which is O(n). Use a head index instead.
Template 1 of 2

Grid BFS: count the islands

Python
from collections import deque

def count_islands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    seen = set()
    islands = 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] != "1" or (r, c) in seen:
                continue
            islands += 1
            seen.add((r, c))
            queue = deque([(r, c)])
            while queue:
                cr, cc = queue.popleft()
                for nr, nc in ((cr + 1, cc), (cr - 1, cc), (cr, cc + 1), (cr, cc - 1)):
                    if (0 <= nr < rows and 0 <= nc < cols
                            and grid[nr][nc] == "1" and (nr, nc) not in seen):
                        seen.add((nr, nc))  # mark when you enqueue, not when you pop
                        queue.append((nr, nc))
    return islands
JavaScript
function countIslands(grid) {
  if (grid.length === 0) return 0;
  const rows = grid.length;
  const cols = grid[0].length;
  const seen = Array.from({ length: rows }, () => new Array(cols).fill(false));
  const moves = [[1, 0], [-1, 0], [0, 1], [0, -1]];
  let islands = 0;
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] !== "1" || seen[r][c]) continue;
      islands++;
      seen[r][c] = true;
      const queue = [[r, c]];
      for (let head = 0; head < queue.length; head++) {
        const [cr, cc] = queue[head];
        for (const [dr, dc] of moves) {
          const nr = cr + dr;
          const nc = cc + dc;
          if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === "1" && !seen[nr][nc]) {
            seen[nr][nc] = true; // mark when you enqueue, not when you pop
            queue.push([nr, nc]);
          }
        }
      }
    }
  }
  return islands;
}
Template 2 of 2

Topological sort: an order that respects every prerequisite

Python
from collections import deque

def topo_order(n, edges):
    # edges: (a, b) means a must come before b
    graph = [[] for _ in range(n)]
    indegree = [0] * n
    for a, b in edges:
        graph[a].append(b)
        indegree[b] += 1
    queue = deque(i for i in range(n) if indegree[i] == 0)
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for nxt in graph[node]:
            indegree[nxt] -= 1
            if indegree[nxt] == 0:
                queue.append(nxt)
    return order if len(order) == n else []  # [] means there is a cycle
JavaScript
function topoOrder(n, edges) {
  // edges: [a, b] means a must come before b
  const graph = Array.from({ length: n }, () => []);
  const indegree = new Array(n).fill(0);
  for (const [a, b] of edges) {
    graph[a].push(b);
    indegree[b]++;
  }
  const queue = [];
  for (let i = 0; i < n; i++) if (indegree[i] === 0) queue.push(i);
  const order = [];
  for (let head = 0; head < queue.length; head++) {
    const node = queue[head];
    order.push(node);
    for (const next of graph[node]) {
      indegree[next]--;
      if (indegree[next] === 0) queue.push(next);
    }
  }
  return order.length === n ? order : []; // [] means there is a cycle
}
Time and space complexity for the graphs templates
TemplateTimeSpace
Grid BFSO(rows * cols)O(rows * cols)
Topological sortO(V + E)O(V + E)
Practice this pattern

Open the Coding Practice library filtered to graphs problems.

Practice graphs
Pattern 12 of 18

Advanced graphs

Advanced graph problems add weights or ask you to connect things cheaply. Dijkstra's algorithm finds the cheapest path when weights are non-negative. Union-find tracks which nodes are connected as edges arrive, which powers minimum spanning trees and cycle checks.

Signals in the problem

  • Edges have costs, times or distances.
  • Cheapest or fastest route, network delay, minimum effort.
  • Connect every point at the lowest total cost (minimum spanning tree).
  • Edges arrive one by one and you must know when a cycle forms or groups merge.

Classic problem types

  • How long until a signal reaches every node

    Dijkstra from the source. The answer is the largest distance.

  • Cheapest route with at most k stops

    Bellman-Ford style relaxation for k + 1 rounds, copying distances each round.

  • Lowest total cost to connect all points

    Sort edges by cost and join with union-find (Kruskal), or grow a tree with a heap (Prim).

Common mistakes

  • Using Dijkstra with negative edge weights.
  • Not skipping stale heap entries, which makes the algorithm much slower.
  • Union-find without path compression or union by rank.
Template 1 of 2

Dijkstra: shortest paths with non-negative weights

Python
import heapq

def dijkstra(n, edges, source):
    # edges: (u, v, weight) for a directed graph; weights must be non-negative
    graph = [[] for _ in range(n)]
    for u, v, w in edges:
        graph[u].append((v, w))
    dist = [float("inf")] * n
    dist[source] = 0
    heap = [(0, source)]
    while heap:
        d, node = heapq.heappop(heap)
        if d > dist[node]:
            continue  # stale entry: we already found a shorter path
        for nxt, w in graph[node]:
            if d + w < dist[nxt]:
                dist[nxt] = d + w
                heapq.heappush(heap, (dist[nxt], nxt))
    return dist
JavaScript
// Uses the MinHeap class from the Heaps template above.
function dijkstra(n, edges, source) {
  // edges: [u, v, weight] for a directed graph; weights must be non-negative
  const graph = Array.from({ length: n }, () => []);
  for (const [u, v, w] of edges) graph[u].push([v, w]);
  const dist = new Array(n).fill(Infinity);
  dist[source] = 0;
  const heap = new MinHeap((a, b) => a[0] < b[0]); // [distance, node]
  heap.push([0, source]);
  while (heap.size() > 0) {
    const [d, node] = heap.pop();
    if (d > dist[node]) continue; // stale entry: we already found a shorter path
    for (const [next, w] of graph[node]) {
      if (d + w < dist[next]) {
        dist[next] = d + w;
        heap.push([dist[next], next]);
      }
    }
  }
  return dist;
}
Template 2 of 2

Union-find: merge groups and catch cycles

Python
class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]  # path halving
            x = self.parent[x]
        return x

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False  # already connected: this edge would make a cycle
        if self.rank[ra] < self.rank[rb]:
            ra, rb = rb, ra
        self.parent[rb] = ra  # attach the shorter tree under the taller one
        if self.rank[ra] == self.rank[rb]:
            self.rank[ra] += 1
        return True
JavaScript
class UnionFind {
  constructor(n) {
    this.parent = Array.from({ length: n }, (_, i) => i);
    this.rank = new Array(n).fill(0);
  }

  find(x) {
    while (this.parent[x] !== x) {
      this.parent[x] = this.parent[this.parent[x]]; // path halving
      x = this.parent[x];
    }
    return x;
  }

  union(a, b) {
    let ra = this.find(a);
    let rb = this.find(b);
    if (ra === rb) return false; // already connected: this edge would make a cycle
    if (this.rank[ra] < this.rank[rb]) [ra, rb] = [rb, ra];
    this.parent[rb] = ra; // attach the shorter tree under the taller one
    if (this.rank[ra] === this.rank[rb]) this.rank[ra]++;
    return true;
  }
}
Time and space complexity for the advanced graphs templates
TemplateTimeSpace
Dijkstra with a heapO((V + E) log V)O(V + E)
Union-findNearly O(1) per operation (inverse Ackermann)O(V)
Practice this pattern

Open the Coding Practice library filtered to advanced graphs problems.

Practice advanced graphs
Pattern 13 of 18

1D dynamic programming

Dynamic programming solves a problem by combining answers to smaller versions of it, and saves each one so it is computed once. In 1D DP the state is a single index or amount: the best answer up to position i, or for amount a.

Use the same five steps every time. 1) State: say in words what dp[i] means. 2) Transition: how dp[i] comes from earlier states. 3) Base case: the smallest inputs. 4) Order: compute states so every dependency is ready. 5) Answer: which state holds the result.

Top-down means recursion plus a memo: fast to write and close to how you think. Bottom-up means filling a table in order: no recursion limit, and easy to shrink to a few variables when dp[i] only needs the last one or two states.

Most dynamic programming LeetCode-style problems in coding rounds fall into a few families: counting ways, min or max cost, yes or no reachability, and longest subsequence.

Signals in the problem

  • "How many ways", "minimum cost", "maximum value" or "is it possible".
  • Choices at each step, where a greedy pick can be wrong.
  • A brute force recursion that solves the same subproblem many times.
  • Constraints around a few thousand, where O(n^2) or O(n * amount) is fine.

Classic problem types

  • Number of ways to climb n stairs taking 1 or 2 steps

    ways(i) = ways(i - 1) + ways(i - 2).

  • Fewest coins that make an amount

    dp[a] = 1 + the best dp[a - coin] over every coin.

  • Largest total from items where no two neighbors are both taken

    Track the best total if you took or skipped the previous item.

Common mistakes

  • Starting to code before writing down what dp[i] means.
  • Wrong base case, such as dp[0] = 1 vs dp[0] = 0 for counting vs cost.
  • Top-down recursion that hits Python's recursion limit on large n.
  • Using a greedy rule where the problem needs DP, such as always taking the largest coin.
Template 1 of 3

Top-down: recursion plus a memo

Python
from functools import lru_cache

def count_ways(n, steps=(1, 2)):
    @lru_cache(maxsize=None)
    def ways(i):  # number of ways to reach step i
        if i == 0:
            return 1
        return sum(ways(i - s) for s in steps if s <= i)
    return ways(n)
JavaScript
function countWays(n, steps = [1, 2]) {
  const memo = new Map();
  const ways = (i) => { // number of ways to reach step i
    if (i === 0) return 1;
    if (memo.has(i)) return memo.get(i);
    let total = 0;
    for (const s of steps) if (s <= i) total += ways(i - s);
    memo.set(i, total);
    return total;
  };
  return ways(n);
}
Template 2 of 3

Bottom-up table: fewest coins for an amount

Python
def fewest_coins(coins, amount):
    impossible = amount + 1
    dp = [0] + [impossible] * amount  # dp[a] = fewest coins that make a
    for a in range(1, amount + 1):
        for coin in coins:
            if coin <= a:
                dp[a] = min(dp[a], dp[a - coin] + 1)
    return dp[amount] if dp[amount] != impossible else -1
JavaScript
function fewestCoins(coins, amount) {
  const impossible = amount + 1;
  const dp = new Array(amount + 1).fill(impossible); // dp[a] = fewest coins that make a
  dp[0] = 0;
  for (let a = 1; a <= amount; a++) {
    for (const coin of coins) {
      if (coin <= a) dp[a] = Math.min(dp[a], dp[a - coin] + 1);
    }
  }
  return dp[amount] === impossible ? -1 : dp[amount];
}
Template 3 of 3

Rolling variables: best total with no two neighbors

Python
def max_non_adjacent_sum(values):
    take, skip = 0, 0  # best total if the previous item was taken / skipped
    for v in values:
        take, skip = skip + v, max(take, skip)
    return max(take, skip)
JavaScript
function maxNonAdjacentSum(values) {
  let take = 0; // best total if the previous item was taken
  let skip = 0; // best total if it was skipped
  for (const v of values) {
    [take, skip] = [skip + v, Math.max(take, skip)];
  }
  return Math.max(take, skip);
}
Time and space complexity for the 1D dynamic programming templates
TemplateTimeSpace
Top-down with a memoO(n * number of steps)O(n)
Bottom-up tableO(amount * number of coins)O(amount)
Rolling variablesO(n)O(1)
Practice this pattern

Open the Coding Practice library filtered to 1D dynamic programming problems.

Practice 1D dynamic programming
Pattern 14 of 18

2D dynamic programming

In 2D DP the state needs two numbers: positions in two strings, a row and a column, or an item and a remaining capacity. You fill a table where each cell comes from its neighbors, and you can often keep just one row.

An extra row and column for the empty prefix removes most edge cases, which is why dp is sized (m + 1) by (n + 1) for two strings.

When a cell only needs the row above and the cell to its left, one row of memory is enough. Read the old value before you overwrite it.

Signals in the problem

  • Two strings or sequences compared together.
  • Paths on a grid that can only move in some directions.
  • Items with a weight limit (knapsack) or a target sum.
  • Edit distance, longest common subsequence or "interleave" wording.

Classic problem types

  • Longest subsequence two strings share

    Match: diagonal + 1. No match: best of up or left.

  • Count paths across a grid moving only right or down

    Each cell is the sum of the cell above and the cell to the left.

  • Fewest edits to turn one word into another

    Each cell takes the cheapest of insert, delete or replace.

Common mistakes

  • Off-by-one errors between dp indexes and string indexes. Write dp[i][j] = answer for a[:i], b[:j].
  • Filling the table in an order where a cell is read before it is computed.
  • Shrinking to one row but iterating in the wrong direction, which overwrites values you still need.
Template 1 of 2

Two sequences: longest common subsequence

Python
def lcs_length(a, b):
    rows, cols = len(a), len(b)
    # dp[i][j] = LCS of a[:i] and b[:j]; row 0 and column 0 stand for empty strings
    dp = [[0] * (cols + 1) for _ in range(rows + 1)]
    for i in range(1, rows + 1):
        for j in range(1, cols + 1):
            if a[i - 1] == b[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[rows][cols]
JavaScript
function lcsLength(a, b) {
  const rows = a.length;
  const cols = b.length;
  // dp[i][j] = LCS of a.slice(0, i) and b.slice(0, j); row 0 and column 0 stand for empty strings
  const dp = Array.from({ length: rows + 1 }, () => new Array(cols + 1).fill(0));
  for (let i = 1; i <= rows; i++) {
    for (let j = 1; j <= cols; j++) {
      dp[i][j] = a[i - 1] === b[j - 1]
        ? dp[i - 1][j - 1] + 1
        : Math.max(dp[i - 1][j], dp[i][j - 1]);
    }
  }
  return dp[rows][cols];
}
Template 2 of 2

Grid, one row of memory: paths moving only right or down

Python
def grid_paths(rows, cols):
    row = [1] * cols  # one way to reach every cell in the top row
    for _ in range(1, rows):
        for c in range(1, cols):
            row[c] += row[c - 1]  # paths from above (old value) + from the left
    return row[-1]
JavaScript
function gridPaths(rows, cols) {
  const row = new Array(cols).fill(1); // one way to reach every cell in the top row
  for (let r = 1; r < rows; r++) {
    for (let c = 1; c < cols; c++) {
      row[c] += row[c - 1]; // paths from above (old value) + from the left
    }
  }
  return row[cols - 1];
}
Time and space complexity for the 2D dynamic programming templates
TemplateTimeSpace
Two sequencesO(m * n)O(m * n), or O(n) with one row
Grid paths with one rowO(rows * cols)O(cols)
Practice this pattern

Open the Coding Practice library filtered to 2D dynamic programming problems.

Practice 2D dynamic programming
Pattern 15 of 18

Greedy

A greedy algorithm makes the best choice right now and never looks back. When it works it is short and fast, but it needs a reason. In an interview, say why the local choice can never hurt, or give a quick exchange argument.

Signals in the problem

  • Maximum or minimum with a natural "best next step".
  • Reaching the end of an array with jump lengths.
  • Scheduling or picking items where sorting first makes the choice obvious.
  • A DP solution exists, but you notice you only ever need the best option so far.

Classic problem types

  • Largest sum of a contiguous run

    Extend the current run or restart at this value, whichever is bigger.

  • Can you reach the last index with these jump lengths?

    Track the farthest index you can reach so far.

  • Where to start a round trip so the fuel never runs out

    If the running total drops below zero, the start must be after this point.

Common mistakes

  • Using greedy without checking it on a counterexample. Coins such as 1, 3 and 4 for an amount of 6 break the "largest coin first" idea.
  • Starting Kadane's best at 0, which returns 0 for an all-negative array.
  • Sorting by the wrong key in scheduling problems: by end time, not start time, to fit the most meetings.
Template 1 of 1

Extend or restart, and track the farthest reach

Python
def max_subarray_sum(nums):
    best = current = nums[0]
    for num in nums[1:]:
        current = max(num, current + num)  # extend the run or start over here
        best = max(best, current)
    return best

def can_reach_end(jumps):
    farthest = 0
    for i, jump in enumerate(jumps):
        if i > farthest:
            return False  # stuck before index i
        farthest = max(farthest, i + jump)
    return True
JavaScript
function maxSubarraySum(nums) {
  let best = nums[0];
  let current = nums[0];
  for (let i = 1; i < nums.length; i++) {
    current = Math.max(nums[i], current + nums[i]); // extend the run or start over here
    best = Math.max(best, current);
  }
  return best;
}

function canReachEnd(jumps) {
  let farthest = 0;
  for (let i = 0; i < jumps.length; i++) {
    if (i > farthest) return false; // stuck before index i
    farthest = Math.max(farthest, i + jumps[i]);
  }
  return true;
}
Time and space complexity for the greedy templates
TemplateTimeSpace
Best run, farthest reachO(n)O(1)
Practice this pattern

Open the Coding Practice library filtered to greedy problems.

Practice greedy
Pattern 16 of 18

Intervals

Interval problems become simple once the intervals are sorted. After sorting by start, overlaps can only happen with the last interval you kept. Counting how many overlap at once is a sweep over sorted starts and ends.

Signals in the problem

  • Pairs of start and end times or positions.
  • Merge, insert or remove overlapping ranges.
  • How many rooms, platforms or machines are needed at the same time.
  • Free time across several calendars.

Classic problem types

  • Merge every overlapping range

    Sort by start, then extend or append.

  • Fewest rooms so every meeting fits

    Sort starts and ends separately and sweep, or keep a min-heap of end times.

  • Fewest ranges to remove so none overlap

    Sort by end time and keep each range that starts at or after the last kept end.

Common mistakes

  • Forgetting to sort first.
  • Not agreeing on touching ends: does [1, 2] overlap [2, 3]? Ask, then code it on purpose.
  • Changing the caller's input when you sort or merge. Copy it, or say that you are changing it.
Template 1 of 2

Sort by start, then merge

Python
def merge_intervals(intervals):
    merged = []
    for start, end in sorted(intervals):
        if merged and start <= merged[-1][1]:  # overlaps the last kept interval
            merged[-1][1] = max(merged[-1][1], end)
        else:
            merged.append([start, end])
    return merged
JavaScript
function mergeIntervals(intervals) {
  const sorted = [...intervals].sort((x, y) => x[0] - y[0]);
  const merged = [];
  for (const [start, end] of sorted) {
    const last = merged[merged.length - 1];
    if (last && start <= last[1]) { // overlaps the last kept interval
      last[1] = Math.max(last[1], end);
    } else {
      merged.push([start, end]);
    }
  }
  return merged;
}
Template 2 of 2

Sweep starts and ends: fewest rooms for every meeting

Python
def min_rooms(meetings):
    starts = sorted(s for s, _ in meetings)
    ends = sorted(e for _, e in meetings)
    rooms = done = 0
    for s in starts:
        if s >= ends[done]:
            done += 1   # a meeting has ended: reuse its room
        else:
            rooms += 1  # every room is busy: open a new one
    return rooms
JavaScript
function minRooms(meetings) {
  const starts = meetings.map((m) => m[0]).sort((a, b) => a - b);
  const ends = meetings.map((m) => m[1]).sort((a, b) => a - b);
  let rooms = 0;
  let done = 0;
  for (const s of starts) {
    if (s >= ends[done]) done++; // a meeting has ended: reuse its room
    else rooms++; // every room is busy: open a new one
  }
  return rooms;
}
Time and space complexity for the intervals templates
TemplateTimeSpace
MergeO(n log n)O(n) for the output
Sweep starts and endsO(n log n)O(n)
Practice this pattern

Open the Coding Practice library filtered to intervals problems.

Practice intervals
Pattern 17 of 18

Math and geometry

These problems reward knowing a few tricks: rotate a matrix by transposing and reversing, walk a matrix in a spiral with four shrinking bounds, and compute powers in O(log n) by squaring. Watch number limits closely.

Signals in the problem

  • Rotate, transpose or spiral through a matrix.
  • Powers, digits, greatest common divisor or modulo arithmetic.
  • Points, lines and distances on a plane.
  • A loop that would run n times for a very large n.

Classic problem types

  • Rotate a square image 90 degrees in place

    Transpose, then reverse every row.

  • Read a matrix in spiral order

    Keep top, bottom, left and right bounds and shrink one after each pass.

  • Raise a number to a large power quickly

    Square the base and halve the exponent each step.

Common mistakes

  • Overflow: JavaScript numbers lose integer precision above 2^53. Use BigInt when exact large integers matter.
  • Using bit shifts on big numbers in JavaScript, where >> works on 32-bit integers.
  • Rotating with a copy when the problem asks for in place.
Template 1 of 1

Rotate a matrix in place, and fast power

Python
def rotate_clockwise(matrix):
    n = len(matrix)
    for i in range(n):              # 1) transpose across the main diagonal
        for j in range(i + 1, n):
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
    for row in matrix:              # 2) reverse each row
        row.reverse()

def fast_pow(x, n):
    if n < 0:
        x, n = 1 / x, -n
    result = 1
    while n:
        if n & 1:      # this bit of n is set
            result *= x
        x *= x         # square for the next bit
        n >>= 1
    return result
JavaScript
function rotateClockwise(matrix) {
  const n = matrix.length;
  for (let i = 0; i < n; i++) { // 1) transpose across the main diagonal
    for (let j = i + 1; j < n; j++) {
      [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
    }
  }
  for (const row of matrix) row.reverse(); // 2) reverse each row
}

function fastPow(x, n) {
  if (n < 0) {
    x = 1 / x;
    n = -n;
  }
  let result = 1;
  while (n > 0) {
    if (n % 2 === 1) result *= x; // this bit of n is set
    x *= x; // square for the next bit
    n = Math.floor(n / 2); // not n >> 1, which breaks past 32 bits
  }
  return result;
}
Time and space complexity for the math and geometry templates
TemplateTimeSpace
Rotate in placeO(n^2)O(1)
Fast powerO(log n)O(1)
Practice this pattern

Open the Coding Practice library filtered to math and geometry problems.

Practice math and geometry
Pattern 18 of 18

Bit manipulation

Bit tricks solve some problems in O(1) extra space. XOR cancels pairs, `n & (n - 1)` clears the lowest set bit, and shifting builds or reads bits one at a time. They are rare in interviews but quick wins when they show up.

Signals in the problem

  • Every value appears twice except one.
  • Count set bits, reverse bits, or check for a power of two.
  • Add or subtract without + or -.
  • Subsets of a small set stored as a bitmask.

Classic problem types

  • The one value that appears once when every other appears twice

    XOR everything. Pairs cancel out.

  • Number of 1 bits for every number from 0 to n

    bits[i] = bits[i >> 1] + (i & 1).

  • Reverse the bits of a 32-bit integer

    Shift the answer left and add the lowest bit of the input, 32 times.

Common mistakes

  • In JavaScript, bitwise operators work on 32-bit signed integers. Use >>> 0 to read the result as unsigned.
  • Operator precedence: in JavaScript n & n - 1 === 0 does not mean what it looks like. Add parentheses.
  • Python integers have no fixed width, so negative numbers need a mask such as & 0xFFFFFFFF.
Template 1 of 1

XOR, low-bit tricks and bit counts

Python
def single_number(nums):
    result = 0
    for num in nums:
        result ^= num  # pairs cancel out: a ^ a == 0
    return result

def is_power_of_two(n):
    return n > 0 and n & (n - 1) == 0  # n & (n - 1) clears the lowest set bit

def count_bits_up_to(n):
    bits = [0] * (n + 1)  # bits[i] = number of 1 bits in i
    for i in range(1, n + 1):
        bits[i] = bits[i >> 1] + (i & 1)
    return bits
JavaScript
function singleNumber(nums) {
  let result = 0;
  for (const num of nums) result ^= num; // pairs cancel out: a ^ a === 0
  return result;
}

function isPowerOfTwo(n) {
  // Parentheses matter: in JavaScript, === binds tighter than &.
  return n > 0 && (n & (n - 1)) === 0; // safe for n below 2 ** 31
}

function countBitsUpTo(n) {
  const bits = new Array(n + 1).fill(0); // bits[i] = number of 1 bits in i
  for (let i = 1; i <= n; i++) bits[i] = bits[i >> 1] + (i & 1);
  return bits;
}
Time and space complexity for the bit manipulation templates
TemplateTimeSpace
XOR for the single valueO(n)O(1)
Bit counts up to nO(n)O(n) for the output
Practice this pattern

Open the Coding Practice library filtered to bit manipulation problems.

Practice bit manipulation
Patterns vs problem lists

Patterns vs Blind 75, NeetCode 150 and Grind 75

Problem lists tell you what to solve. Patterns tell you how to think about a problem you have never seen. The best use of a list is as practice material for the patterns on this page.

ListMade bySizeHow it is organizedBest for
Blind 75Yangshun Tay, an ex-Meta Staff Engineer, who first shared it as a post on Blind, the anonymous workplace network75 problemsA fixed list grouped by topicA short core set if you already know the basics
Grind 75Yangshun Tay, the author of Blind 75, on his Tech Interview Handbook site75 problems by default, from a pool of 169A week-by-week schedule you can change by weeks, hours per week, difficulty and topic. The default is 8 weeks at 8 hours a week.A paced plan with a deadline
NeetCode 150Navdeep Singh, a former Google engineer and the founder of NeetCode.io150 problems: 28 Easy, 101 Medium, 21 HardGrouped into 18 topics and ordered so each problem builds on the ones before it, with video explanationsBroad, topic-by-topic coverage

Where lists fall short

It is easy to finish a list and remember answers instead of ideas. A new problem in the real round then feels unfamiliar, even when it uses a pattern you have solved ten times. Fix that by naming the pattern for every list problem, writing the template from memory and mixing patterns once you know them.

A note on names

These lists link to problems on LeetCode. LeetCode is a trademark of its owner, and OphyAI is not affiliated with LeetCode or with these lists. The problem types on this page and the problems in OphyAI Coding Practice are written in our own words. For other places to practice, see LeetCode alternatives.

Study plan

How to study coding patterns in 4-6 weeks

Six weeks at about an hour a day is enough to work through every pattern and still have time for mixed practice. Short on time? Use the four-week version below.

WeekPatternsGoal for the week
Week 1Arrays and hashing, two pointers, sliding window, stackWrite each template from memory. Three or four problems per pattern.
Week 2Binary search, linked lists, trees, triesGet fast and calm with pointer bugs and recursion.
Week 3Heaps, backtracking, graphsBFS, DFS and topological sort without notes.
Week 4Advanced graphs, 1D dynamic programmingDijkstra and union-find, then the five DP steps on every problem.
Week 52D dynamic programming, greedy, intervalsTables over two indexes. Justify every greedy choice out loud.
Week 6Math and geometry, bit manipulation, mixed reviewMixed sets where you have to pick the pattern yourself, plus full mock rounds.

Every study day

  • One new problem in this week's pattern and one review problem from an earlier week, five days a week.
  • Give yourself about 25 minutes to find an approach before you look at a hint, and take hints one at a time.
  • Say the approach and its complexity out loud before you type, the way you would in the real round.
  • Test every answer with an empty input, a single item, duplicates and the largest input size.
  • Keep a short error log: the pattern, what went wrong and the fix. Reread it before each session.
  • Redo any problem you needed the answer for after three days, then again after a week.

The four-week version

Keep weeks 1 and 2 as they are. In week 3, cover heaps, backtracking, graphs and advanced graphs. In week 4, cover both kinds of dynamic programming, greedy and intervals, and leave math, geometry and bit manipulation for a quick read. Cut to two or three problems per pattern and keep the mixed review and mock rounds on the last two days.

Whichever plan you pick, finish with timed practice. A mock round in Coding Practice is 45 or 60 minutes with two problems picked for your level and a voice AI interviewer.

OphyAI Coding Practice

Practice every pattern in one place

OphyAI Coding Practice is a coding interview practice room: 178 problems, an in-browser editor, hints one at a time, an AI coach that checks your plan and reviews your code, and timed mock rounds with a voice AI interviewer.

153 problems in 18 patterns

153 algorithm problems across 18 patterns plus 25 Terraform tasks, in Easy, Medium and Hard. Python and JavaScript; Terraform tasks use the AWS provider 5.x and are checked against the task's written requirements.

An editor in your browser

Write Python or JavaScript in an in-browser editor at app.ophyai.com. Pick problems by pattern, and by level from Easy to Hard, to follow this guide.

Hints one at a time

Ask for the next hint only when you need it, so you still do the thinking. The AI coach checks your plan and reviews your code.

Mock coding rounds

A mock coding round is 45 or 60 minutes with two problems picked for your level, a voice AI interviewer (typing always works too) and a scorecard for problem solving, coding, correctness, complexity, testing and communication.

Practice explaining

In a mock round you explain your approach to a voice AI interviewer, so you practice thinking out loud. Typing always works too.

What it costs

The full library and AI coach come with Pro and Premium with no credits per problem. A mock coding round uses 20 credits. Free accounts get 4 sample problems.

Runs in the browser at app.ophyai.com; the desktop app links to it. Not in the phone apps. See how Coding Practice works

FAQ

Coding interview patterns FAQ

What are coding interview patterns?

Coding interview patterns are reusable ways to solve whole families of problems, such as two pointers, sliding window, binary search, BFS and DFS, backtracking and dynamic programming. Instead of memorizing hundreds of answers, you learn to spot the signals in a problem statement, pick the matching pattern and adapt its template. Most problems in a coding round are one or two of about 18 patterns.

How many LeetCode patterns are there?

There is no official number. Popular lists group problems into somewhere between 14 and 20 topics. This guide uses 18: arrays and hashing, two pointers, sliding window, stack, binary search, linked lists, trees, tries, heaps, backtracking, graphs, advanced graphs, 1D dynamic programming, 2D dynamic programming, greedy, intervals, math and geometry, and bit manipulation. Together they cover the large majority of problems you are likely to see in a coding interview.

Should I learn patterns or just solve a lot of problems?

Both, in that order. Learn one pattern, solve three to six problems that use it, then mix problems from different patterns so you practice choosing the pattern yourself. Solving hundreds of random problems without naming the pattern behind each one is slow, and it does not prepare you for a problem you have never seen.

Which coding patterns matter most?

Arrays and hashing, two pointers, sliding window, trees, graphs (BFS and DFS) and binary search come up most often, and they are the base for the rest. Dynamic programming and backtracking take the longest to get comfortable with, so start them early. Bit manipulation and math and geometry are worth a short review, not weeks.

What is the sliding window algorithm?

The sliding window algorithm finds the best contiguous subarray or substring in one pass. You keep a window between a left and a right index, add the next item on the right, and move the left edge forward whenever the window breaks the rule. Each item enters and leaves the window at most once, so the scan is O(n) instead of the O(n^2) of checking every start and end.

What is the two pointer technique?

The two pointer technique moves two indexes through the same array or string instead of nesting two loops. With opposite-end pointers on sorted data, each comparison rules out a whole row of pairs, which gives O(n) pair and palindrome checks. With a slow and a fast pointer moving the same way, you can filter an array in place, or find the middle of a linked list or a cycle in it.

How do I get better at dynamic programming?

Use the same five steps on every problem: say what dp[i] means in words, write how it comes from smaller states, set the base cases, pick an order so every dependency is ready, and say which state holds the answer. Start with top-down recursion plus a memo, then turn it into a bottom-up table. Work through the 1D families (counting ways, min cost, reachability) before 2D problems on two strings or a grid.

Is Blind 75 enough to prepare for coding interviews?

Blind 75 is a strong core, but it is small, and many people finish it having memorized answers rather than patterns. Treat any list as practice material for patterns: for each problem, name the pattern, write the template from memory and explain the complexity. Grind 75 and NeetCode 150 are larger lists that fill in more topics.

Which language should I use for coding interviews?

Use the language you write fastest and most accurately. Python is popular because it is short and has built-in tools like heapq, deque and Counter. JavaScript works well too, but you have to write a small heap yourself and watch 32-bit bitwise math. OphyAI Coding Practice supports Python and JavaScript, and every template on this page is written in both.

Does OphyAI Coding Practice use LeetCode problems?

No. OphyAI is not affiliated with LeetCode, and Coding Practice problems are written in our own words. The library has 153 algorithm problems across the same 18 patterns as this guide, plus 25 Terraform tasks, in Easy, Medium and Hard, with hints one at a time and an AI coach that checks your plan and reviews your code. The full library comes with Pro and Premium, and free accounts get 4 sample problems.

Learn the pattern. Then practice it until it is yours.

Work through the 18 patterns with hints one at a time and an AI coach, then try a mock coding round with a voice AI interviewer.