DSA Patterns

The 15 patterns that cover the large majority of software engineering interview problems. Recognize the pattern from the problem signals, then fit the template to the specifics.

How to use this page

Do not read top-to-bottom. Pick a recent problem you solved, identify which pattern it matched, and check that your solution looks like the template here. Gaps between your code and the template are your study list.

Array & String Traversal

Two Pointers

Time O(n)  ·  Space O(1)

Signals in the problem

  • Input is sorted or can be sorted cheaply
  • Looking for a pair or triple with a property (sum, difference)
  • "Remove duplicates in-place" / "partition" / "reverse" on an array or string

When to use

Two indices walking the array — either from opposite ends (sorted pair-sum) or in the same direction (partition / dedupe). Replaces a nested loop with a single pass.

Template

Python

# Opposite-ends: pair sum in a sorted array.
def two_sum_sorted(nums: list[int], target: int) -> tuple[int, int] | None:
lo, hi = 0, len(nums) - 1
while lo < hi:
s = nums[lo] + nums[hi]
if s == target:
return (lo, hi)
if s < target:
lo += 1
else:
hi -= 1
return None

TypeScript

// Opposite-ends: pair sum in a sorted array.
function twoSumSorted(nums: number[], target: number): [number, number] | null {
let lo = 0, hi = nums.length - 1;
while (lo < hi) {
const s = nums[lo] + nums[hi];
if (s === target) return [lo, hi];
if (s < target) lo++;
else hi--;
}
return null;
}

Pitfalls

  • Using on an unsorted array without justification — the invariant collapses
  • Off-by-one when moving both pointers on equality: decide up front which direction to advance

Canonical problems

  • Two Sum II — sorted array  (LC 167)
  • 3Sum  (LC 15)
  • Valid Palindrome  (LC 125)
  • Remove Duplicates from Sorted Array  (LC 26)

Sliding Window

Time O(n)  ·  Space O(k) — usually the window alphabet

Signals in the problem

  • "Contiguous subarray / substring with property X"
  • "Longest / shortest window such that …"
  • "At most K distinct" / "sum ≥ target" / "contains all of …"

When to use

Maintain a window [left, right] and an invariant about its contents. Expand right each step; shrink left while the invariant breaks.

Template

Python

# Variable-size window: longest substring with at most K distinct chars.
from collections import defaultdict
def longest_k_distinct(s: str, k: int) -> int:
counts: dict[str, int] = defaultdict(int)
left = best = 0
for right, ch in enumerate(s):
counts[ch] += 1
while len(counts) > k:
counts[s[left]] -= 1
if counts[s[left]] == 0:
del counts[s[left]]
left += 1
best = max(best, right - left + 1)
return best

TypeScript

