Interview Prep

DSA Patterns · Code Templates · LeetCode

A structured reference for coding interviews. Each topic explains the underlying pattern and a reusable template — so problems become recognition exercises, not memory tests.

18
Topics
~165
Problems
0
Solved
How to use
  1. Read the pattern — understand the core technique and when to apply it
  2. Study the code template until you can write it cold; use the copy button
  3. Work through problems in order (Easy → Hard) and check them off as you go
Press / to focus
DIFFICULTY:
0 / 0 solved
No matches found.

Arrays & Strings

The Pattern

Most array/string problems reduce to one of four shapes: prefix sums (range queries in O(1)), Kadane's algorithm (max-sum subarray), in-place two-pass (extra space → O(1)), or frequency counting. Recognise the shape first.

Reach for this when: the problem mentions a contiguous subarray/substring, a running total or range sum, a "maximum/minimum subarray", or asks you to modify an array without extra space.
Template — Prefix Sum
# Build prefix sums → range sum query in O(1)
prefix = [0] * (len(arr) + 1)
for i, v in enumerate(arr):
    prefix[i + 1] = prefix[i] + v

# sum of arr[l..r]  (0-indexed, inclusive)
def range_sum(l, r):
    return prefix[r + 1] - prefix[l]
Template — Kadane's (Max Subarray)
max_sum = cur_sum = arr[0]
for num in arr[1:]:
    cur_sum = max(num, cur_sum + num)   # restart or extend
    max_sum = max(max_sum, cur_sum)
# max_sum holds the answer
Time: O(n) Space: O(n) prefix · O(1) Kadane

Problems

1. Two Sumhash complementEasy
217. Contains Duplicatehash set O(1) lookupEasy
242. Valid Anagramfrequency count / sortEasy
53. Maximum SubarrayKadane's algorithmMedium
238. Product of Array Except Selfprefix + suffix productsMedium
152. Maximum Product Subarraytrack min & max simultaneouslyMedium
189. Rotate Arraytriple-reverse trick, O(1) spaceMedium
42. Trapping Rain Waterprefix/suffix max arrays or two pointersHard

Two Pointers

The Pattern

Two pointers eliminate a nested loop. On a sorted array, a meet-in-the-middle pair (left=0, right=n−1) finds pairs/triplets in O(n). A fast-slow pair (both moving right) partitions the array in-place without extra space.

Reach for this when: the input is sorted (or can be sorted without breaking the answer), you need to find pairs/triplets summing to a target, remove/move elements in-place, or check palindrome-like properties.
Template — Meet in the Middle
left, right = 0, len(arr) - 1
while left < right:
    s = arr[left] + arr[right]
    if s == target:
        # found a pair
        left += 1; right -= 1
    elif s < target:
        left += 1   # need larger sum
    else:
        right -= 1  # need smaller sum
Template — Fast / Slow (in-place partition)
slow = 0                         # next write position
for fast in range(len(arr)):
    if keep(arr[fast]):          # your condition here
        arr[slow] = arr[fast]
        slow += 1
# arr[:slow] is the filtered result
Template — 3Sum (sort + two-pointer for each pivot)
nums.sort()
result = []
for i, pivot in enumerate(nums):
    if i > 0 and nums[i] == nums[i-1]: continue  # skip dup pivots
    l, r = i + 1, len(nums) - 1
    while l < r:
        s = pivot + nums[l] + nums[r]
        if   s < 0: l += 1
        elif s > 0: r -= 1
        else:
            result.append([pivot, nums[l], nums[r]])
            while l < r and nums[l] == nums[l+1]: l += 1
            while l < r and nums[r] == nums[r-1]: r -= 1
            l += 1; r -= 1
Time: O(n) pair · O(n²) triplet Space: O(1)

Problems

125. Valid Palindromeconverging pointers, skip non-alphanumEasy
283. Move Zeroespartition pointerEasy
167. Two Sum IImeet-in-middle on sorted inputMedium
15. 3Sumsort + two-pointer per pivot, skip dupsMedium
11. Container With Most Watermove shorter side inwardMedium
75. Sort ColorsDutch National Flag (3-way partition)Medium

Sliding Window

The Pattern

Maintain a window [left, right] over the array. Expand right greedily; shrink left only when the window violates the constraint. The key question to ask yourself: "what makes a window valid, and how do I check it in O(1)?" Usually the answer is a counter or a hash map.

