Algorithm Interview Capability Map & Assessment
基于本次完整算法摸底整理。目标不是记录“做过哪些题”,而是记录当前真实能力、暴露的问题、需要形成的模板,以及 Live Coding 前的复习优先级。
1. 总体结论
Level 当前能力 Topics
A 面试中基本可以直接做 HashMap、Heap / Priority Queue、Binary Search、Linked List
A-/B+ 会做,少量细节可能出错 LRU Cache、Two Pointers、Sliding Window、Prefix Sum、Intervals
B 理解核心,需要形成稳定模板 Monotonic Stack、Basic DP、Tree BFS/DFS、Graph BFS/DFS、Rate Limiter
C Pattern recognition / Topological Sort、0/1 implementation 不稳定 Knapsack、Backtracking、Greedy
D 基本没系统学过 Union Find
当前最高 ROI
Live Coding 前优先:
- Graph
- Tree
- Backtracking
- 0/1 Knapsack
- Rate Limiter / LRU implementation
HashMap、Heap、Binary Search、Linked List 不需要再投入大量时间刷基础题。
2. HashMap — A
明显强项。看到 frequency、lookup、duplicate、mapping、Two Sum、character count,基本可以自然想到 HashMap。
复杂度:
lookup: expected O(1)
insert: expected O(1)
delete: expected O(1)
结论:不需要重点复习。
3. Heap / Priority Queue — A
Pattern recognition 比较稳定。看到 Top K、K-th largest/smallest、continuously get min/max、merge K sorted lists,应优先想到 Heap。
关键词 → Heap
Top K / K-th largest / K-th smallest → Min Heap (size K)
Continuously get min/max → Min/Max Heap
Merge K sorted lists/arrays → Min Heap
Median of stream → Two Heaps (max + min)
Meeting Rooms / Intervals + overlap → Min Heap (by end time)
Go container/heap 完整实现
Go 的 heap 需要实现 heap.Interface(5 个方法)。你写的 Push/Pop 操作 slice 尾部,heap 包内部负责 sift up/down。
import "container/heap"
// --- Min Heap ---
type MinHeap []int
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] } // < 就是 Min Heap
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
// --- Max Heap ---
// 只改 Less:> 就是 Max Heap
type MaxHeap []int
func (h MaxHeap) Len() int { return len(h) }
func (h MaxHeap) Less(i, j int) bool { return h[i] > h[j] } // > 就是 Max Heap
func (h MaxHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MaxHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *MaxHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
重点记忆:
| Min Heap | Max Heap | |
|---|---|---|
Less(i,j) |
h[i] < h[j] |
h[i] > h[j] |
heap.Pop() 返回 |
最小值 | 最大值 |
| Top K largest | 维护 size K 的 Min Heap | ✗ |
| Top K smallest | ✗ | 维护 size K 的 Max Heap |
为什么 Top K largest 用 Min Heap? 因为 Pop 掉最小的,留下的 K 个就是最大的。
使用示例:Top K Frequent Elements
func topKFrequent(nums []int, k int) []int {
freq := map[int]int{}
for _, n := range nums {
freq[n]++
}
h := &MinHeap{}
heap.Init(h)
for num, count := range freq {
heap.Push(h, [2]int{count, num})
if h.Len() > k {
heap.Pop(h)
}
}
result := make([]int, k)
for i := k - 1; i >= 0; i-- {
result[i] = heap.Pop(h).([2]int)[1]
}
return result
}
使用示例:Merge K Sorted Lists
type ListNodeHeap []*ListNode
func (h ListNodeHeap) Len() int { return len(h) }
func (h ListNodeHeap) Less(i, j int) bool { return h[i].Val < h[j].Val }
func (h ListNodeHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *ListNodeHeap) Push(x any) { *h = append(*h, x.(*ListNode)) }
func (h *ListNodeHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
func mergeKLists(lists []*ListNode) *ListNode {
h := &ListNodeHeap{}
heap.Init(h)
for _, l := range lists {
if l != nil {
heap.Push(h, l)
}
}
dummy := &ListNode{}
curr := dummy
for h.Len() > 0 {
node := heap.Pop(h).(*ListNode)
curr.Next = node
curr = curr.Next
if node.Next != nil {
heap.Push(h, node.Next)
}
}
return dummy.Next
}
常见错误
✗ 直接调用 h.Push() / h.Pop() → 必须调用 heap.Push(h, x) / heap.Pop(h)
✗ Pop 里删 index 0 → 必须删最后一个元素(heap 包会先 swap 到尾部)
✗ 忘记 heap.Init(h) → 如果初始就有元素,必须先 Init
复杂度
heap.Init: O(n)
heap.Push: O(log n)
heap.Pop: O(log n)
Top K: O(n log k)
4. Binary Search — A
核心本质
Binary Search 的核心不是“找某个值”,而是在一个单调布尔序列里找边界——找第一个让 condition(mid) 为 true 的位置。
Lower bound / upper bound / exact search 都只是 condition() 的具体化。
统一模板(通用 condition helper)
func binarySearch(n int, condition func(mid int) bool) int {
left, right := 0, n
for left < right {
mid := left + (right-left)/2
if condition(mid) {
right = mid
} else {
left = mid + 1
}
}
return left
}
Lower Bound:第一个 >= target
func lowerBound(nums []int, target int) int {
left, right := 0, len(nums)
for left < right {
mid := left + (right-left)/2
if nums[mid] >= target { // condition: >= target
right = mid
} else {
left = mid + 1
}
}
return left
}
Upper Bound:第一个 > target
func upperBound(nums []int, target int) int {
left, right := 0, len(nums)
for left < right {
mid := left + (right-left)/2
if nums[mid] > target { // condition: > target
right = mid
} else {
left = mid + 1
}
}
return left
}
Exact Search:基于 Lower Bound
func exactSearch(nums []int, target int) int {
i := lowerBound(nums, target)
if i < len(nums) && nums[i] == target {
return i
}
return -1
}
所有变体的统一关系
first >= x = lowerBound(x)
first > x = upperBound(x)
last < x = lowerBound(x) - 1
last <= x = upperBound(x) - 1
exact x = lowerBound(x),然后检查 == x
count of x = upperBound(x) - lowerBound(x)
Search on Answer(搜索答案空间)
当问题不是在数组里搜索,而是在答案空间里搜索最小/最大可行值:
left, right := minPossible, maxPossible
for left < right {
mid := left + (right-left)/2
if canAchieve(mid) {
right = mid // feasible → try smaller
} else {
left = mid + 1 // not feasible → go bigger
}
}
return left
Rotated Sorted Array
先判断哪半边是 sorted,再决定 target 在哪边:
func search(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
return mid
}
if nums[left] <= nums[mid] { // left half sorted
if target >= nums[left] && target < nums[mid] {
right = mid - 1
} else {
left = mid + 1
}
} else { // right half sorted
if target > nums[mid] && target <= nums[right] {
left = mid + 1
} else {
right = mid - 1
}
}
}
return -1
}
关键词 → Binary Search
sorted array + find target → exact search / lower bound
first/last occurrence → lower bound / upper bound
"minimum X such that condition" → search on answer
rotated sorted array → modified binary search
peak element / mountain array → binary search on condition
复杂度
Time: O(log n)
Space: O(1)
5. Linked List — A
Go Struct 定义
type ListNode struct {
Val int
Next *ListNode
}
Dummy Head 技巧
当 head 可能被修改(删除、插入)时,用 dummy node 避免特殊处理:
dummy := &ListNode{Next: head}
curr := dummy
// ... 操作 ...
return dummy.Next
Reverse Linked List
Mental model:三个指针 prev / curr / next
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next
curr.Next = prev
prev = curr
curr = next
}
return prev
}
Detect Cycle(快慢指针)
func hasCycle(head *ListNode) bool {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}
Find Cycle Entry
func detectCycle(head *ListNode) *ListNode {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
slow = head
for slow != fast {
slow = slow.Next
fast = fast.Next
}
return slow
}
}
return nil
}
Find Middle
func middleNode(head *ListNode) *ListNode {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
Merge Two Sorted Lists
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
dummy := &ListNode{}
curr := dummy
for l1 != nil && l2 != nil {
if l1.Val <= l2.Val {
curr.Next = l1
l1 = l1.Next
} else {
curr.Next = l2
l2 = l2.Next
}
curr = curr.Next
}
if l1 != nil {
curr.Next = l1
} else {
curr.Next = l2
}
return dummy.Next
}
关键词 → Linked List Pattern
reverse / palindrome → reverse linked list
cycle detection → fast-slow pointers
find middle → fast-slow pointers
merge sorted → dummy head + two pointers
remove nth from end → two pointers (gap = n)
intersection of two lists → align lengths / two-pass
复杂度
Reverse: O(n) time, O(1) space
Cycle: O(n) time, O(1) space
Find Middle: O(n) time, O(1) space
Merge: O(n+m) time, O(1) space
6. LRU Cache — A-/B+
核心结构:
HashMap<key, *Node> + Doubly Linked List
head(dummy) ⇄ MRU ⇄ ... ⇄ LRU ⇄ tail(dummy)
操作逻辑:
get → lookup → moveToHead
put existing → update value → moveToHead
put new → create node → map insert → addToHead
over capacity → remove tail.prev → delete map[lru.key]
完整 Go 实现
type LRUCache struct {
capacity int
cache map[int]*Node
head, tail *Node
}
type Node struct {
key, value int
prev, next *Node
}
func Constructor(capacity int) LRUCache {
head := &Node{}
tail := &Node{}
head.next = tail
tail.prev = head
return LRUCache{capacity, map[int]*Node{}, head, tail}
}
func (c *LRUCache) Get(key int) int {
if node, ok := c.cache[key]; ok {
c.moveToHead(node)
return node.value
}
return -1
}
func (c *LRUCache) Put(key, value int) {
if node, ok := c.cache[key]; ok {
node.value = value
c.moveToHead(node)
return
}
node := &Node{key: key, value: value}
c.cache[key] = node
c.addToHead(node)
if len(c.cache) > c.capacity {
lru := c.removeTail()
delete(c.cache, lru.key)
}
}
func (c *LRUCache) addToHead(node *Node) {
node.prev = c.head
node.next = c.head.next
c.head.next.prev = node
c.head.next = node
}
func (c *LRUCache) removeNode(node *Node) {
node.prev.next = node.next
node.next.prev = node.prev
}
func (c *LRUCache) moveToHead(node *Node) {
c.removeNode(node)
c.addToHead(node)
}
func (c *LRUCache) removeTail() *Node {
node := c.tail.prev
c.removeNode(node)
return node
}
为什么需要 Doubly Linked List?
HashMap → O(1) lookup by key
DLL → O(1) insert/remove/reorder
Singly linked list 无法 O(1) 删除(不知道 prev)。
为什么 Node 需要存 key?
evict tail 时需要从 map 中删除,但 map.delete 需要 key。
如果 Node 不存 key,就不知道该删 map 中的哪个 entry。
结论:不用重新学,mock 前完整手写一次。
7. Two Pointers / Sliding Window — A-/B+
Two Sum Sorted:
sum < target → left++
sum > target → right--
3Sum:
sort → fix i → left/right
需要加强 duplicate handling:
if i > 0 && nums[i] == nums[i-1] {
continue
}
Longest Substring Without Repeating Characters:
HashMap/Frequency + Sliding Window
Invariant:window 内不能存在 frequency > 1 的 character。
8. Prefix Sum — A-
统一使用 n+1:
prefix[i] = sum(nums[0...i-1])
prefix := make([]int, len(nums)+1)
for i := 0; i < len(nums); i++ {
prefix[i+1] = prefix[i] + nums[i]
}
sum := prefix[right+1] - prefix[left]
复杂度:preprocessing O(n),query O(1),space O(n)。
9. Monotonic Stack — B
条件反射:
对每个元素寻找右侧第一个 greater/smaller element → Monotonic Stack。
Next Greater Element
通常存 index:
func dailyTemperatures(temperatures []int) []int {
n := len(temperatures)
result := make([]int, n)
stack := []int{} // stores indices, monotonically decreasing
for i := 0; i < n; i++ {
for len(stack) > 0 && temperatures[i] > temperatures[stack[len(stack)-1]] {
j := stack[len(stack)-1]
stack = stack[:len(stack)-1]
result[j] = i - j
}
stack = append(stack, i)
}
return result
}
每个 index 最多 push/pop 一次,所以 O(n)。
Monotonic Stack 变体
Next Greater Element → 维护递减栈,遇到更大的就 pop
Next Smaller Element → 维护递增栈,遇到更小的就 pop
Previous Greater → 从左往右,递减栈
Previous Smaller → 从左往右,递增栈
关键词 → Monotonic Stack
next greater / next warmer day → Monotonic Stack
largest rectangle in histogram → Monotonic Stack
stock span → Monotonic Stack
10. Tree — B-/C+
需要系统补基础。
Go Struct 定义
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
Traversal 顺序
Preorder: Root → Left → Right (常用于 copy / serialize)
Inorder: Left → Root → Right (BST 中得到有序序列)
Postorder: Left → Right → Root (常用于 delete / 计算子树结果)
DFS 递归模板
// Preorder
func preorder(root *TreeNode, result *[]int) {
if root == nil { return }
*result = append(*result, root.Val)
preorder(root.Left, result)
preorder(root.Right, result)
}
// Inorder
func inorder(root *TreeNode, result *[]int) {
if root == nil { return }
inorder(root.Left, result)
*result = append(*result, root.Val)
inorder(root.Right, result)
}
// Postorder
func postorder(root *TreeNode, result *[]int) {
if root == nil { return }
postorder(root.Left, result)
postorder(root.Right, result)
*result = append(*result, root.Val)
}
DFS 通用递归框架
大部分 Tree 题都是这个 pattern:
func dfs(root *TreeNode) ResultType {
// base case
if root == nil {
return baseValue
}
// 递归处理左右子树
left := dfs(root.Left)
right := dfs(root.Right)
// 用 left, right, root.Val 合并出当前节点的结果
return combine(left, right, root.Val)
}
经典 DFS 示例
// Maximum Depth
func maxDepth(root *TreeNode) int {
if root == nil { return 0 }
return 1 + max(maxDepth(root.Left), maxDepth(root.Right))
}
// Invert Binary Tree
func invertTree(root *TreeNode) *TreeNode {
if root == nil { return nil }
root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
return root
}
// Is Balanced
func isBalanced(root *TreeNode) bool {
return height(root) != -1
}
func height(root *TreeNode) int {
if root == nil { return 0 }
l := height(root.Left)
r := height(root.Right)
if l == -1 || r == -1 || abs(l-r) > 1 {
return -1
}
return 1 + max(l, r)
}
// Lowest Common Ancestor
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == nil || root == p || root == q {
return root
}
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
if left != nil && right != nil {
return root
}
if left != nil { return left }
return right
}
// Diameter of Binary Tree
var diameter int
func diameterOfBinaryTree(root *TreeNode) int {
diameter = 0
depthForDiameter(root)
return diameter
}
func depthForDiameter(root *TreeNode) int {
if root == nil { return 0 }
l := depthForDiameter(root.Left)
r := depthForDiameter(root.Right)
diameter = max(diameter, l+r)
return 1 + max(l, r)
}
BFS 层序遍历模板
func levelOrder(root *TreeNode) [][]int {
if root == nil { return nil }
var result [][]int
queue := []*TreeNode{root}
for len(queue) > 0 {
levelSize := len(queue) // 关键:先取当前层的大小
level := make([]int, 0, levelSize)
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
level = append(level, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
result = append(result, level)
}
return result
}
DFS 迭代版本(用显式栈)
// Iterative Preorder
func preorderIterative(root *TreeNode) []int {
if root == nil { return nil }
var result []int
stack := []*TreeNode{root}
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, node.Val)
if node.Right != nil { stack = append(stack, node.Right) }
if node.Left != nil { stack = append(stack, node.Left) }
}
return result
}
// Iterative Inorder
func inorderIterative(root *TreeNode) []int {
var result []int
var stack []*TreeNode
curr := root
for curr != nil || len(stack) > 0 {
for curr != nil {
stack = append(stack, curr)
curr = curr.Left
}
curr = stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, curr.Val)
curr = curr.Right
}
return result
}
BST 特殊性质
Inorder traversal of BST → sorted sequence
Search / Insert / Delete → O(h), balanced = O(log n)
Validate BST → inorder check 或 递归传递 (min, max) range
func isValidBST(root *TreeNode) bool {
return validate(root, math.MinInt64, math.MaxInt64)
}
func validate(node *TreeNode, min, max int) bool {
if node == nil { return true }
if node.Val <= min || node.Val >= max { return false }
return validate(node.Left, min, node.Val) && validate(node.Right, node.Val, max)
}
关键词 → Tree Pattern
depth / height / balanced → DFS 递归返回 int
path sum / max path sum → DFS 递归 + 全局变量
LCA → DFS 递归返回 *TreeNode
serialize / deserialize → preorder DFS
level order / zigzag / right view → BFS
BST search / validate / kth → BST 性质 + inorder
复杂度
Time: O(n) — 每个节点访问一次
Space: O(h) — 递归栈深度
balanced tree: O(log n)
skewed tree: O(n)
BFS space: O(w) — 最宽层的宽度,worst case O(n/2)
常见错误
✗ Maximum Depth 误判为 O(log n) time → 实际 O(n),每个节点都要访问
✗ Space 误判为 O(1) → 递归栈是 O(h)
✗ Preorder / Inorder 混淆 → 记住 "Pre" = root 在前,"In" = root 在中间
11. Graph — B-/C+
当前最高优先级模块之一。
Node vs Edge
edges = [[0,1], [1,2], [3,4]]
Nodes 是 0,1,2,3,4;数组中的 pair 才是 edges。
Graph node 有任意多个 neighbors,不是 binary tree 的 left/right。
Adjacency List 构建
// Undirected graph
graph := make([][]int, n)
for _, edge := range edges {
a, b := edge[0], edge[1]
graph[a] = append(graph[a], b)
graph[b] = append(graph[b], a) // 无向图双向
}
// Directed graph
graph := make([][]int, n)
for _, edge := range edges {
from, to := edge[0], edge[1]
graph[from] = append(graph[from], to) // 有向图单向
}
DFS 完整模板
func dfs(graph [][]int, node int, visited []bool) {
if visited[node] {
return
}
visited[node] = true
for _, next := range graph[node] {
dfs(graph, next, visited)
}
}
// 调用
visited := make([]bool, n)
dfs(graph, startNode, visited)
BFS 完整模板
func bfs(graph [][]int, start int, n int) {
visited := make([]bool, n)
visited[start] = true // mark when enqueued!
queue := []int{start}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
for _, next := range graph[node] {
if visited[next] {
continue
}
visited[next] = true // mark when enqueued, NOT when dequeued
queue = append(queue, next)
}
}
}
关键:mark visited when enqueued, not when dequeued。否则同一个节点会被多次入队。
BFS Shortest Path(带距离)
func shortestPath(graph [][]int, start, end, n int) int {
visited := make([]bool, n)
visited[start] = true
queue := []int{start}
dist := 0
for len(queue) > 0 {
levelSize := len(queue)
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
if node == end {
return dist
}
for _, next := range graph[node] {
if !visited[next] {
visited[next] = true
queue = append(queue, next)
}
}
}
dist++
}
return -1
}
Connected Components
func countComponents(n int, edges [][]int) int {
graph := make([][]int, n)
for _, e := range edges {
graph[e[0]] = append(graph[e[0]], e[1])
graph[e[1]] = append(graph[e[1]], e[0])
}
visited := make([]bool, n)
count := 0
for i := 0; i < n; i++ {
if !visited[i] {
count++
dfs(graph, i, visited)
}
}
return count
}
Grid DFS(Number of Islands)
func numIslands(grid [][]byte) int {
rows, cols := len(grid), len(grid[0])
count := 0
for i := 0; i < rows; i++ {
for j := 0; j < cols; j++ {
if grid[i][j] == '1' {
count++
gridDFS(grid, i, j, rows, cols)
}
}
}
return count
}
func gridDFS(grid [][]byte, i, j, rows, cols int) {
if i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] == '0' {
return
}
grid[i][j] = '0' // mark visited by modifying in-place
gridDFS(grid, i+1, j, rows, cols)
gridDFS(grid, i-1, j, rows, cols)
gridDFS(grid, i, j+1, rows, cols)
gridDFS(grid, i, j-1, rows, cols)
}
Grid BFS(Shortest Path)
四方向:
dirs := [4][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
func gridBFS(grid [][]int, start, end [2]int) int {
rows, cols := len(grid), len(grid[0])
dirs := [4][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
visited := make([][]bool, rows)
for i := range visited {
visited[i] = make([]bool, cols)
}
visited[start[0]][start[1]] = true
queue := [][2]int{start}
steps := 0
for len(queue) > 0 {
levelSize := len(queue)
for i := 0; i < levelSize; i++ {
cell := queue[0]
queue = queue[1:]
if cell == end {
return steps
}
for _, d := range dirs {
nr, nc := cell[0]+d[0], cell[1]+d[1]
if nr >= 0 && nr < rows && nc >= 0 && nc < cols &&
!visited[nr][nc] && grid[nr][nc] != 1 {
visited[nr][nc] = true
queue = append(queue, [2]int{nr, nc})
}
}
}
steps++
}
return -1
}
条件反射:
Unweighted graph + minimum steps/hops → BFS.
Cycle Detection(Directed Graph,3 states)
// 0=unvisited, 1=visiting, 2=visited
func hasCycleDirected(graph [][]int, node int, state []int) bool {
state[node] = 1 // visiting
for _, next := range graph[node] {
if state[next] == 1 { return true } // back edge = cycle
if state[next] == 0 {
if hasCycleDirected(graph, next, state) { return true }
}
}
state[node] = 2 // visited
return false
}
Cycle Detection(Undirected Graph,DFS + parent)
无向图不能用 3-state(因为 A→B 和 B→A 总是同时存在)。用 parent 区分“回边”和“来时的路”:
func hasCycleUndirected(graph [][]int, node, parent int, visited []bool) bool {
visited[node] = true
for _, next := range graph[node] {
if !visited[next] {
if hasCycleUndirected(graph, next, node, visited) {
return true
}
} else if next != parent {
return true // visited 且不是 parent → cycle
}
}
return false
}
// 调用(检查所有 component)
func containsCycle(n int, graph [][]int) bool {
visited := make([]bool, n)
for i := 0; i < n; i++ {
if !visited[i] {
if hasCycleUndirected(graph, i, -1, visited) {
return true
}
}
}
return false
}
也可以用 Union Find:加边时如果两个 node 已经在同一个 component → cycle。
Dijkstra(Weighted Shortest Path)
条件反射:带正权重的图求最短路 → Dijkstra。
核心思想:贪心 + 优先队列。每次取出距离最小的未确定节点,用它更新邻居距离。
import "container/heap"
type Edge struct {
to, weight int
}
type Item struct {
node, dist int
}
type PQ []Item
func (h PQ) Len() int { return len(h) }
func (h PQ) Less(i, j int) bool { return h[i].dist < h[j].dist } // min heap by dist
func (h PQ) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *PQ) Push(x any) { *h = append(*h, x.(Item)) }
func (h *PQ) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
func dijkstra(n int, graph [][]Edge, src int) []int {
dist := make([]int, n)
for i := range dist {
dist[i] = math.MaxInt64
}
dist[src] = 0
pq := &PQ{{src, 0}}
heap.Init(pq)
for pq.Len() > 0 {
curr := heap.Pop(pq).(Item)
if curr.dist > dist[curr.node] {
continue // 已经有更短的路径,跳过
}
for _, e := range graph[curr.node] {
newDist := dist[curr.node] + e.weight
if newDist < dist[e.to] {
dist[e.to] = newDist
heap.Push(pq, Item{e.to, newDist})
}
}
}
return dist
}
Dijkstra 使用示例:Network Delay Time
func networkDelayTime(times [][]int, n int, k int) int {
graph := make([][]Edge, n+1)
for _, t := range times {
from, to, w := t[0], t[1], t[2]
graph[from] = append(graph[from], Edge{to, w})
}
dist := dijkstra(n+1, graph, k)
maxDist := 0
for i := 1; i <= n; i++ {
if dist[i] == math.MaxInt64 {
return -1 // unreachable
}
maxDist = max(maxDist, dist[i])
}
return maxDist
}
Shortest Path 选择指南
Unweighted graph → BFS O(V+E)
Positive weights → Dijkstra O((V+E) log V)
Negative weights (no neg cycle) → Bellman-Ford O(V·E)
All pairs → Floyd-Warshall O(V³)
DAG → Topological Sort + relax O(V+E)
BFS/Dijkstra + Extra State
有些最短路问题需要额外状态维度(如“最多经过 K 站”、“带钥匙状态”):
// 示例:Cheapest Flights Within K Stops
// state = (node, stops_used)
type State struct {
node, cost, stops int
}
func findCheapestPrice(n int, flights [][]int, src, dst, k int) int {
graph := make([][]Edge, n)
for _, f := range flights {
graph[f[0]] = append(graph[f[0]], Edge{f[1], f[2]})
}
// dist[node] = min cost to reach node
dist := make([]int, n)
for i := range dist { dist[i] = math.MaxInt64 }
dist[src] = 0
// BFS by level (each level = one stop)
queue := []State{{src, 0, 0}}
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
if curr.stops > k { continue }
for _, e := range graph[curr.node] {
newCost := curr.cost + e.weight
if newCost < dist[e.to] {
dist[e.to] = newCost
queue = append(queue, State{e.to, newCost, curr.stops + 1})
}
}
}
if dist[dst] == math.MaxInt64 { return -1 }
return dist[dst]
}
Minimum Spanning Tree(MST)⚠️ 知道即可
条件反射:连接所有节点,总权重最小 → MST。
Kruskal(贪心 + Union Find)
思路:把所有边按权重排序,从小到大加边,如果两端不在同一个 component 就合并。
func kruskal(n int, edges [][]int) int {
// edges[i] = [from, to, weight]
sort.Slice(edges, func(i, j int) bool {
return edges[i][2] < edges[j][2]
})
uf := NewUnionFind(n)
totalWeight := 0
edgesUsed := 0
for _, e := range edges {
if uf.Union(e[0], e[1]) {
totalWeight += e[2]
edgesUsed++
if edgesUsed == n-1 {
break // MST 有 n-1 条边
}
}
}
if edgesUsed < n-1 {
return -1 // graph not connected
}
return totalWeight
}
Prim(贪心 + Priority Queue)
思路:从任意节点开始,每次把当前 MST 集合能到达的最短边加进来。
func prim(n int, graph [][]Edge) int {
visited := make([]bool, n)
pq := &PQ{{0, 0}} // start from node 0, cost 0
heap.Init(pq)
totalWeight := 0
nodesAdded := 0
for pq.Len() > 0 && nodesAdded < n {
curr := heap.Pop(pq).(Item)
if visited[curr.node] {
continue
}
visited[curr.node] = true
totalWeight += curr.dist
nodesAdded++
for _, e := range graph[curr.node] {
if !visited[e.to] {
heap.Push(pq, Item{e.to, e.weight})
}
}
}
if nodesAdded < n { return -1 }
return totalWeight
}
Kruskal vs Prim
| Kruskal | Prim | |
|---|---|---|
| 思路 | 全局排序边,贪心加边 | 从一个点扩展,贪心加最近的边 |
| 数据结构 | Union Find | Priority Queue |
| 适合 | 边少(sparse graph) | 边多(dense graph) |
| 复杂度 | O(E log E) | O((V+E) log V) |
Graph 问题分类总览
Graph Problems
│
├── 1. Traversal / Connectivity
│ ├── DFS
│ └── BFS
│
├── 2. Shortest Path
│ ├── Unweighted → BFS
│ ├── Positive weighted → Dijkstra
│ └── With extra state → BFS/Dijkstra + State
│
├── 3. Dependency / Ordering
│ └── Topological Sort (Kahn's BFS)
│
├── 4. Connected Components
│ ├── DFS / BFS
│ └── Union Find (DSU)
│
├── 5. Cycle Detection
│ ├── Undirected → DFS + parent / Union Find
│ └── Directed → DFS 3-state / Topological Sort
│
└── 6. Minimum Spanning Tree
├── Kruskal (sort edges + Union Find)
└── Prim (priority queue expansion)
关键词 → Graph Pattern
connected components / islands → DFS/BFS + visited
shortest path (unweighted) → BFS
shortest path (positive weights) → Dijkstra
shortest path (with constraints) → BFS/Dijkstra + extra state
cycle detection (directed) → DFS 3-state / Topo Sort
cycle detection (undirected) → DFS + parent / Union Find
prerequisites / dependencies → Topological Sort
"is A connected to B" (dynamic) → Union Find
minimum cost to connect all nodes → MST (Kruskal / Prim)
常见错误
✗ BFS 在 dequeue 时才 mark visited → 导致重复入队,TLE
✗ 无向图忘记双向添加边
✗ Grid 越界检查遗漏
✗ 混淆 node 和 edge
✗ Dijkstra 用于负权图 → 不适用,需要 Bellman-Ford
✗ Undirected cycle 用 3-state → 无法区分回边和来时的路,需要 parent 参数
✗ MST 忘记检查 graph 是否 connected
12. Topological Sort — C+/B-
核心概念
indegree = number of incoming edges(有多少边指向这个 node)。
Dependency 方向:
A depends on B → edge: B → A → indegree[A]++
"要先完成 B 才能做 A"
Kahn’s Algorithm(BFS)完整 Go 实现
func topologicalSort(n int, prerequisites [][]int) ([]int, bool) {
graph := make([][]int, n)
indegree := make([]int, n)
for _, p := range prerequisites {
// p = [course, prerequisite]
// prerequisite → course
course, prereq := p[0], p[1]
graph[prereq] = append(graph[prereq], course)
indegree[course]++
}
// 所有 indegree == 0 的入队
queue := []int{}
for i := 0; i < n; i++ {
if indegree[i] == 0 {
queue = append(queue, i)
}
}
order := []int{}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
order = append(order, node)
for _, next := range graph[node] {
indegree[next]-- // decrement neighbor's indegree
if indegree[next] == 0 {
queue = append(queue, next)
}
}
}
if len(order) < n {
return nil, false // cycle detected
}
return order, true
}
Course Schedule(能否修完所有课)
func canFinish(numCourses int, prerequisites [][]int) bool {
_, ok := topologicalSort(numCourses, prerequisites)
return ok
}
关键词 → Topological Sort
prerequisites / dependencies → Topo Sort
build order / task scheduling → Topo Sort
detect cycle in directed graph → Topo Sort (len(order) < n)
常见错误
✗ edge direction 写反:A depends on B,edge 应该是 B → A,不是 A → B
✗ decrement 了错误 node 的 indegree:应该 decrement neighbor 的,不是当前 node 的
✗ 忘记检测 cycle:len(order) < n 说明有 cycle
13. Basic DP — B
DP 面试固定流程
1. Define state: dp[i] 代表什么?
2. Choices: 每一步有什么选择?
3. Transition: dp[i] 怎么从之前的状态得到?
4. Base cases: dp[0], dp[1] 等初始值
5. Complexity: Time & Space
6. Optimization: 能否只用 O(1) 变量代替数组?
三种 DP 类型
// Counting(多少种方式?) → 加法
dp[i] = dp[i-1] + dp[i-2] // Climbing Stairs
// Optimization(最小/最大?) → min/max
dp[i] = max(dp[i-1], dp[i-2] + nums[i]) // House Robber
dp[i] = min(dp[i-coin] + 1, dp[i]) // Coin Change
// Longest Sequence → max with condition
dp[i] = max(dp[j]+1) for j < i where nums[j] < nums[i] // LIS
House Robber 完整实现
func rob(nums []int) int {
n := len(nums)
if n == 1 { return nums[0] }
dp := make([]int, n)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i := 2; i < n; i++ {
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
}
return dp[n-1]
}
// Space optimized O(1)
func rob(nums []int) int {
prev2, prev1 := 0, 0
for _, num := range nums {
curr := max(prev1, prev2+num)
prev2 = prev1
prev1 = curr
}
return prev1
}
Coin Change 完整实现
func coinChange(coins []int, amount int) int {
dp := make([]int, amount+1)
for i := range dp {
dp[i] = amount + 1 // impossible value
}
dp[0] = 0
for i := 1; i <= amount; i++ {
for _, coin := range coins {
if coin <= i {
dp[i] = min(dp[i], dp[i-coin]+1)
}
}
}
if dp[amount] > amount { return -1 }
return dp[amount]
}
Longest Increasing Subsequence
func lengthOfLIS(nums []int) int {
n := len(nums)
dp := make([]int, n)
for i := range dp { dp[i] = 1 }
result := 1
for i := 1; i < n; i++ {
for j := 0; j < i; j++ {
if nums[j] < nums[i] {
dp[i] = max(dp[i], dp[j]+1)
}
}
result = max(result, dp[i])
}
return result
}
2D DP:Unique Paths
func uniquePaths(m, n int) int {
dp := make([][]int, m)
for i := range dp {
dp[i] = make([]int, n)
dp[i][0] = 1
}
for j := 0; j < n; j++ { dp[0][j] = 1 }
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
dp[i][j] = dp[i-1][j] + dp[i][j-1]
}
}
return dp[m-1][n-1]
}
关键词 → DP Pattern
how many ways → counting DP (加法)
minimum cost / maximum → optimization DP (min/max)
longest / shortest seq → sequence DP
can I reach / is possible → boolean DP
grid paths → 2D DP
14. 0/1 Knapsack — B-/C+
核心区别
0/1 Knapsack → 每个 item 最多用一次 → 容量从大到小遍历
Complete Knapsack → 每个 item 可以无限用 → 容量从小到大遍历
为什么 0/1 必须倒序? 正序时 dp[w-weight] 可能已经在本轮被更新(包含了当前 item),导致同一个 item 被重复使用。
0/1 Knapsack 完整实现
func knapsack01(weights, values []int, capacity int) int {
dp := make([]int, capacity+1)
for i := 0; i < len(weights); i++ {
// 必须从大到小!
for w := capacity; w >= weights[i]; w-- {
dp[w] = max(dp[w], dp[w-weights[i]]+values[i])
}
}
return dp[capacity]
}
Complete Knapsack 完整实现
func knapsackComplete(weights, values []int, capacity int) int {
dp := make([]int, capacity+1)
for i := 0; i < len(weights); i++ {
// 从小到大,允许重复使用
for w := weights[i]; w <= capacity; w++ {
dp[w] = max(dp[w], dp[w-weights[i]]+values[i])
}
}
return dp[capacity]
}
常见变体
Subset Sum (能否凑出 target?) → 0/1 Knapsack, dp[w] = bool
Coin Change (最少硬币数) → Complete Knapsack, dp[w] = min coins
Coin Change II (组合数) → Complete Knapsack, dp[w] += dp[w-coin]
Partition Equal Subset Sum → 0/1 Knapsack, target = sum/2
15. Backtracking — B-/C+
核心 Mental Model
current state → choices → choose → recurse → unchoose
核心 invariant:进入一个 branch 前是什么状态,从 branch 返回后就恢复成什么状态。
Subsets
func subsets(nums []int) [][]int {
var result [][]int
var backtrack func(start int, path []int)
backtrack = func(start int, path []int) {
// 每个状态都是一个合法子集
result = append(result, append([]int{}, path...))
for i := start; i < len(nums); i++ {
path = append(path, nums[i])
backtrack(i+1, path)
path = path[:len(path)-1] // unchoose
}
}
backtrack(0, []int{})
return result
}
Permutations
func permute(nums []int) [][]int {
var result [][]int
used := make([]bool, len(nums))
var backtrack func(path []int)
backtrack = func(path []int) {
if len(path) == len(nums) {
result = append(result, append([]int{}, path...))
return
}
for i := 0; i < len(nums); i++ {
if used[i] { continue }
used[i] = true
path = append(path, nums[i])
backtrack(path)
path = path[:len(path)-1]
used[i] = false
}
}
backtrack([]int{})
return result
}
Combination Sum(可重复使用元素)
func combinationSum(candidates []int, target int) [][]int {
var result [][]int
sort.Ints(candidates)
var backtrack func(start, remain int, path []int)
backtrack = func(start, remain int, path []int) {
if remain == 0 {
result = append(result, append([]int{}, path...))
return
}
for i := start; i < len(candidates); i++ {
if candidates[i] > remain { break }
path = append(path, candidates[i])
backtrack(i, remain-candidates[i], path) // i not i+1, can reuse
path = path[:len(path)-1]
}
}
backtrack(0, target, []int{})
return result
}
Generate Parentheses(Constraint Pruning)
func generateParenthesis(n int) []string {
var result []string
var backtrack func(path []byte, open, close int)
backtrack = func(path []byte, open, close int) {
if len(path) == 2*n {
result = append(result, string(path))
return
}
if open < n {
backtrack(append(path, '('), open+1, close)
}
if close < open {
backtrack(append(path, ')'), open, close+1)
}
}
backtrack([]byte{}, 0, 0)
return result
}
关键词 → Backtracking
all subsets / combinations → Subsets template (start index)
all permutations / arrangements → Permutations template (used array)
generate valid X → constraint pruning
word search / path finding → grid backtracking
Go Slice 陷阱
保存结果时必须 copy,否则后续修改会影响已保存的结果:
// ✗ 错误:直接 append path
result = append(result, path)
// ✓ 正确:copy 一份
result = append(result, append([]int{}, path...))
常见错误
✗ Generate Parentheses 识别成 Stack 题 → 它是 backtracking + constraint pruning
✗ 不理解为什么要 unchoose → 因为要回到 branch 前的状态,才能探索其他 branch
✗ Subsets 用 i+1,Combination Sum (reuse) 用 i → 是否允许重复使用
16. Greedy — TBD,约 B-/C+
Stock 题最初主要是漏看 buy once / sell once。理解后能得到:
minPrice
maxProfit
扫描时:
profit if selling today = price - minPrice
维护 minPrice 与 maxProfit。
Greedy 更准确的理解:
Make a locally optimal choice that can be proven not to hurt the global optimum.
需要再用 1–2 道题确认真实水平。
17. Union Find / DSU — D
核心操作
find(x) → x 属于哪个 connected component(返回 root)
union(a,b) → merge two components
完整 Go 实现(Path Compression + Union by Rank)
type UnionFind struct {
parent []int
rank []int
count int // number of components
}
func NewUnionFind(n int) *UnionFind {
parent := make([]int, n)
rank := make([]int, n)
for i := range parent {
parent[i] = i
}
return &UnionFind{parent, rank, n}
}
func (uf *UnionFind) Find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.Find(uf.parent[x]) // path compression
}
return uf.parent[x]
}
func (uf *UnionFind) Union(x, y int) bool {
px, py := uf.Find(x), uf.Find(y)
if px == py { return false } // already connected
// union by rank
if uf.rank[px] < uf.rank[py] {
px, py = py, px
}
uf.parent[py] = px
if uf.rank[px] == uf.rank[py] {
uf.rank[px]++
}
uf.count--
return true
}
func (uf *UnionFind) Connected(x, y int) bool {
return uf.Find(x) == uf.Find(y)
}
使用示例:Redundant Connection
func findRedundantConnection(edges [][]int) []int {
n := len(edges)
uf := NewUnionFind(n + 1)
for _, e := range edges {
if !uf.Union(e[0], e[1]) {
return e // already connected → this edge creates cycle
}
}
return nil
}
关键词 → Union Find
dynamic connectivity / "are A and B connected" → Union Find
detect cycle in undirected graph → Union Find
number of connected components (dynamic edges) → Union Find
accounts merge / friend circles → Union Find
Union Find vs DFS/BFS
| Union Find | DFS/BFS | |
|---|---|---|
| 适用 | 动态加边,在线查询 | 静态图,一次性遍历 |
| 优势 | merge + query 近似 O(1) | 完整路径/组件探索 |
| 典型 | Redundant Connection | Number of Islands |
复杂度
Find: O(α(n)) ≈ O(1) amortized (with path compression + union by rank)
Union: O(α(n)) ≈ O(1) amortized
Space: O(n)
18. Rate Limiter — B
Fixed Window — B+
userID → windowStart + count
窗口对齐。主要问题是 boundary burst。
Sliding Window Log — B+/A-
userID → []timestamp
对于 (t-60, t]:
remove timestamp <= t-60
keep timestamp > t-60
可以 binary search upper bound,也可以 deque 从 head 清理。
Concurrency
相同 timestamp 本身完全合法。
真正需要 atomic 的是:
cleanup → check quota → update state
单机可 per-user mutex;distributed 可以使用 Redis atomic operation / Lua。
Sliding Window Counter
保存 previous/current fixed-window counts,通过 previous window 的 weighted portion 近似 sliding window。
优点 O(1) state/user;缺点是 approximation。
Token Bucket — C+/B-
状态:
tokens
lastRefillTime
不是周期性 reset。
refill = elapsed * refillRate
tokens = min(capacity, tokens + refill)
capacity → maximum burst
refill rate → long-term rate
19. Clarification:跨题核心能力
有时不是不会算法,而是 requirement 没完全读清楚。Stock 题漏掉 once
是典型例子。
Live Coding 固定流程:
1. Restate the problem
2. Clarify constraints
3. Give example / edge case
4. Explain approach
5. Analyze complexity
6. Code
7. Test
特别确认:
duplicates?
sorted?
negative numbers?
empty input?
one solution or multiple?
can reuse elements?
once or unlimited?
directed or undirected?
weighted or unweighted?
input size?
20. 最终能力地图
Topic Level Priority
HashMap A Low Heap / Priority Queue A Low Binary Search A Low Linked List A Low LRU Cache A-/B+ Medium Two Pointers A- Low Sliding Window A-/B+ Medium Prefix Sum A- Low Intervals A-/B+ Low Monotonic Stack B Medium Basic DP B Medium Tree B-/C+ High Graph BFS/DFS B-/C+ High Grid BFS B- High Topological Sort C+/B- High 0/1 Knapsack B-/C+ High Backtracking B-/C+ High Greedy TBD ~ B-/C+ Medium Union Find D Medium-Low Fixed Window Rate Limiter B+ Medium Sliding Window Rate Limiter B+/A- Medium Token Bucket C+/B- Medium
21. 接下来两天复习优先级
Priority 1
Graph
├── adjacency list
├── DFS
├── BFS
├── connected components
├── grid BFS / shortest path
└── topological sort
Tree
├── preorder / inorder / postorder
├── recursive DFS
├── level-order BFS
└── common tree recursion
Backtracking
├── subsets
├── permutations
├── generate parentheses
└── choose → explore → unchoose
DP
├── House Robber
└── 0/1 Knapsack
Priority 2
LRU Cache → 完整手写一次
Rate Limiter → 完整手写一次
Monotonic Stack → 完整手写一次
Priority 3
进入 Live Coding Mock。重点测试:
clarify
→ identify pattern
→ explain
→ code
→ test
→ handle follow-up
22. Jacksite 推荐结构
Interview Preparation
│
├── Algorithm Capability Map
├── Algorithm Patterns
│ ├── HashMap
│ ├── Sliding Window
│ ├── Binary Search
│ ├── Tree
│ ├── Graph
│ ├── Backtracking
│ ├── DP
│ ├── Knapsack
│ ├── Rate Limiter
│ └── LRU
├── Live Coding Retrospectives
├── System Design
│ ├── Asset Price Watching
│ ├── Order Routing
│ └── Account Opening
└── Interview Question Bank
Capability Map 应动态维护:
Graph = C+
→ 练习
→ Graph = B
→ Mock 中独立完成
→ Graph = B+
23. 本次摸底最值得保留的错误清单
- Tree traversal:Preorder / Inorder 一开始混淆。
- Tree complexity:Maximum Depth 一开始误判为 O(log n) time / O(1) space。
- Graph representation:一开始混淆 node 与 edge,不熟悉 adjacency list。
- Graph BFS:enqueue neighbor 时漏掉
visited = true。 - Topological Sort:dependency edge direction 写反。
- Topological Sort:decrement 了错误 node 的 indegree。
- 0/1 Knapsack:没有稳定记住 capacity 必须倒序。
- Generate Parentheses:一开始识别成 Valid Parentheses / Stack。
- Backtracking:开始不理解 recursion 返回后为什么必须 unchoose。
- Token Bucket:一开始理解成周期性 reset,而不是 elapsed-time refill。
- Stock / Greedy:漏读
buy once / sell once,说明 requirement clarification 需要刻意执行。 - Sliding Window Rate Limiter:主动识别 concurrency 问题;需记住问题是 atomic check-and-update,而不是相同 timestamp 本身。
24. 面试前最终原则
不要追求“所有算法都学过”。
更重要的是:
强项稳定拿分,弱项补到能识别 pattern,并把 Live Coding 的完整执行流程练熟。
每一道题强制按照:
Clarify
→ Define
→ Approach
→ Invariant / State
→ Complexity
→ Code
→ Test
→ Follow-up
执行。