// Variable-size window: longest substring with at most K distinct chars.
function longestKDistinct(s: string, k: number): number {
const counts = new Map<string, number>();
let left = 0, best = 0;
for (let right = 0; right < s.length; right++) {
const ch = s[right];
counts.set(ch, (counts.get(ch) ?? 0) + 1);
while (counts.size > k) {
const lc = s[left];
counts.set(lc, counts.get(lc)! - 1);
if (counts.get(lc) === 0) counts.delete(lc);
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}

Pitfalls

  • Forgetting to remove the left element from the auxiliary counter when shrinking
  • Confusing fixed-size windows (always width K) with variable-size (width varies by invariant)

Canonical problems

  • Longest Substring Without Repeating Characters  (LC 3)
  • Minimum Window Substring  (LC 76)
  • Longest Substring with At Most K Distinct Characters  (LC 340)
  • Maximum Sum Subarray of Size K  — fixed window

Fast / Slow Pointers

Time O(n)  ·  Space O(1)

Signals in the problem

  • Linked-list cycle detection
  • "Find the middle / Nth-from-end" of a linked list in one pass
  • Happy-number / sequence-cycle problems on functional graphs

When to use

Two pointers on the same structure moving at different speeds. The fast one laps the slow one iff there is a cycle; when the fast one reaches the end the slow one is at the midpoint.

Template

Python

# Floyd's cycle detection on a linked list.
class Node:
def __init__(self, val: int, nxt: 'Node | None' = None):
self.val, self.next = val, nxt
def has_cycle(head: Node | None) -> bool:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False

TypeScript

// Floyd's cycle detection on a linked list.
class Node {
constructor(public val: number, public next: Node | null = null) {}
}
function hasCycle(head: Node | null): boolean {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow!.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}

Pitfalls

  • Checking `fast` without also checking `fast.next` — null-deref on odd-length lists
  • Claiming a cycle exists when what you actually detected is a meet point inside one

Canonical problems

  • Linked List Cycle  (LC 141)
  • Linked List Cycle II — find cycle start  (LC 142)
  • Middle of the Linked List  (LC 876)
  • Happy Number  (LC 202)

Divide & Conquer

Graph Traversal

Breadth-First Search

Time O(V + E)  ·  Space O(V)

Signals in the problem

  • Shortest path in an unweighted graph or grid
  • "Minimum number of steps / transformations"
  • Level-order traversal on a tree

When to use

Explore the graph layer by layer using a queue. First time you reach the target, you have the shortest path in terms of edges.

Template

Python

# Shortest path in an unweighted graph (adjacency list).
from collections import deque
def shortest_path(graph: dict[int, list[int]], src: int, dst: int) -> int:
if src == dst:
return 0
seen = {src}
q: deque[tuple[int, int]] = deque([(src, 0)])
while q:
node, dist = q.popleft()
for nxt in graph.get(node, []):
if nxt == dst:
return dist + 1
if nxt not in seen:
seen.add(nxt)
q.append((nxt, dist + 1))
return -1

TypeScript

// Shortest path in an unweighted graph (adjacency list).
function shortestPath(graph: Map<number, number[]>, src: number, dst: number): number {
if (src === dst) return 0;
const seen = new Set<number>([src]);
const q: [number, number][] = [[src, 0]];
let head = 0;
while (head < q.length) {
const [node, dist] = q[head++];
for (const nxt of graph.get(node) ?? []) {
if (nxt === dst) return dist + 1;
if (!seen.has(nxt)) {
seen.add(nxt);
q.push([nxt, dist + 1]);
}
}
}
return -1;
}

Pitfalls

  • Forgetting to mark nodes as seen at enqueue time — same node goes on the queue multiple times and memory explodes
  • Using on weighted graphs — BFS solves unweighted shortest path only. Weighted → Dijkstra

Canonical problems

  • Binary Tree Level Order Traversal  (LC 102)
  • Word Ladder  (LC 127)
  • Rotting Oranges  (LC 994)
  • Shortest Path in Binary Matrix  (LC 1091)

Depth-First Search

Time O(V + E)  ·  Space O(V) — recursion stack or explicit stack

Signals in the problem

  • Tree / graph traversal where depth matters or you need full exploration
  • "Count connected components" / "is there a path from A to B?"
  • Tree recursion (subtree properties, path sums)

When to use

Explore as deep as possible before backtracking. Recursive form is concise but watch the call-stack depth; iterative with an explicit stack is safer for large inputs.

Template

Python

# DFS on a graph with visited set — iterative with explicit stack.
def dfs(graph: dict[int, list[int]], start: int) -> list[int]:
seen, order, stack = {start}, [], [start]
while stack:
node = stack.pop()
order.append(node)
for nxt in graph.get(node, []):
if nxt not in seen:
seen.add(nxt)
stack.append(nxt)
return order

TypeScript

// DFS on a graph with visited set — iterative with explicit stack.
function dfs(graph: Map<number, number[]>, start: number): number[] {
const seen = new Set<number>([start]);
const order: number[] = [];
const stack: number[] = [start];
while (stack.length) {
const node = stack.pop()!;
order.push(node);
for (const nxt of graph.get(node) ?? []) {
if (!seen.has(nxt)) {
seen.add(nxt);
stack.push(nxt);
}
}
}
return order;
}

Pitfalls

  • Stack overflow on deep recursive DFS — switch to iterative or raise the recursion limit explicitly
  • Mutating the visited set across unrelated branches in recursion — pass copies or restore state

Canonical problems

  • Number of Islands  (LC 200)
  • Clone Graph  (LC 133)
  • Path Sum  (LC 112)
  • Validate Binary Search Tree  (LC 98)

Topological Sort

Time O(V + E)  ·  Space O(V + E)

Signals in the problem

  • Tasks with dependencies / prerequisite relationships
  • "Is this schedule possible?" / "Order tasks so each runs after its deps"
  • Build systems, course schedules, package managers

When to use

DAG only — a cycle makes a topological order impossible. Kahn's BFS variant also detects cycles (returned order shorter than N).

Template

Python

# Kahn's algorithm: topological order of a DAG. Returns [] if a cycle exists.
from collections import deque
def topo_sort(n: int, edges: list[tuple[int, int]]) -> list[int]:
graph: dict[int, list[int]] = {i: [] for i in range(n)}
indeg = [0] * n
for u, v in edges:
graph[u].append(v)
indeg[v] += 1
q = deque(i for i in range(n) if indeg[i] == 0)
order: list[int] = []
while q:
node = q.popleft()
order.append(node)
for nxt in graph[node]:
indeg[nxt] -= 1
if indeg[nxt] == 0:
q.append(nxt)
return order if len(order) == n else []

TypeScript

// Kahn's algorithm: topological order of a DAG. Returns [] if a cycle exists.
function topoSort(n: number, edges: [number, number][]): number[] {
const graph: number[][] = Array.from({ length: n }, () => []);
const indeg = new Array(n).fill(0);
for (const [u, v] of edges) {
graph[u].push(v);
indeg[v]++;
}
const q: number[] = [];
for (let i = 0; i < n; i++) if (indeg[i] === 0) q.push(i);
const order: number[] = [];
let head = 0;
while (head < q.length) {
const node = q[head++];
order.push(node);
for (const nxt of graph[node]) {
if (--indeg[nxt] === 0) q.push(nxt);
}
}
return order.length === n ? order : [];
}

Pitfalls

  • Running on a graph that might not be a DAG without checking — infinite loop or wrong answer
  • Using DFS variant and forgetting to reverse the post-order

Canonical problems

  • Course Schedule  (LC 207)
  • Course Schedule II  (LC 210)
  • Alien Dictionary  (LC 269)
  • Minimum Height Trees  (LC 310)

Union-Find (DSU)

Time O(α(n)) per op — effectively constant  ·  Space O(n)

Signals in the problem

  • Dynamic connectivity — "are A and B in the same group?"
  • Count connected components under incremental unions
  • Kruskal's MST, cycle detection in an undirected graph

When to use

Each element points to a parent; `find` returns the root (with path compression); `union` merges two trees (by rank). Near-O(1) per op amortized with both optimizations.

Template

Python

# Union-Find with path compression and union by rank.
class UnionFind:
def __init__(self, n: int):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x: int) -> int:
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # path compression
x = self.parent[x]
return x
def union(self, a: int, b: int) -> bool:
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False
if self.rank[ra] < self.rank[rb]:
ra, rb = rb, ra
self.parent[rb] = ra
if self.rank[ra] == self.rank[rb]:
self.rank[ra] += 1
return True

TypeScript

// Union-Find with path compression and union by rank.
class UnionFind {
parent: number[];
rank: number[];
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.rank = new Array(n).fill(0);
}
find(x: number): number {
while (this.parent[x] !== x) {
this.parent[x] = this.parent[this.parent[x]]; // path compression
x = this.parent[x];
}
return x;
}
union(a: number, b: number): boolean {
let ra = this.find(a), rb = this.find(b);
if (ra === rb) return false;
if (this.rank[ra] < this.rank[rb]) [ra, rb] = [rb, ra];
this.parent[rb] = ra;
if (this.rank[ra] === this.rank[rb]) this.rank[ra]++;
return true;
}
}