Reach for this when: the problem asks for the longest/shortest contiguous subarray or substring satisfying a constraint. If window size is fixed, it's a simpler rolling sum. If variable, use the expand-shrink template.
Template — Variable Window
from collections import defaultdict

left = 0
window = defaultdict(int)   # or a Counter / set
result = 0

for right in range(len(s)):
    window[s[right]] += 1           # expand: include s[right]

    while not valid(window):        # shrink until valid
        window[s[left]] -= 1
        if window[s[left]] == 0:
            del window[s[left]]
        left += 1

    result = max(result, right - left + 1)  # window size
Template — Fixed Window (size k)
k = ...
window_sum = sum(arr[:k])
best = window_sum
for i in range(k, len(arr)):
    window_sum += arr[i] - arr[i - k]  # slide
    best = max(best, window_sum)
Key trick — "at most K distinct" → longest window with ≤ K distinct
# Many hard window problems decompose:
# "exactly K" = f(at most K) - f(at most K-1)
Time: O(n) — each element enters/leaves once Space: O(k) window state

Problems

643. Maximum Average Subarray Ifixed-size rolling sumEasy
424. Longest Repeating Character Replacementwindow - max_count ≤ kMedium
567. Permutation in Stringfixed window, freq compareMedium
76. Minimum Window Substringhave/need counters, shrink on validHard
239. Sliding Window Maximummonotonic deque (O(n))Hard

Linked Lists

The Pattern

Linked list problems almost always reduce to one of three techniques: reversal with prev/curr pointers, a dummy head node to simplify edge cases at the head, and a two-pointer runner (e.g., move one pointer k steps ahead to find the kth-from-end node). Always draw the pointer reassignment order before coding — getting it backwards causes off-by-one bugs that are hard to debug.

Reach for this when: you see "reverse", "merge sorted lists", "remove nth from end", "reorder", "add two numbers", or "palindrome linked list". For cycle problems, use Fast & Slow Pointers instead.
Template — Iterative Reversal
def reverse_list(head):
    prev, curr = None, head
    while curr:
        nxt       = curr.next   # save next
        curr.next = prev        # reverse the link
        prev      = curr        # advance prev
        curr      = nxt         # advance curr
    return prev                 # new head
Template — Dummy Head (merge / remove)
def merge_two_lists(l1, l2):
    dummy = ListNode(0)
    cur   = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            cur.next, l1 = l1, l1.next
        else:
            cur.next, l2 = l2, l2.next
        cur = cur.next
    cur.next = l1 or l2
    return dummy.next
Template — Remove Nth From End (two-pointer gap)
def remove_nth_from_end(head, n):
    dummy      = ListNode(0, head)
    fast = slow = dummy
    for _ in range(n + 1):      # advance fast n+1 steps
        fast = fast.next
    while fast:                 # move both until fast hits end
        fast = fast.next
        slow = slow.next
    slow.next = slow.next.next  # skip the target node
    return dummy.next
Time: O(n) Space: O(1)

Problems

206. Reverse Linked Listprev/curr iterationEasy
21. Merge Two Sorted Listsdummy head + pointer advanceEasy
141. Linked List Cyclehash set visited OR fast/slowEasy
234. Palindrome Linked Listfind mid, reverse second half, compareEasy
19. Remove Nth Node From End of Listtwo-pointer n+1 gapMedium
2. Add Two Numbersdummy head, carry propagationMedium
143. Reorder Listfind mid + reverse second half + mergeMedium
148. Sort Listmerge sort on linked list (find mid, split, merge)Medium
23. Merge K Sorted Listsmin-heap of (val, node) across k listsHard
25. Reverse Nodes in k-Groupreverse in chunks, re-link groupsHard

Fast & Slow Pointers

The Pattern — Floyd's Tortoise & Hare

Two pointers advance at different speeds through the same structure. The fast pointer moves 2 steps per iteration; the slow moves 1. If a cycle exists, they must eventually meet — the fast pointer laps the slow one inside the loop. If no cycle, fast reaches null first.

This works on any structure where you can follow a "next" link, including arrays used as implicit linked lists (e.g., 287. Find the Duplicate, where index → value forms a linked list).

Reach for this when: you see "detect cycle", "find cycle entry", "find middle of linked list", "happy number", or any problem where following a chain of values could loop.
Template — Cycle Detection
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         # cycle detected
    return False
Template — Find Cycle Entry (Linked List Cycle II)
def detect_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:
            # Reset one pointer to head; advance both at speed 1.
            # They meet at the cycle entry.
            slow = head
            while slow is not fast:
                slow, fast = slow.next, fast.next
            return slow
    return None
