一小組 algorithm patterns 會反覆出現:搜索、遍歷 graphs、收縮 windows、caching,以及用 memory 換 time。
- 認出 pattern,比背問題名稱更重要。
- Input 形狀 通常已經點名了工具。
- 下面每個 pattern 都附上精確的 TypeScript 實作與 complexity 說明。
Pattern 地圖
| Pattern | Typical input | Reach for it when |
|---|---|---|
| Hash map | Unsorted array / set | 需要 O(1) lookups 或 frequency counts |
| Two pointers | Sorted array, or pair from ends | Linear scan 可以取代 nested loops |
| Sliding window | Array / string contiguous segment | Subarray 或 substring 約束 |
| Binary search | Sorted array, or monotonic answer | Search space 每一步可以減半 |
| Stack | Nested / matching structure | Latest-open / next-greater 問題 |
| Linked list | Pointer-based sequence | Reverse、cycle、constant-space walks |
| Tree DFS / BFS | Binary / n-ary tree | Depth vs level-order 問題 |
| Graph search | Grid or adjacency list | Connectivity、paths、components |
| Topological sort | Directed acyclic graph | 帶 prerequisites 的排序 |
| Heap | Stream or 「top K」 | 要 extremes 但不想完整 sort |
| Union-Find | Undirected connectivity | Merge components、detect cycles |
| Backtracking | Combinatorial search | 建立所有 valid candidates |
| Dynamic programming | Overlapping subproblems | 可重用的 optimal count / path |
| LRU | Cache with capacity | O(1) get/put 搭配 eviction |
Hash Map:Two Sum
Hash map 把昂貴的 scan 變成 constant-time lookups。
function twoSum(nums: number[], target: number): [number, number] {
const seen = new Map<number, number>() // value -> index
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i]
const j = seen.get(need)
if (j !== undefined) return [j, i]
seen.set(nums[i], i)
}
throw new Error("no pair sums to target")
}
twoSum([2, 7, 11, 15], 9) // [0, 1]- 當你本來會 nest loops 去找 complement、數 frequencies,或記住「是否已經見過?」時,就用它。
- Complexity: O(n) time, O(n) space.
- 同一個想法支撐 anagram checks(character counts 的
Map)以及 first-unique-character 問題。 - Failure: 對本來可以 keyed 的 data 做 nested loops。
Two Pointers:Pair With Target (Sorted)
當 array 已 sorted,兩個 indices 可以向內(或一起向前)移動,而不必用 nested loop 搜索。
function twoSumSorted(nums: number[], target: number): [number, number] {
let left = 0
let right = nums.length - 1
while (left < right) {
const sum = nums[left] + nums[right]
if (sum === target) return [left, right]
if (sum < target) left++
else right--
}
throw new Error("no pair sums to target")
}
twoSumSorted([1, 2, 3, 4, 6], 6) // [1, 3] -> 2 + 4- 移動那個能相對 target 減少 error 的 pointer。
- Complexity: O(n) time, O(1) extra space(假設 array 已經 sorted)。
- Failure: 在 unsorted array 上套 inward pointers——移動規則假定有序。
Sliding Window:Longest Substring Without Repeating Characters
Window 是一段 contiguous segment [left, right],你一邊 expand 一邊 shrink,同時維持一個 invariant。
function lengthOfLongestSubstring(s: string): number {
const lastIndex = new Map<string, number>()
let left = 0
let best = 0
for (let right = 0; right < s.length; right++) {
const ch = s[right]
const prev = lastIndex.get(ch)
if (prev !== undefined && prev >= left) {
left = prev + 1
}
lastIndex.set(ch, right)
best = Math.max(best, right - left + 1)
}
return best
}
lengthOfLongestSubstring("abcabcbb") // 3 ("abc")- 用於 unique characters、sum ≤ K,或最多 K 個 distinct values——grow 與 shrink 一段 range,而不是從頭開始。
- Complexity: O(n) time, O(min(n, alphabet)) space.
- Failure: shrink
left時沒有檢查 last occurrence 是否仍在 window 裡。
Binary Search:Lower Bound
Binary search 反覆把 monotonic search space 切成兩半。
lo → mid → hi
mid:
nums[mid] < target → lo = mid + 1
else → hi = midfunction lowerBound(nums: number[], target: number): number {
let lo = 0
let hi = nums.length // exclusive upper bound
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2)
if (nums[mid] < target) lo = mid + 1
else hi = mid
}
return lo
}
lowerBound([1, 3, 3, 5, 7], 3) // 1
lowerBound([1, 3, 3, 5, 7], 4) // 3 (insert before 5)- 仔細的版本返回第一個滿足
nums[i] >= target的 index(lower bound / insertion point)。 - 當 array 本身未 sorted,但 answer 是 monotonic 的(例如能用的最小 capacity),就對 answer 做 binary search。
- Complexity: O(log n) time, O(1) space.
- Failure: 混用 inclusive 與 exclusive bounds——
hi = nums.length是 exclusive;hi = mid(不是mid - 1)才能保住 invariant。
Stack:Valid Parentheses
Stack 存放「仍未關閉」的工作,並按 LIFO 順序 pop。
function isValidParentheses(s: string): boolean {
const pairs: Record<string, string> = {
")": "(",
"]": "[",
"}": "{",
}
const stack: string[] = []
for (const ch of s) {
if (ch === "(" || ch === "[" || ch === "{") {
stack.push(ch)
continue
}
const expected = pairs[ch]
if (!expected || stack.pop() !== expected) return false
}
return stack.length === 0
}
isValidParentheses("()[]{}") // true
isValidParentheses("(]") // false- Matching brackets、path simplification,以及 monotonic next-greater 問題,都會 push 與 pop。
- Complexity: O(n) time, O(n) space.
- Failure: 因為每個 closer 都匹配了就 return true,卻沒檢查 stack 是否為空——剩下的 openers 仍然無效。
Linked List:Reverse and Cycle Detection
Linked-list 問題通常是小心的 pointer rewiring。
type ListNode = {
val: number
next: ListNode | null
}
function reverseList(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null
let curr = head
while (curr) {
const next = curr.next
curr.next = prev
prev = curr
curr = next
}
return prev
}
function hasCycle(head: ListNode | null): boolean {
let slow = head
let fast = head
while (fast?.next) {
slow = slow!.next
fast = fast.next.next
if (slow === fast) return true
}
return false
}- 兩個經典:in place reverse 整條 list,以及用 Floyd’s tortoise and hare 偵測 cycle。
- Complexity: reverse 是 O(n) time, O(1) space;cycle detection 是 O(n) time, O(1) space.
- Failure: 在保存
next之前就 rewirecurr.next——list 的其餘部分就沒了。
Tree DFS and BFS:Depth and Level Order
Trees 是沒有 cycles 的 graphs。DFS 往深處走;BFS 一層一層走。
type TreeNode = {
val: number
left: TreeNode | null
right: TreeNode | null
}
function maxDepth(root: TreeNode | null): number {
if (!root) return 0
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right))
}
function levelOrder(root: TreeNode | null): number[][] {
if (!root) return []
const result: number[][] = []
const queue: TreeNode[] = [root]
while (queue.length > 0) {
const size = queue.length
const level: number[] = []
for (let i = 0; i < size; i++) {
const node = queue.shift()!
level.push(node.val)
if (node.left) queue.push(node.left)
if (node.right) queue.push(node.right)
}
result.push(level)
}
return result
}Root
├─ Level1
│ ├─ Level2
│ └─ Level2
└─ Level1
└─ Level2- DFS 用 recursion 或顯式 stack。BFS 用 queue——unweighted tree 上 shortest path 的通常選擇。
- BFS 在進入下一層之前走完當前層;DFS 沿一條 branch 走到 leaf 再 backtrack。
- Complexity: 兩者最壞都是 O(n) time 與 O(n) space(skewed tree 或很寬的 level)。
- Failure: 問題是 unweighted tree 上的 shortest path 卻用 DFS——那是 BFS。
Graph:Number of Islands
Grid 是一張 graph:每個 cell 是 node,四方向 neighbors 是 edges。
function numIslands(grid: string[][]): number {
if (grid.length === 0) return 0
const rows = grid.length
const cols = grid[0].length
let count = 0
function sink(r: number, c: number): void {
if (r < 0 || c < 0 || r >= rows || c >= cols) return
if (grid[r][c] !== "1") return
grid[r][c] = "0"
sink(r + 1, c)
sink(r - 1, c)
sink(r, c + 1)
sink(r, c - 1)
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === "1") {
count++
sink(r, c)
}
}
}
return count
}
numIslands([
["1", "1", "0", "0"],
["1", "0", "0", "1"],
["0", "0", "1", "1"],
]) // 3- 用 DFS 或 BFS 做 flood-fill,計算 land 的 connected components。
- Complexity: O(rows × cols) time,最壞 O(rows × cols) space 給 recursion stack。
- Failure: 數 cells 而不是 connected components——flood-fill 必須把整座 island 標成 visited。
Topological Sort:Course Schedule
當 tasks 有 prerequisites 時,把它們建成 directed graph,並產出一個順序,使得每條 edge u → v 都表示 u 在 v 之前。
function canFinish(numCourses: number, prerequisites: number[][]): boolean {
const graph: number[][] = Array.from({ length: numCourses }, () => [])
const indegree = Array.from({ length: numCourses }, () => 0)
for (const [course, pre] of prerequisites) {
graph[pre].push(course)
indegree[course]++
}
const queue: number[] = []
for (let i = 0; i < numCourses; i++) {
if (indegree[i] === 0) queue.push(i)
}
let taken = 0
while (queue.length > 0) {
const course = queue.shift()!
taken++
for (const next of graph[course]) {
indegree[next]--
if (indegree[next] === 0) queue.push(next)
}
}
return taken === numCourses // false means a cycle exists
}
canFinish(2, [[1, 0]]) // true
canFinish(2, [
[1, 0],
[0, 1],
]) // false- Kahn’s algorithm 用 indegrees 與 queue。
- Complexity: O(V + E) time, O(V + E) space.
- Failure: 假定每張 directed graph 都有順序——
taken !== numCourses表示有 cycle。
Heap:Top K Frequent Elements
Heap 高效地保住當前的 extreme。對「top K」來說,一個 size-K 的 frequencies min-heap,可以避免 sort 整張 map。
function topKFrequent(nums: number[], k: number): number[] {
const freq = new Map<number, number>()
for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1)
type Item = { value: number; count: number }
const heap: Item[] = []
const siftUp = (i: number) => {
while (i > 0) {
const parent = Math.floor((i - 1) / 2)
if (heap[parent].count <= heap[i].count) break
;[heap[parent], heap[i]] = [heap[i], heap[parent]]
i = parent
}
}
const siftDown = (i: number) => {
while (true) {
let smallest = i
const left = 2 * i + 1
const right = 2 * i + 2
if (left < heap.length && heap[left].count < heap[smallest].count) {
smallest = left
}
if (right < heap.length && heap[right].count < heap[smallest].count) {
smallest = right
}
if (smallest === i) break
;[heap[smallest], heap[i]] = [heap[i], heap[smallest]]
i = smallest
}
}
for (const [value, count] of freq) {
heap.push({ value, count })
siftUp(heap.length - 1)
if (heap.length > k) {
heap[0] = heap.pop()!
siftDown(0)
}
}
return heap.map((item) => item.value)
}
topKFrequent([1, 1, 1, 2, 2, 3], 2) // [1, 2] (order among ties may vary)- 在 stream 或「top K」上,當你需要 extremes 卻不想完整 sort 時,就用它。
- Complexity: O(n log k) time,frequency map 佔 O(n) space(heap 最多持有 k 個 items)。
- Failure: 當 size-K heap 就夠時,卻去 sort 整張 frequency map。
Union-Find:Connected Components
Union-Find(Disjoint Set Union)維護一份把 elements 分成 components 的 partition。
class UnionFind {
private parent: number[]
private rank: number[]
components: number
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i)
this.rank = Array.from({ length: n }, () => 0)
this.components = n
}
find(x: number): number {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]) // path compression
}
return this.parent[x]
}
union(a: number, b: number): boolean {
const ra = this.find(a)
const rb = this.find(b)
if (ra === rb) return false // already connected — edge is redundant
if (this.rank[ra] < this.rank[rb]) this.parent[ra] = rb
else if (this.rank[ra] > this.rank[rb]) this.parent[rb] = ra
else {
this.parent[rb] = ra
this.rank[ra]++
}
this.components--
return true
}
}
function countComponents(n: number, edges: number[][]): number {
const uf = new UnionFind(n)
for (const [a, b] of edges) uf.union(a, b)
return uf.components
}
countComponents(5, [
[0, 1],
[1, 2],
[3, 4],
]) // 2find返回 representative;union合併兩個 sets。- Path compression + union by rank 讓 operations 接近 amortized O(1)。
- Complexity: n 個 nodes 與 m 條 edges 實際上是 O(n + m α(n))——α 是 inverse Ackermann function(實務上很小)。
- Failure: union 之前沒有先
find——你合併的是 nodes 而不是它們的 roots,partition 就破了。
Backtracking:Subsets
Backtracking 增量建立 candidates,並在探索下一條 branch 時撤銷上一次選擇。
function subsets(nums: number[]): number[][] {
const result: number[][] = []
const path: number[] = []
function dfs(start: number): void {
result.push([...path])
for (let i = start; i < nums.length; i++) {
path.push(nums[i])
dfs(i + 1)
path.pop()
}
}
dfs(0)
return result
}
subsets([1, 2, 3])
// [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]- 用於 subsets、permutations、combinations,以及 constraint search。
- Complexity: 產生並 copy 所有 subsets 是 O(n · 2ⁿ) time,recursion path 額外 O(n) space(不含 output)。
- Failure: 忘了
path.pop()——後面的 branches 會共享一份被 mutate 的 path。
Dynamic Programming:Climbing Stairs and Unique Paths
當問題有 optimal substructure 與 overlapping subproblems 時,用 dynamic programming:較小的 pieces 只解一次,再重用。
- 從 base cases 出發往上建。
- Failure: 子問題重疊時卻 recurse 而不 reuse——那是 exponential,不是 DP。
1D:climbing stairs
你可以爬 1 或 2 步。到達 n 的 ways = 到達 n - 1 的 ways + 到達 n - 2 的 ways。
function climbStairs(n: number): number {
if (n <= 2) return n
let prev2 = 1
let prev1 = 2
for (let i = 3; i <= n; i++) {
const curr = prev1 + prev2
prev2 = prev1
prev1 = curr
}
return prev1
}
climbStairs(5) // 8- Complexity: O(n) time, O(1) space.
2D:unique paths
在 m × n grid 上,你只能向右或向下。到 (r, c) 的 paths = 從上方來的 paths + 從左方來的 paths。
function uniquePaths(m: number, n: number): number {
const dp = Array.from({ length: m }, () => Array.from({ length: n }, () => 1))
for (let r = 1; r < m; r++) {
for (let c = 1; c < n; c++) {
dp[r][c] = dp[r - 1][c] + dp[r][c - 1]
}
}
return dp[m - 1][n - 1]
}
uniquePaths(3, 7) // 28- Complexity: O(m · n) time, O(m · n) space(用 rolling row 可壓到 O(n))。
LRU Cache
LRU cache 以 O(1) 返回 values,滿了就驅逐 least recently used key。
class LRUCache {
private readonly capacity: number
private readonly map = new Map<number, number>()
constructor(capacity: number) {
this.capacity = capacity
}
get(key: number): number {
if (!this.map.has(key)) return -1
const value = this.map.get(key)!
this.map.delete(key)
this.map.set(key, value) // move to most-recently used
return value
}
put(key: number, value: number): void {
if (this.map.has(key)) this.map.delete(key)
this.map.set(key, value)
if (this.map.size > this.capacity) {
const oldest = this.map.keys().next().value as number
this.map.delete(oldest)
}
}
}
const cache = new LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
cache.get(1) // 1
cache.put(3, 3) // evicts key 2
cache.get(2) // -1- 在現代 JavaScript 裡,
Map保留 insertion order,所以每次 access 把 key 移到末尾,就能實作 LRU,而不必手寫 doubly linked list。 - Complexity: amortized O(1) get 與 put;O(capacity) space.
- Failure:
get只讀卻沒有delete+set——insertion order 就不再追蹤 recency。
選擇 Pattern
選 invariant 最清楚的那個 pattern,然後在寫 code 之前先說出 time 與 space。
- 需要 fast lookup 或 frequency count? Hash map。
- Array 已 sorted,在找 boundary 或 pair? Binary search 或 two pointers。
- 帶約束的 contiguous subarray / substring? Sliding window。
- Matching 或 nested structure / next greater? Stack。
- Unweighted graph 的 shortest path 或 level-order tree? BFS。
- 探索所有 paths、components 或 hierarchies? DFS。
- DAG 上的 prerequisites / ordering? Topological sort。
- 大量 merges 的 connectivity? Union-Find。
- Top K 或 running extreme? Heap。
- Overlapping subproblems 加上 optimal reuse? Dynamic programming。
- 在約束下產生所有 candidates? Backtracking。
- 帶 recency 的 bounded cache? LRU。
要點
先掌握這些 patterns,再去追 exotic algorithms。大多數日常問題都是 hash maps、windows、binary search、graph traversal、heaps 與 DP 的重組。
- 對每一個解,要能說出 pattern、time 與 space complexity,以及如果 input 是 sorted、streaming,或大到放不進 memory,會怎麼變。
- 專門工具(KMP、Bloom filters、consistent hashing)在更窄的領域才重要。上面這些 patterns 才是共享的 baseline。
- Failure: 優化 nested loops,而真正的瓶頸是從未被衡量過的 network round trip。Complexity 是決策工具,不是競技。