Pitfalls

  • Omitting path compression — single operations degrade to O(log n), worst-case O(n)
  • Reading from `parent[x]` directly instead of calling `find(x)` — gives a stale root

Canonical problems

  • Number of Provinces  (LC 547)
  • Graph Valid Tree  (LC 261)
  • Accounts Merge  (LC 721)
  • Redundant Connection  (LC 684)

Search

Backtracking

Time Exponential — O(b^d) where b = branching, d = depth  ·  Space O(d) recursion depth

Signals in the problem

  • "Generate all combinations / permutations / subsets"
  • "Find all valid configurations" (N-Queens, sudoku, word break)
  • Constraint satisfaction over a small search space

When to use

Enumerate candidates via a recursive choose → explore → unchoose pattern. Prune aggressively; the base is usually "candidate is complete" or "candidate violates a constraint".

Template

Python

# Generate all subsets via choose / explore / unchoose.
def subsets(nums: list[int]) -> list[list[int]]:
out: list[list[int]] = []
path: list[int] = []
def backtrack(start: int) -> None:
out.append(path.copy())
for i in range(start, len(nums)):
path.append(nums[i]) # choose
backtrack(i + 1) # explore
path.pop() # unchoose
backtrack(0)
return out

TypeScript

// Generate all subsets via choose / explore / unchoose.
function subsets(nums: number[]): number[][] {
const out: number[][] = [];
const path: number[] = [];
const backtrack = (start: number): void => {
out.push([...path]);
for (let i = start; i < nums.length; i++) {
path.push(nums[i]); // choose
backtrack(i + 1); // explore
path.pop(); // unchoose
}
};
backtrack(0);
return out;
}