Template — Find Middle
def middle_node(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow                 # for even length, returns second middle
Time: O(n) Space: O(1)

Problems

876. Middle of the Linked Listfast reaches null when slow is at midEasy
202. Happy Numbersum-of-squares forms implicit linked list; detect cycleEasy
142. Linked List Cycle IImeet inside cycle → reset one to head → meet at entryMedium
287. Find the Duplicate Numberindex→value = implicit linked list; Floyd's finds duplicateMedium
457. Circular Array Loopfast/slow on circular index jumps; direction must stay consistentMedium

Hashing

The Pattern

Hash maps trade O(n) extra space for O(1) lookup, turning brute-force O(n²) into O(n). Three main uses: complement lookup (two-sum style), frequency counting (anagram/top-K), and canonical grouping (group anagrams). For existence checks alone, a set is enough.

Reach for this when: you need to find a pair, detect a duplicate, count frequencies, or group items by some property — and O(n²) is too slow.
Template — Complement Lookup
seen = {}                           # val → index
for i, num in enumerate(nums):
    complement = target - num
    if complement in seen:
        return [seen[complement], i]
    seen[num] = i
Template — Frequency / Top-K
from collections import Counter

freq = Counter(nums)
# bucket sort for O(n) top-K (beats heap when k ~ n)
buckets = [[] for _ in range(len(nums) + 1)]
for val, cnt in freq.items():
    buckets[cnt].append(val)

result = []
for cnt in range(len(buckets) - 1, 0, -1):
    result.extend(buckets[cnt])
    if len(result) >= k:
        return result[:k]
Template — Canonical Grouping
from collections import defaultdict

groups = defaultdict(list)
for word in words:
    key = tuple(sorted(word))       # canonical form
    groups[key].append(word)
return list(groups.values())
Time: O(n) Space: O(n)

Problems

1. Two Sumcomplement mapEasy
290. Word Patternbidirectional char↔word mappingEasy
205. Isomorphic Stringstwo maps, one per directionEasy
49. Group Anagramssorted-word as keyMedium
347. Top K Frequent ElementsCounter + bucket sortMedium
128. Longest Consecutive Sequenceonly start sequences from roots (num-1 not in set)Medium
271. Encode and Decode Stringslength-prefix encodingMedium

Stacks & Queues

The Pattern

A stack is useful whenever you need to undo or match something recently seen (parentheses, function calls). The power move is the monotonic stack: maintain a stack that is always increasing (or decreasing). When a new element breaks monotonicity, pop and process — this gives you "next greater/smaller element" in O(n).

Reach for this when: you see nested/matching structure (brackets, HTML tags), need the "next greater/smaller element" for each position, or need to compute span/histogram areas.
Template — Balanced Brackets
pairs = {')': '(', ']': '[', '}': '{'}
stack = []
for ch in s:
    if ch in '([{':
        stack.append(ch)
    elif not stack or stack[-1] != pairs[ch]:
        return False
    else:
        stack.pop()
return not stack
Template — Monotonic Stack (Next Greater Element)
result = [-1] * len(arr)
stack = []                  # stores indices, arr is decreasing from bottom→top

for i in range(len(arr)):
    # arr[i] is greater than everything under it → pop and resolve
    while stack and arr[stack[-1]] < arr[i]:
        result[stack.pop()] = arr[i]
    stack.append(i)
# remaining indices in stack have no greater element → result stays -1
Template — Two Stacks = Queue
inbox, outbox = [], []

def push(x): inbox.append(x)

def pop():
    if not outbox:                  # amortised O(1)
        while inbox:
            outbox.append(inbox.pop())
    return outbox.pop()
Time: O(n) amortised (each element pushed/popped once) Space: O(n)

Problems

20. Valid Parenthesesmatching stackEasy
155. Min Stackparallel min stackMedium
739. Daily Temperaturesmonotonic decreasing stackMedium
853. Car Fleetstack of finish times after sorting by startMedium
22. Generate Parenthesesbacktracking with open/close countersMedium
84. Largest Rectangle in Histogrammonotonic increasing stack, extend backwardsHard

Merge Intervals

The Pattern

Sort intervals by start time. Then iterate: if the current interval's start is ≤ the previous interval's end, they overlap — merge by extending the end to max(prev.end, curr.end). Otherwise push the previous interval and start fresh with the current one.

A common variant is "how many rooms are needed?" — sort start and end times into separate arrays and use a two-pointer sweep to count concurrent overlaps.

Reach for this when: the input is a list of intervals and the problem asks to merge, count overlaps, find gaps, remove minimum, or find intersections. Dead giveaway words: "meeting rooms", "overlapping", "insert interval", "minimum arrows".
Template — Merge Overlapping Intervals
def merge(intervals):
    intervals.sort(key=lambda x: x[0])
    merged = [intervals[0]]
    for start, end in intervals[1:]:
        if start <= merged[-1][1]:          # overlap
            merged[-1][1] = max(merged[-1][1], end)
        else:
            merged.append([start, end])
    return merged
Template — Minimum Meeting Rooms (sweep)
import heapq

def min_meeting_rooms(intervals):
    intervals.sort()            # sort by start
    heap = []                   # min-heap of end times
    for start, end in intervals:
        if heap and heap[0] <= start:
            heapq.heapreplace(heap, end)  # reuse freed room
        else:
            heapq.heappush(heap, end)     # need new room
    return len(heap)
Time: O(n log n) Space: O(n)

Problems

56. Merge Intervalssort by start, extend end on overlapMedium
57. Insert Intervalwalk list: add before/overlap/after new intervalMedium
435. Non-Overlapping Intervalsgreedy: remove interval with larger end when overlap foundMedium
452. Minimum Number of Arrows to Burst Balloonssort by end; arrow at curr.end bursts all overlappingMedium
986. Interval List Intersectionstwo-pointer on sorted lists; advance whichever ends earlierMedium
1288. Remove Covered Intervalssort by start desc end; count non-coveredMedium
759. Employee Free Timeflatten + sort all intervals; find gaps between merged spansHard

Trees

The Pattern

Almost every tree problem is a variant of DFS or BFS. Post-order DFS (process children before parent) solves path-sum and height problems. Pre-order (process parent first) suits serialization or printing. BFS with a deque naturally gives you level-by-level access. For BSTs, exploit the invariant: left < node < right.

Reach for this when: you see a tree structure. Default to recursive DFS — write the helper's return value contract first, then implement. Switch to BFS only when you need levels or shortest-path semantics.
Template — DFS (post-order)
def dfs(node):
    if not node:
        return 0                        # base case (adjust per problem)
    left  = dfs(node.left)
    right = dfs(node.right)
    # combine at current node
    return 1 + max(left, right)         # e.g. height
Template — BFS (level-order)
from collections import deque

def level_order(root):
    if not root: return []
    q, result = deque([root]), []
    while q:
        level = []
        for _ in range(len(q)):         # freeze level size
            node = q.popleft()
            level.append(node.val)
            if node.left:  q.append(node.left)
            if node.right: q.append(node.right)
        result.append(level)
    return result
Template — BST Validation (pass valid range down)
def is_valid(node, lo=float('-inf'), hi=float('inf')):
    if not node: return True
    if not (lo < node.val < hi): return False
    return (is_valid(node.left,  lo, node.val) and
            is_valid(node.right, node.val, hi))
Time: O(n) Space: O(h) DFS · O(w) BFS (h=height, w=max width)

Problems

226. Invert Binary Treepost-order swapEasy
104. Maximum Depth of Binary Treepost-order DFS heightEasy
543. Diameter of Binary Treemax(left+right) at each node via nonlocalEasy
100. Same Treesimultaneous DFS on two treesEasy
110. Balanced Binary Treebottom-up height, return -1 on imbalanceEasy
102. Binary Tree Level Order TraversalBFS, snapshot level sizeMedium
199. Binary Tree Right Side ViewBFS, take last node per levelMedium
98. Validate Binary Search Treepass (lo, hi) range down recursivelyMedium
235. LCA of a BSTBST property: diverge at LCAMedium
230. Kth Smallest Element in a BSTinorder traversal = sorted orderMedium
1448. Count Good Nodes in Binary Treepre-order DFS with max-so-farMedium
105. Construct Binary Tree from Preorder & Inorderpreorder[0]=root, split inorder around itMedium
208. Implement Trie (Prefix Tree)dict of dicts + end_of_word flagMedium
124. Binary Tree Maximum Path Sumpost-order, track global max, return one-arm maxHard

Graphs

The Pattern

Graph problems are just DFS/BFS with a visited set. Choose BFS for shortest path (unweighted). Use DFS for connectivity, cycle detection, and topological sort. Union-Find (Disjoint Set Union) is the cleanest tool for dynamic connectivity. Dijkstra handles weighted shortest paths.

Reach for this when: you see a 2D grid (islands, flow), a node-edge relationship (courses, dependencies), or need connectivity/path queries. Grids are implicit graphs — (r, c) is a node, 4-directional neighbours are edges.
Template — BFS (shortest path / flood fill)
from collections import deque

def bfs(start, graph):
    visited = {start}
    q = deque([(start, 0)])             # (node, distance)
    while q:
        node, dist = q.popleft()
        for nb in graph[node]:
            if nb not in visited:
                visited.add(nb)
                q.append((nb, dist + 1))
Template — Union-Find (with path compression + union by rank)
parent = list(range(n))
rank   = [0] * n

def find(x):
    if parent[x] != x:
        parent[x] = find(parent[x])    # path compression
    return parent[x]

def union(x, y):
    px, py = find(x), find(y)
    if px == py: return False           # already connected
    if rank[px] < rank[py]: px, py = py, px
    parent[py] = px
    if rank[px] == rank[py]: rank[px] += 1
    return True
Template — Topological Sort (Kahn's BFS)
from collections import deque, defaultdict

indegree = [0] * numCourses
adj = defaultdict(list)
for a, b in prerequisites:
    adj[b].append(a)
    indegree[a] += 1

q = deque([i for i in range(numCourses) if indegree[i] == 0])
order = []
while q:
    node = q.popleft()
    order.append(node)
    for nb in adj[node]:
        indegree[nb] -= 1
        if indegree[nb] == 0:
            q.append(nb)
# cycle exists if len(order) < numCourses
Time: O(V + E) Space: O(V) visited set

Problems

200. Number of IslandsDFS flood fill, mark visited in-placeMedium
695. Max Area of IslandDFS returning accumulated areaMedium
994. Rotting Orangesmulti-source BFS from all rotten orangesMedium
133. Clone GraphDFS + node→clone mapMedium
417. Pacific Atlantic Water Flowreverse BFS from both borders simultaneouslyMedium
207. Course Schedulecycle detection via DFS (3 colours) or Kahn'sMedium
210. Course Schedule IIKahn's topological sortMedium
684. Redundant ConnectionUnion-Find — edge that connects already-joined nodesMedium
323. Number of Connected ComponentsUnion-Find or DFS, count rootsMedium
743. Network Delay TimeDijkstra with min-heapMedium
127. Word LadderBFS on word graph, wildcard neighboursHard

Top K Elements

The Pattern

Whenever a problem asks for the K largest, K most frequent, or K closest items, a heap is almost always the right tool. The key insight: maintain a min-heap of size K while scanning. When the heap exceeds size K, pop the smallest — what remains is the K largest seen so far.

For K smallest, flip to a max-heap (negate values in Python). For "top K frequent", use a bucket sort approach for O(n) time.

Reach for this when: you see "kth largest/smallest", "top K frequent elements", "K closest points". The brute-force is sort-then-slice at O(n log n); a heap gives O(n log k) — better when k ≪ n. Use heapq.nlargest(k, arr) in a pinch, but know the manual approach.
Template — K Largest with Min-Heap
import heapq

def find_kth_largest(nums, k):
    heap = []
    for n in nums:
        heapq.heappush(heap, n)
        if len(heap) > k:
            heapq.heappop(heap)     # discard smallest seen
    return heap[0]                  # smallest of the K largest = kth largest
Template — Top K Frequent (bucket sort, O(n))
from collections import Counter

def top_k_frequent(nums, k):
    count   = Counter(nums)
    buckets = [[] for _ in range(len(nums) + 1)]
    for num, freq in count.items():
        buckets[freq].append(num)
    result = []
    for i in range(len(buckets) - 1, 0, -1):
        result.extend(buckets[i])
        if len(result) >= k:
            return result[:k]
Time: O(n log k) heap · O(n) bucket sort Space: O(k)

Problems

215. Kth Largest Element in an Arraymin-heap size k; or quickselect O(n) avgMedium
347. Top K Frequent ElementsCounter + bucket sort or heapMedium
973. K Closest Points to Originmax-heap of size k keyed by dist²Medium
658. Find K Closest Elementsbinary search for left boundary of k-windowMedium
767. Reorganize Stringmax-heap; always place most-frequent char not equal to lastMedium
358. Rearrange String k Distance Apartmax-heap + cooldown queue of size kHard

Two Heaps

The Pattern

Maintain two heaps that partition a running dataset at its median: a max-heap for the lower half and a min-heap for the upper half. Keep them balanced (size difference ≤ 1). The median is either the top of the larger heap, or the average of both tops.

Each insertion is O(log n). Finding the median is O(1). This beats sorting the array on every query (O(n log n) per query).

Reach for this when: you need a running median as elements arrive, or when a problem partitions a set into two halves where you need the maximum of one half and the minimum of the other simultaneously (scheduling, IPO).
Template — Running Median
import heapq

class MedianFinder:
    def __init__(self):
        self.lo = []    # max-heap (negate values): lower half
        self.hi = []    # min-heap: upper half

    def addNum(self, num):
        heapq.heappush(self.lo, -num)       # push to lower half
        # ensure max(lo) <= min(hi)
        heapq.heappush(self.hi, -heapq.heappop(self.lo))
        # rebalance sizes so |lo| >= |hi|
        if len(self.lo) < len(self.hi):
            heapq.heappush(self.lo, -heapq.heappop(self.hi))

    def findMedian(self):
        if len(self.lo) > len(self.hi):
            return -self.lo[0]
        return (-self.lo[0] + self.hi[0]) / 2
Time: O(log n) add · O(1) median Space: O(n)

Problems

295. Find Median from Data Streammax-heap lower half + min-heap upper half, keep balancedHard
480. Sliding Window Mediantwo heaps + lazy deletion for the evicted elementHard
502. IPOmin-heap by capital to unlock projects; max-heap by profit to pick bestHard
1439. Kth Smallest Sum (Matrix)min-heap over row combinations; pop k timesHard

Dynamic Programming

The Pattern

DP applies when a problem has overlapping subproblems (same inputs recomputed) and optimal substructure (optimal answer to sub-problems → optimal answer to whole). The method: (1) define state clearly, (2) write the recurrence, (3) identify base cases, (4) decide memo vs tabulation.

Common DP shapes: 1-D (Fibonacci-style), 2-D (grid or two-sequence), knapsack (subset-sum), interval DP.

Reach for this when: the problem says "count ways", "min/max cost", "can we achieve X", and the brute-force tries all combinations. If you're writing recursion with repeated sub-problems, add memo — you have DP.
Template — Top-down (memoization)
from functools import lru_cache

@lru_cache(maxsize=None)
def dp(i, ...):                          # define your state
    if i == base: return base_value
    return min(dp(i-1, ...) + cost, ...)  # recurrence
Template — Bottom-up 1-D
dp = [0] * (n + 1)
dp[0], dp[1] = base0, base1
for i in range(2, n + 1):
    dp[i] = dp[i-1] + dp[i-2]           # or your recurrence
return dp[n]
Template — 0/1 Knapsack (iterate capacity backwards!)
dp = [0] * (capacity + 1)
for weight, value in items:
    for w in range(capacity, weight - 1, -1):  # ← reverse prevents reuse
        dp[w] = max(dp[w], dp[w - weight] + value)
Template — 2-D (edit distance / LCS)
dp = [[0] * (len(t)+1) for _ in range(len(s)+1)]
for i in range(1, len(s)+1):
    for j in range(1, len(t)+1):
        if s[i-1] == t[j-1]:
            dp[i][j] = dp[i-1][j-1]
        else:
            dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
Time: O(states × transitions) Space: O(states) — often reducible to O(1 row)

Problems

70. Climbing StairsFibonacci — 2 variablesEasy
746. Min Cost Climbing Stairsdp[i] = cost[i] + min(dp[i-1], dp[i-2])Easy
198. House Robberdp[i] = max(dp[i-1], dp[i-2] + nums[i])Medium
213. House Robber IIrun House Robber on [0..n-2] and [1..n-1], take maxMedium
322. Coin Changeunbounded knapsack: dp[i] = min(dp[i-c]+1)Medium
300. Longest Increasing Subsequencepatience sort (binary search) → O(n log n)Medium
416. Partition Equal Subset Sum0/1 knapsack boolean DP, target = sum//2Medium
62. Unique Pathsdp[i][j] = dp[i-1][j] + dp[i][j-1]Medium
91. Decode Waysdp[i] += dp[i-1] if valid single, dp[i-2] if valid pairMedium
139. Word Breakdp[i] = any(dp[j] and s[j:i] in word_set)Medium
5. Longest Palindromic Substringexpand-around-center, O(n²) time O(1) spaceMedium
55. Jump Gamegreedy or DP: track max reachableMedium
72. Edit Distance2-D DP on two stringsHard
312. Burst Balloonsinterval DP — think "last balloon to burst"Hard

Greedy

The Pattern

At each step, take the locally optimal choice and never look back. Greedy works when you can prove (via exchange argument) that swapping any two adjacent choices doesn't improve the answer. The typical setup: sort by some criteria, then iterate linearly making irrevocable decisions.

Reach for this when: the problem involves scheduling, intervals, or building a result incrementally. If you can show "there's always an optimal solution that makes this greedy choice first", it works.
Template — Interval Scheduling (max non-overlapping)
intervals.sort(key=lambda x: x[1])      # sort by end time
count, end = 0, float('-inf')
for start, finish in intervals:
    if start >= end:                        # no overlap
        count += 1
        end = finish
Template — Jump Game (max reach)
max_reach = 0
for i, jump in enumerate(nums):
    if i > max_reach: return False          # unreachable
    max_reach = max(max_reach, i + jump)
return True
Template — Partition Labels (last occurrence)
last = {c: i for i, c in enumerate(s)}  # rightmost position of each char
start = end = 0
sizes = []
for i, c in enumerate(s):
    end = max(end, last[c])
    if i == end:                            # can cut here
        sizes.append(end - start + 1)
        start = i + 1
Time: O(n log n) sort · O(n) scan Space: O(1)

Problems

55. Jump Gametrack max reachable indexMedium
45. Jump Game IIBFS-style: track current level boundaryMedium
134. Gas Stationif total gas ≥ total cost, a solution exists; start resets on deficitMedium
763. Partition Labelslast-occurrence map, extend window greedilyMedium
846. Hand of Straightssort + Counter, greedily consume from smallestMedium
678. Valid Parenthesis Stringtrack (lo, hi) range for open-count; valid if lo ≥ 0 throughoutMedium
1899. Merge Triplets to Form Target Tripletinclude triplets that don't exceed target in any dimensionMedium

Backtracking

The Pattern

Backtracking is exhaustive search with pruning. You build a candidate solution one element at a time; if at any point the partial candidate cannot lead to a valid solution, you undo the last choice and try the next one. Think of it as a DFS over a decision tree.

The three-step loop: chooseexploreun-choose (backtrack).

Reach for this when: the problem asks for all solutions (permutations, subsets, combinations), constraint satisfaction (N-queens, Sudoku), or when "try every possibility" is the only option but pruning makes it feasible.
Template — Subsets / Combinations
def backtrack(start, path):
    result.append(path[:])             # record every state (subsets)
    for i in range(start, len(nums)):
        if i > start and nums[i] == nums[i-1]:
            continue                    # skip duplicates at same depth
        path.append(nums[i])           # choose
        backtrack(i + 1, path)         # explore (i+1 = no reuse)
        path.pop()                     # un-choose

nums.sort()                            # sort first to group duplicates
result = []
backtrack(0, [])
Template — Permutations
def backtrack(path, used):
    if len(path) == len(nums):
        result.append(path[:])
        return
    for i, num in enumerate(nums):
        if used[i]: continue
        if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continue  # dup check
        used[i] = True
        path.append(num)
        backtrack(path, used)
        path.pop()
        used[i] = False
Time: O(2ⁿ) subsets · O(n!) perms Space: O(n) call stack

Problems

78. Subsetsrecord path at every nodeMedium
90. Subsets IIsort + skip duplicate at same depthMedium
39. Combination Sumreuse allowed: backtrack(i, ...) not (i+1, ...)Medium
40. Combination Sum IIno reuse + skip sibling duplicatesMedium
46. Permutationsused[] boolean arrayMedium
17. Letter Combinations of a Phone Numberdigit→letters map, recurse on digitsMedium
131. Palindrome Partitioningcheck palindrome before branchingMedium
79. Word SearchDFS on grid, mark visited in-placeMedium
51. N-Queenstrack cols, diag1 (r-c), diag2 (r+c) setsHard


Bit Manipulation

The Pattern

Bit tricks give O(1) solutions to problems that otherwise take O(n). The two most useful facts: XOR cancels duplicates (a ^ a = 0, a ^ 0 = a), so XOR-ing an array leaves the unique element. And n & (n−1) clears the lowest set bit, giving you popcount in O(set bits) iterations.

Reach for this when: you need to find a single unpaired number (XOR all), check powers of 2 (n & (n−1) == 0), enumerate subsets (for mask in range(1 << n)), or simulate addition without +.
Cheat-Sheet
# --- Core operations ---
x & y          # AND: bits set in both
x | y          # OR:  bits set in either
x ^ y          # XOR: bits set in exactly one
~x             # NOT: flip all bits
x << k         # left shift  = x * 2^k
x >> k         # right shift = x // 2^k

# --- Key tricks ---
n & (n - 1)    # clear lowest set bit
n & (-n)       # isolate lowest set bit
n & 1          # check if odd
(n >> k) & 1   # check k-th bit

# Find single number (XOR all)
result = 0
for num in nums: result ^= num

# Count set bits (Brian Kernighan)
count = 0
while n: n &= n - 1; count += 1

# Is power of 2?
n > 0 and (n & (n - 1)) == 0

# Enumerate all subsets of n-element set
for mask in range(1 << n):
    subset = [i for i in range(n) if mask >> i & 1]
Time: O(1) per operation Space: O(1)

Problems

136. Single NumberXOR all — pairs cancel to 0Easy
268. Missing NumberXOR indices 0..n with all valuesEasy
191. Number of 1 Bitsn & (n-1) loopEasy
338. Counting Bitsdp: bits[i] = bits[i>>1] + (i&1)Easy
190. Reverse Bitsshift and OR 32 timesEasy
371. Sum of Two IntegersXOR = sum w/o carry; AND<<1 = carry; repeatMedium
201. Bitwise AND of Numbers Rangefind common left prefix (shift right until equal)Medium

System Design

Interview Framework (use this structure every time)
  1. Clarify — functional requirements (what it does) + non-functional (scale, latency, consistency, availability). Never design before you know the scope.
  2. Estimate — QPS, storage GB/TB, bandwidth. Back-of-napkin math shows you're reasoning about scale.
  3. High-Level Design — draw client → LB → servers → DB → cache. Name the components before detailing them.
  4. Deep Dive — data model, key APIs, the one component the interviewer cares about most (usually the hardest one).
  5. Bottlenecks & Trade-offs — SPOF, sharding strategy, caching eviction policy, eventual vs strong consistency. Show you know what you're giving up.
Key rule: drive the conversation. Don't wait to be asked — propose a design, then critique it yourself. Interviewers want to see your thought process, not a memorised answer.
Essential Concepts Reference
# Capacity estimation cheat-sheet
1 million req/day  ≈ 12 req/s
read-heavy system  →  cache aggressively (Redis, CDN)
write-heavy system →  async queue (Kafka), eventual consistency

# Storage sizing
1 tweet (280 chars + metadata) ≈ 1 KB
1 photo                        ≈ 200 KB
1 min HD video                 ≈ 50 MB

# Database choice heuristic
structured + ACID + complex queries → SQL (PostgreSQL)
document / flexible schema         → MongoDB
key-value / cache / sessions        → Redis
time-series data                   → InfluxDB / TimescaleDB
graph relationships                → Neo4j

Key Concepts

  • Horizontal vs Vertical Scaling
  • Load Balancers (Round Robin, Least Connections, Consistent Hash)
  • SQL vs NoSQL trade-offs
  • Caching: LRU eviction, Write-through vs Write-back
  • CDN for static assets, edge caching
  • Message Queues: Kafka (streaming), RabbitMQ (task queue)
  • Database Sharding (by user ID hash) + Replication (primary-replica)
  • CAP Theorem: pick 2 of Consistency / Availability / Partition-tolerance
  • Rate Limiting: Token Bucket (smooth bursts), Leaky Bucket (strict rate)
  • Consistent Hashing (minimal re-sharding when nodes join/leave)
  • Bloom Filter (probabilistic set membership, no false negatives)
  • WebSockets vs Long-Polling vs SSE for real-time

Classic Design Questions

  • URL Shortener (TinyURL) — hashing, redirect, analytics
  • Twitter / News Feed — fanout-on-write vs fanout-on-read
  • Key-Value Store — consistent hashing, quorum reads/writes
  • Web Crawler — politeness, dedup via Bloom Filter, BFS queue
  • YouTube / Video Streaming — CDN, transcoding pipeline, chunking
  • Chat System (WhatsApp) — WebSocket per client, message ordering
  • Notification System — fan-out via queue, push vs pull
  • API Rate Limiter — Token Bucket in Redis, per-user keys
  • Distributed Cache (Redis) — consistent hashing, eviction, replication
  • Search Autocomplete — Trie + top-K heap, offline index build
  • Ride-Sharing (Uber) — geospatial indexing, driver matching, ETA
  • Stock Exchange — order book, matching engine, low-latency queue