Pitfalls

  • Appending the mutable path to the results list instead of a copy — all outputs end up equal
  • Missing the unchoose step — state leaks into the next branch
  • No pruning — the search explodes on inputs where pruning would have been cheap

Canonical problems

  • Subsets  (LC 78)
  • Permutations  (LC 46)
  • Combination Sum  (LC 39)
  • N-Queens  (LC 51)

Stack & Heap

Monotonic Stack

Time O(n) — amortized; each index pushed and popped at most once  ·  Space O(n)

Signals in the problem

  • "Next greater / next smaller element"
  • "Largest rectangle in histogram"
  • Problems about spans, temperatures, or visibility

When to use

Maintain a stack whose values are strictly increasing or decreasing. Pop while the incoming value breaks the invariant — the popped elements have found their answer.

Template

Python

# Next greater element for each index (-1 if none).
def next_greater(nums: list[int]) -> list[int]:
out = [-1] * len(nums)
stack: list[int] = [] # indices with strictly-decreasing values
for i, x in enumerate(nums):
while stack and nums[stack[-1]] < x:
out[stack.pop()] = x
stack.append(i)
return out

TypeScript

// Next greater element for each index (-1 if none).
function nextGreater(nums: number[]): number[] {
const out = new Array(nums.length).fill(-1);
const stack: number[] = []; // indices with strictly-decreasing values
for (let i = 0; i < nums.length; i++) {
while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
out[stack.pop()!] = nums[i];
}
stack.push(i);
}
return out;
}

Pitfalls

  • Storing values instead of indices when indices are what you need for span / distance questions
  • Using `<` vs `<=` incorrectly — strict vs non-strict changes the behavior on duplicates

Canonical problems

  • Daily Temperatures  (LC 739)
  • Next Greater Element I  (LC 496)
  • Largest Rectangle in Histogram  (LC 84)
  • Trapping Rain Water  (LC 42)

Top-K / Heap

Time O(n log k)  ·  Space O(k)

Signals in the problem

  • "K largest / smallest / most frequent"
  • Streaming median
  • "Merge K sorted lists"

When to use

Min-heap of size K for K largest (invariant: heap holds the K biggest seen so far). Two heaps for a streaming median (max-heap of lower half, min-heap of upper half).

Template

Python

# K largest elements — min-heap of size K.
import heapq
def top_k(nums: list[int], k: int) -> list[int]:
heap: list[int] = []
for x in nums:
if len(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x)
return heap # K smallest-of-the-top, unordered

TypeScript

// K largest elements via an array-backed min-heap — O(n log k).
class MinHeap {
h: number[] = [];
size() { return this.h.length; }
peek() { return this.h[0]; }
push(x: number) {
this.h.push(x);
for (let i = this.h.length - 1; i > 0; ) {
const p = (i - 1) >> 1;
if (this.h[p] <= this.h[i]) break;
[this.h[p], this.h[i]] = [this.h[i], this.h[p]];
i = p;
}
}
pop(): number {
const top = this.h[0];
const last = this.h.pop()!;
if (this.h.length) {
this.h[0] = last;
for (let i = 0; ; ) {
const l = 2 * i + 1, r = l + 1;
let m = i;
if (l < this.h.length && this.h[l] < this.h[m]) m = l;
if (r < this.h.length && this.h[r] < this.h[m]) m = r;
if (m === i) break;
[this.h[i], this.h[m]] = [this.h[m], this.h[i]];
i = m;
}
}
return top;
}
}
function topK(nums: number[], k: number): number[] {
const heap = new MinHeap();
for (const x of nums) {
if (heap.size() < k) heap.push(x);
else if (x > heap.peek()) { heap.pop(); heap.push(x); }
}
return heap.h; // K largest, unordered
}

Pitfalls

  • Using a full sort when a size-K heap would be O(n log k) — pointing this out signals seniority
  • Forgetting that Python's `heapq` is a min-heap — invert signs for a max-heap

Canonical problems

  • Top K Frequent Elements  (LC 347)
  • Kth Largest Element in an Array  (LC 215)
  • Merge k Sorted Lists  (LC 23)
  • Find Median from Data Stream  (LC 295)

Sort & Sweep

Interval Merge / Sweep

Time O(n log n) — dominated by the sort  ·  Space O(n)

Signals in the problem

  • Inputs are ranges / intervals / events
  • "Merge overlapping"
  • "Minimum rooms / CPUs / resources"

When to use

Sort by start time, then iterate with a running accumulator. For resource-count problems, process start and end events separately as a two-pointer sweep.

Template

Python

# Merge overlapping intervals.
def merge(intervals: list[list[int]]) -> list[list[int]]:
intervals.sort(key=lambda iv: iv[0])
merged: list[list[int]] = []
for start, end in intervals:
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged

TypeScript

// Merge overlapping intervals.
function merge(intervals: number[][]): number[][] {
intervals.sort((a, b) => a[0] - b[0]);
const merged: number[][] = [];
for (const [start, end] of intervals) {
const last = merged[merged.length - 1];
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
else merged.push([start, end]);
}
return merged;
}

Pitfalls

  • Forgetting to sort first — correctness collapses
  • Treating touching intervals [1, 2] and [2, 3] as non-overlapping when the problem wants them merged

Canonical problems

  • Merge Intervals  (LC 56)
  • Insert Interval  (LC 57)
  • Meeting Rooms II  (LC 253)
  • Non-overlapping Intervals  (LC 435)

Greedy

Time O(n log n) — usually dominated by a sort  ·  Space O(1)

Signals in the problem

  • You can justify a local choice that is always safe
  • Sorting the input unlocks an obvious rule (earliest deadline, smallest cost)
  • Interval scheduling / activity selection

When to use

Prove an exchange argument: swapping a non-greedy choice for the greedy one never makes the solution worse. Without the proof, greedy is a guess — mention you would verify with small cases.

Template

Python

# Meeting-room count: the classic greedy-over-sorted-events pattern.
def min_meeting_rooms(intervals: list[list[int]]) -> int:
starts = sorted(iv[0] for iv in intervals)
ends = sorted(iv[1] for iv in intervals)
rooms = max_rooms = 0
j = 0
for s in starts:
if s < ends[j]:
rooms += 1
max_rooms = max(max_rooms, rooms)
else:
j += 1
return max_rooms

TypeScript

// Meeting-room count: the classic greedy-over-sorted-events pattern.
function minMeetingRooms(intervals: number[][]): number {
const starts = intervals.map((iv) => iv[0]).sort((a, b) => a - b);
const ends = intervals.map((iv) => iv[1]).sort((a, b) => a - b);
let rooms = 0, maxRooms = 0, j = 0;
for (const s of starts) {
if (s < ends[j]) {
rooms++;
maxRooms = Math.max(maxRooms, rooms);
} else {
j++;
}
}
return maxRooms;
}

Pitfalls

  • Applying greedy where DP is required — counterexample exists but is not obvious on small inputs
  • Sorting by the wrong key (end time vs start time) — interval scheduling needs end-time sort

Canonical problems

  • Assign Cookies  (LC 455)
  • Jump Game  (LC 55)
  • Gas Station  (LC 134)
  • Task Scheduler  (LC 621)

Dynamic Programming

Dynamic Programming

Time O(n · W) for knapsack-shape; O(n²) for LIS/LCS  ·  Space O(W) rolling array

Signals in the problem

  • "Count the number of ways" / "Minimum cost to ..." / "Longest ..."
  • Optimal substructure + overlapping subproblems
  • Recursion with repeated subtrees

When to use

Identify the state (inputs that uniquely determine the subproblem), then the transition. Two canonical shapes: 0/1 knapsack (rolling 1-D array, iterate reverse) and LIS/LCS (2-D table or patience-sort trick).

Template

Python

# 0/1 knapsack with a 1-D rolling array.
# Key trick: iterate capacity in REVERSE so each item is used at most once.
def knapsack(weights: list[int], values: list[int], capacity: int) -> int:
dp = [0] * (capacity + 1)
for w, v in zip(weights, values):
for c in range(capacity, w - 1, -1): # reverse!
dp[c] = max(dp[c], dp[c - w] + v)
return dp[capacity]

TypeScript

// 0/1 knapsack with a 1-D rolling array.
// Key trick: iterate capacity in REVERSE so each item is used at most once.
function knapsack(weights: number[], values: number[], capacity: number): number {
const dp = new Array(capacity + 1).fill(0);
for (let i = 0; i < weights.length; i++) {
const w = weights[i], v = values[i];
for (let c = capacity; c >= w; c--) { // reverse!
dp[c] = Math.max(dp[c], dp[c - w] + v);
}
}
return dp[capacity];
}

Pitfalls

  • Forward iteration in 0/1 knapsack — an item gets picked multiple times (becomes unbounded knapsack)
  • Getting the base case wrong — dp[0] for "at most 0 capacity" is not the same as dp[0] for "exactly 0"
  • Top-down memoization without hashing all state variables — wrong answers from cache collisions

Canonical problems

  • Climbing Stairs  (LC 70)
  • Coin Change  (LC 322)
  • Longest Increasing Subsequence  (LC 300)
  • Partition Equal Subset Sum  (LC 416)  — 0/1 knapsack
  • Edit Distance  (LC 72)  — LCS-shape

Bit Manipulation

Bit Manipulation

Time O(n) or O(n · 2ⁿ) for subset DP  ·  Space O(1) or O(2ⁿ)

Signals in the problem

  • "Every element appears twice except one" — XOR
  • Subset enumeration via bitmask (n ≤ ~20)
  • Packing / unpacking small integers into a single machine word

When to use

When the state fits in a word and the problem has a natural XOR or bitmask structure. Bit tricks are rare in general SWE interviews but appear in systems-leaning rounds.

Template

Python

# Essential bit idioms.
def count_bits(x: int) -> int: # popcount
c = 0
while x:
x &= x - 1 # clears lowest set bit
c += 1
return c
def lowest_bit(x: int) -> int: # isolate lowest set bit
return x & -x
def single_number(nums: list[int]) -> int: # every value appears twice except one
out = 0
for x in nums:
out ^= x
return out

TypeScript

// Essential bit idioms.
function countBits(x: number): number { // popcount
let c = 0;
while (x) { x &= x - 1; c++; } // clears lowest set bit
return c;
}
function lowestBit(x: number): number { // isolate lowest set bit
return x & -x;
}
function singleNumber(nums: number[]): number { // every value appears twice except one
let out = 0;
for (const x of nums) out ^= x;
return out;
}

Pitfalls

  • Mixing signed and unsigned arithmetic — right shift differs (arithmetic vs logical)
  • Forgetting operator precedence — `(x & mask) == y` needs parens in C-family languages

Canonical problems

  • Single Number  (LC 136)
  • Number of 1 Bits  (LC 191)
  • Maximum XOR of Two Numbers in an Array  (LC 421)
  • Counting Bits  (LC 338)