Coding Test 速查:Hot 100 + 手写 MHA / Conv
这页是面试前两小时复习用的,不是完整题解集。目标是快速回忆 Hot 100 的题型入口、常见模板,以及三个高频深度学习手写题:multi-head attention、1D convolution、2D convolution。
核心原则:Hot 100 不是背题号,而是背“看到题目先套哪个状态/数据结构/转移”。代码题不是炫技,而是写清楚 shape、边界、mask、stride/padding。
1. 两小时复习计划
| 时间 | 复习内容 | 目标 |
|---|---|---|
| 0-15 min | 数组、哈希、前缀和、双指针、滑动窗口 | 先拿最容易出的题 |
| 15-35 min | 链表、栈、堆、二分 | 把模板写顺 |
| 35-60 min | 二叉树 DFS/BFS、BST、LCA | 递归返回值要清楚 |
| 60-85 min | 图 DFS/BFS、拓扑排序、并查/矩阵搜索 | visited 和状态染色 |
| 85-115 min | DP、回溯、贪心/区间 | 写出状态定义和转移 |
| 115-120 min | MHA / Conv1D / Conv2D shape 口述 | 防止代码题卡在维度 |
2. Hot 100 题型速查
| 类型 | 必会题 | 第一反应 |
|---|---|---|
| 哈希 / set | Two Sum, Longest Consecutive, Group Anagrams, Majority Element | 计数、查补数、set 判断序列起点 |
| 前缀和 | Subarray Sum Equals K, Path Sum III, Product Except Self | prefix[cur-k],树上回溯要撤销 |
| 双指针 | 3Sum, Container With Most Water, Move Zeroes, Palindrome Linked List | 排序后夹逼;链表找中点再反转 |
| 滑动窗口 | Longest Substring Without Repeating, Find All Anagrams | 右扩左缩,维护计数 |
| 链表 | Reverse List, Merge Two Lists, Cycle, Detect Cycle, Remove Nth, LRU | dummy、快慢指针、哈希+双向链表 |
| 栈 | Valid Parentheses, Daily Temperatures, Decode String, MinStack | 单调栈存 index;辅助栈存 min |
| 堆 / TopK | Kth Largest, Top K Frequent, Merge/Meeting style | heap 或 bucket;Kth largest 可 quickselect |
| 二分 | Search Rotated Array, Search Range, Matrix Search | 明确哪边有序;左右边界分开写 |
| 树 DFS | Max Depth, Invert Tree, Diameter, LCA, Flatten, Build Tree | 递归函数返回什么先说清 |
| BST | Validate BST, Convert BST, Kth-like traversal | 中序、上下界、反中序累加 |
| 图 / 矩阵 | Number of Islands, Course Schedule, Word Search, Evaluate Division | DFS/BFS visited;拓扑排序查环 |
| DP 一维 | House Robber, Coin Change, Word Break, LIS | dp[i] 的含义先定 |
| DP 二维 | Maximal Square, Edit Distance, Unique Paths, Min Path Sum | 当前格依赖左/上/左上 |
| 回溯 | Generate Parentheses, Permutations, Subsets, Combination Sum | choose -> dfs -> unchoose |
| 贪心 / 区间 | Jump Game, Merge Intervals, Queue Reconstruction, Meeting Rooms | 排序准则最重要 |
3. Hot 100 解法速查表
这张表只放面试复习最有用的信息:题目入口、模板、最关键的一步。真正写代码时先说模板,再落到边界。
| 题 | 模板入口 | 一行解法 |
|---|---|---|
| 1 Two Sum | 哈希表 | 遍历 x 时查 target-x 是否见过,存值到下标。 |
| 2 Add Two Numbers | 链表模拟 | 双指针逐位相加,维护 carry,用 dummy 串结果。 |
| 3 Longest Substring Without Repeating | 滑动窗口 | 右扩计数,重复时左缩到合法,更新最大长度。 |
| 4 Median of Two Sorted Arrays | 二分划分 | 在短数组二分切分点,使左右两半数量相等且左侧都小于右侧。 |
| 5 Longest Palindromic Substring | 中心扩展 | 枚举奇偶中心向两侧扩,保存最长区间。 |
| 10 Regular Expression Matching | DP | dp[i][j] 表示 s[:i] 匹配 p[:j],重点处理 * 的零次和多次。 |
| 11 Container With Most Water | 双指针 | 左右夹逼,每次移动较短边,因为面积受短边限制。 |
| 15 3Sum | 排序 + 双指针 | 固定第一个数,剩下区间夹逼,注意跳过重复值。 |
| 17 Letter Combinations | 回溯 | 每位数字展开字符,路径长度等于输入长度时收集。 |
| 19 Remove Nth From End | 快慢指针 | dummy 起步,fast 先走 n 步,再同步走到待删前驱。 |
| 20 Valid Parentheses | 栈 | 左括号入栈,右括号必须匹配栈顶,最后栈空。 |
| 21 Merge Two Sorted Lists | 链表归并 | dummy + tail,每次接较小节点,最后接剩余链表。 |
| 22 Generate Parentheses | 回溯剪枝 | left < n 可放左括号,right < left 可放右括号。 |
| 23 Merge k Sorted Lists | 堆 / 分治 | 小根堆按节点值弹出,或两两归并到一条链表。 |
| 31 Next Permutation | 后缀处理 | 从右找下降点,和右侧刚大于它的数交换,再反转后缀。 |
| 32 Longest Valid Parentheses | 栈 / DP | 栈存下标并保留最后非法位置,遇到匹配时更新长度。 |
| 33 Search in Rotated Sorted Array | 二分 | 每轮判断哪一半有序,再决定目标是否落在有序半边。 |
| 34 Find First and Last Position | 二分边界 | 分别写 lower_bound 找第一个 target 和第一个 > target。 |
| 39 Combination Sum | 回溯 | candidates 可重复,递归传当前 index,剩余和为 0 收集。 |
| 42 Trapping Rain Water | 双指针 | 维护左右最大值,移动较小侧并累加可接水量。 |
| 46 Permutations | 回溯 | used 数组控制每个数只选一次,路径满长收集。 |
| 48 Rotate Image | 原地矩阵 | 先沿主对角线转置,再反转每一行。 |
| 49 Group Anagrams | 哈希签名 | 排序字符串或 26 维计数作为 key 分组。 |
| 53 Maximum Subarray | Kadane | cur=max(x, cur+x),答案取所有 cur 最大值。 |
| 55 Jump Game | 贪心 | 维护当前能到的最远位置,遍历中一旦 i > far 返回 false。 |
| 56 Merge Intervals | 排序 + 合并 | 按 start 排序,当前区间和结果尾部重叠就扩 end。 |
| 62 Unique Paths | DP | dp[j] += dp[j-1],每格来自上方和左方。 |
| 64 Minimum Path Sum | DP | 原地或一维 DP,当前格加上上方/左方较小值。 |
| 70 Climbing Stairs | 斐波那契 | dp[i]=dp[i-1]+dp[i-2],可用两个变量滚动。 |
| 72 Edit Distance | 二维 DP | dp[i][j] 表示前缀距离,增删改取最小。 |
| 75 Sort Colors | 三指针 | zero/i/two 扫描,0 换前面,2 换后面,1 跳过。 |
| 76 Minimum Window Substring | 滑动窗口 | 右扩满足 need,左缩保持有效并更新最短窗口。 |
| 78 Subsets | 回溯 / 位枚举 | 每个位置选或不选,或遍历 bitmask。 |
| 79 Word Search | DFS 回溯 | 从匹配首字母格出发,四方向搜索,访问过的格临时标记。 |
| 84 Largest Rectangle in Histogram | 单调栈 | 栈递增,遇到更矮柱子就弹出并以弹出高度算面积。 |
| 85 Maximal Rectangle | 单调栈 + 行高 | 每行把连续 1 转成 histogram,套 84。 |
| 94 Binary Tree Inorder Traversal | 栈 / DFS | 左根右,迭代版一路压左,弹出后转右。 |
| 96 Unique Binary Search Trees | DP / Catalan | 枚举根 k,dp[n]+=dp[k-1]*dp[n-k]。 |
| 98 Validate BST | 上下界 / 中序 | 递归带 (low, high),或中序必须严格递增。 |
| 101 Symmetric Tree | 双递归 | 比较 left.left 对 right.right、left.right 对 right.left。 |
| 102 Binary Tree Level Order | BFS | 队列逐层弹出,每层记录当前队列长度。 |
| 104 Maximum Depth of Binary Tree | DFS | 返回 1 + max(left_depth, right_depth)。 |
| 105 Build Tree from Preorder and Inorder | 递归建树 | preorder 首元素是根,用 inorder 下标切左右子树。 |
| 114 Flatten Binary Tree to Linked List | 后序 / 前序重连 | 把左子树展平接到右边,再把原右子树接到左链尾部。 |
| 121 Best Time to Buy and Sell Stock | 一次扫描 | 维护历史最低价,用当前价减最低价更新最大利润。 |
| 124 Binary Tree Maximum Path Sum | DFS | 返回单边最大贡献,答案用 left+root+right 更新。 |
| 128 Longest Consecutive Sequence | 哈希 set | 只从没有前驱的数开始向后数长度。 |
| 136 Single Number | 位运算 | 所有数字异或,成对数字抵消,剩下单数。 |
| 139 Word Break | DP + set | dp[i] 表示前缀可拆,枚举切点 j 查 s[j:i]。 |
| 141 Linked List Cycle | 快慢指针 | slow 走一步 fast 走两步,相遇则有环。 |
| 142 Linked List Cycle II | 快慢指针 | 相遇后一个指针回 head,再同步走,相遇点是入环点。 |
| 146 LRU Cache | 哈希 + 双向链表 | map 定位节点,链表维护最近使用顺序,容量满删尾部。 |
| 148 Sort List | 归并排序 | 快慢指针切半,递归排序后 merge 两条有序链。 |
| 152 Maximum Product Subarray | DP 滚动 | 同时维护最大/最小乘积,遇负数会交换角色。 |
| 155 Min Stack | 辅助栈 | 主栈存值,min 栈同步存当前最小值。 |
| 160 Intersection of Two Linked Lists | 双指针换头 | A/B 指针走到尾后切到对方头部,路径总长相同后相遇。 |
| 169 Majority Element | Boyer-Moore | 票数为 0 换候选,相同加一,不同减一。 |
| 198 House Robber | 一维 DP | dp=max(skip, rob_prev+x),两个变量滚动。 |
| 200 Number of Islands | DFS/BFS | 遇到陆地就 flood fill 标记整座岛,计数加一。 |
| 206 Reverse Linked List | 指针反转 | prev, cur 迭代,每次保存 next 后反向。 |
| 207 Course Schedule | 拓扑 / 染色 DFS | 入度 BFS 看能否修完,或 DFS 三色判环。 |
| 208 Implement Trie | 前缀树 | 每个节点保存 children 和 is_end,插入/查询逐字符走。 |
| 215 Kth Largest Element | 堆 / quickselect | 小根堆保留 k 个,或 partition 找下标 n-k。 |
| 221 Maximal Square | 二维 DP | 若当前为 1,边长是左/上/左上最小值加一。 |
| 226 Invert Binary Tree | DFS | 交换左右子树,再递归处理子树。 |
| 234 Palindrome Linked List | 快慢 + 反转 | 找中点,反转后半段,再和前半段逐个比较。 |
| 236 Lowest Common Ancestor | 后序 DFS | 左右子树都命中则当前是 LCA,否则返回命中的一边。 |
| 238 Product Except Self | 前后缀乘积 | 左乘积扫一遍,右乘积反向扫一遍,无需除法。 |
| 239 Sliding Window Maximum | 单调队列 | 队列存下标且值递减,窗口右移时弹出过期下标。 |
| 240 Search a 2D Matrix II | 右上角搜索 | 从右上开始,大了左移,小了下移。 |
| 279 Perfect Squares | DP / BFS | dp[i]=min(dp[i-j*j]+1),也可把余数当图 BFS。 |
| 283 Move Zeroes | 双指针 | slow 指向下一个非零位置,遍历时把非零换到前面。 |
| 287 Find the Duplicate Number | 快慢指针 | 把数组值看成 next 指针,Floyd 找环入口。 |
| 300 Longest Increasing Subsequence | 贪心 + 二分 | tails[k] 存长度 k+1 的最小尾值,二分替换。 |
| 301 Remove Invalid Parentheses | BFS / 回溯 | BFS 按删除次数扩展,首次出现合法层即答案。 |
| 309 Best Time to Buy and Sell Stock with Cooldown | 状态机 DP | 维护 hold/sold/rest,sold 后一天只能到 cooldown/rest。 |
| 312 Burst Balloons | 区间 DP | dp[l][r] 表示开区间最大值,枚举最后戳破的气球。 |
| 322 Coin Change | 完全背包 | dp[amount]=min(dp[amount], dp[amount-coin]+1)。 |
| 337 House Robber III | 树形 DP | 每个节点返回 rob/not_rob 两个值。 |
| 338 Counting Bits | 位 DP | bits[i]=bits[i>>1]+(i&1)。 |
| 347 Top K Frequent Elements | 堆 / 桶 | 统计频率后用小根堆保 k 个,或按频率进桶。 |
| 394 Decode String | 栈 | 遇 [ 保存当前字符串和重复次数,遇 ] 弹出拼接。 |
| 399 Evaluate Division | 图 DFS / 并查 | 变量是点,比值是带权边,查询时找路径乘积。 |
| 406 Queue Reconstruction by Height | 贪心排序 | 按身高降序、k 升序排序,再按 k 插入。 |
| 416 Partition Equal Subset Sum | 0/1 背包 | 目标是 sum/2,倒序更新 boolean dp。 |
| 437 Path Sum III | 树上前缀和 | DFS 维护 prefix count,进入加一,退出撤销。 |
| 438 Find All Anagrams in a String | 滑动窗口计数 | 固定长度窗口维护字符差异,差异为 0 记录起点。 |
| 448 Find All Numbers Disappeared | 原地标记 | 用 abs(nums[i])-1 做下标,把对应位置变负。 |
| 461 Hamming Distance | 位运算 | x ^ y 后统计 1 的个数。 |
| 494 Target Sum | 背包转化 | 正负号问题转成子集和 (sum+target)/2。 |
| 538 Convert BST to Greater Tree | 反中序 | 右根左遍历,累加已见更大值。 |
| 543 Diameter of Binary Tree | DFS 高度 | 每个节点用左右高度更新直径,返回单边高度。 |
| 560 Subarray Sum Equals K | 前缀和哈希 | 当前前缀为 cur,答案加 count[cur-k]。 |
| 581 Shortest Unsorted Continuous Subarray | 双扫描 | 从左找右侧最小破坏,从右找左侧最大破坏,确定边界。 |
| 617 Merge Two Binary Trees | DFS | 两树节点都在则值相加,否则返回非空节点。 |
| 621 Task Scheduler | 贪心公式 / 堆 | 由最高频任务决定空槽,答案是 max(n, (maxFreq-1)*(cool+1)+countMax)。 |
| 647 Palindromic Substrings | 中心扩展 | 每个字符和字符间隙作中心,扩到不等为止。 |
| 739 Daily Temperatures | 单调栈 | 栈存等待更高温的下标,遇更高温时弹出并填天数。 |
| 763 Partition Labels | 贪心 | 记录每个字符最后位置,扫描到当前段最大 last 时切分。 |
| 138 Copy List with Random Pointer | 哈希 / 织链 | map 原节点到新节点,或复制节点插入原链再拆分。 |
4. 看到题后的选择顺序
1. 需要“连续子数组/路径和”为 K? -> prefix sum + hashmap
2. 需要“最长/最短 substring”? -> sliding window
3. 已排序 / 可排序后找三元组? -> two pointers
4. 链表删除/合并/反转? -> dummy + pointer rewrite
5. 树题问路径/高度/祖先? -> DFS return value
6. 矩阵连通块? -> BFS/DFS + visited
7. 课程/依赖/先后关系? -> graph + cycle detection/toposort
8. 最优值/方案数/可行性? -> DP
9. 枚举所有方案? -> backtracking
10. Top K / 频率? -> heap / bucket / quickselect
5. DP 和回溯最容易丢分的点
DP 面试时先说四句话:
1. state: dp[i] / dp[i][j] 表示什么
2. transition: 从哪些旧状态转移
3. init: 空串、0、第一行第一列怎么设
4. answer: 返回 dp[n] 还是 max(dp)
回溯模板:
def backtrack(path, choices):
if is_done(path):
res.append(path.copy())
return
for choice in choices:
if invalid(choice):
continue
path.append(choice)
backtrack(path, next_choices)
path.pop()
6. 手写 Multi-Head Attention
面试必须讲清楚 shape:
x: [B, T, C]
q/k/v: [B, T, C] -> [B, H, T, D]
scores: [B, H, T, T]
out: [B, H, T, D] -> [B, T, C]
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
class MultiHeadAttention(nn.Module):
def __init__(self, embed_dim, num_heads, dropout=0.0, bias=True):
super().__init__()
assert embed_dim % num_heads == 0
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
self.k_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
self.v_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
self.dropout = nn.Dropout(dropout)
def _split_heads(self, x):
# [B, T, C] -> [B, H, T, D]
B, T, C = x.shape
return x.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
def _merge_heads(self, x):
# [B, H, T, D] -> [B, T, C]
B, H, T, D = x.shape
return x.transpose(1, 2).contiguous().view(B, T, H * D)
def forward(self, x, mask=None, return_attn=False):
# x: [B, T, C]
q = self._split_heads(self.q_proj(x))
k = self._split_heads(self.k_proj(x))
v = self._split_heads(self.v_proj(x))
scores = q @ k.transpose(-2, -1)
scores = scores / math.sqrt(self.head_dim)
if mask is not None:
# bool mask: True means masked out; 0/1 mask: 0 means masked out
if mask.dtype == torch.bool:
scores = scores.masked_fill(mask, float("-inf"))
else:
scores = scores.masked_fill(mask == 0, float("-inf"))
attn = F.softmax(scores, dim=-1)
attn = self.dropout(attn)
out = attn @ v
out = self.out_proj(self._merge_heads(out))
return (out, attn) if return_attn else out
常见追问:
- 为什么除以
sqrt(head_dim):防止 dot product 方差随维度变大,softmax 饱和。 - 为什么
contiguous():transpose后内存不连续,view前要整理。 - mask 放哪:softmax 前,把非法位置填
-inf。 - causal mask shape:
[T, T]或可 broadcast 到[B, H, T, T]。
7. 手写 1D Convolution
输入和权重:
x: [B, Cin, L]
weight: [Cout, Cin, K]
out: [B, Cout, Lout]
Lout = floor((L + 2P - D*(K-1) - 1) / S + 1)
import torch
import torch.nn.functional as F
def conv1d_naive(x, weight, bias=None, stride=1, padding=0, dilation=1):
B, Cin, L = x.shape
Cout, Cin_w, K = weight.shape
assert Cin == Cin_w
x = F.pad(x, (padding, padding))
Lpad = x.shape[-1]
Lout = (Lpad - dilation * (K - 1) - 1) // stride + 1
out = x.new_zeros(B, Cout, Lout)
for b in range(B):
for co in range(Cout):
for i in range(Lout):
start = i * stride
total = x.new_tensor(0.0)
for ci in range(Cin):
for k in range(K):
total = total + x[b, ci, start + k * dilation] * weight[co, ci, k]
if bias is not None:
total = total + bias[co]
out[b, co, i] = total
return out
验证:
torch.manual_seed(0)
x = torch.randn(2, 3, 12)
w = torch.randn(4, 3, 5)
b = torch.randn(4)
y = conv1d_naive(x, w, b, stride=2, padding=2)
ref = F.conv1d(x, w, b, stride=2, padding=2)
assert torch.allclose(y, ref, atol=1e-5)
8. 手写 2D Convolution
输入和权重:
x: [B, Cin, H, W]
weight: [Cout, Cin, KH, KW]
out: [B, Cout, Hout, Wout]
Hout = floor((H + 2PH - DH*(KH-1) - 1) / SH + 1)
Wout = floor((W + 2PW - DW*(KW-1) - 1) / SW + 1)
import torch
import torch.nn.functional as F
def _pair(v):
return v if isinstance(v, tuple) else (v, v)
def conv2d_naive(x, weight, bias=None, stride=1, padding=0, dilation=1):
stride_h, stride_w = _pair(stride)
pad_h, pad_w = _pair(padding)
dil_h, dil_w = _pair(dilation)
B, Cin, H, W = x.shape
Cout, Cin_w, KH, KW = weight.shape
assert Cin == Cin_w
x = F.pad(x, (pad_w, pad_w, pad_h, pad_h))
Hpad, Wpad = x.shape[-2], x.shape[-1]
Hout = (Hpad - dil_h * (KH - 1) - 1) // stride_h + 1
Wout = (Wpad - dil_w * (KW - 1) - 1) // stride_w + 1
out = x.new_zeros(B, Cout, Hout, Wout)
for b in range(B):
for co in range(Cout):
for oh in range(Hout):
for ow in range(Wout):
h0 = oh * stride_h
w0 = ow * stride_w
total = x.new_tensor(0.0)
for ci in range(Cin):
for kh in range(KH):
for kw in range(KW):
total = total + (
x[b, ci, h0 + kh * dil_h, w0 + kw * dil_w]
* weight[co, ci, kh, kw]
)
if bias is not None:
total = total + bias[co]
out[b, co, oh, ow] = total
return out
验证:
torch.manual_seed(0)
x = torch.randn(2, 3, 8, 9)
w = torch.randn(4, 3, 3, 5)
b = torch.randn(4)
y = conv2d_naive(x, w, b, stride=(2, 1), padding=(1, 2))
ref = F.conv2d(x, w, b, stride=(2, 1), padding=(1, 2))
assert torch.allclose(y, ref, atol=1e-5)
9. 三个代码题的面试讲法
| 题 | 先说什么 | 最容易错 |
|---|---|---|
| MHA | shape 流、scale、mask、softmax 维度 | mask 语义反了;view 前忘 contiguous |
| Conv1D | Lout 公式、stride/padding/dilation |
padding 方向、kernel index |
| Conv2D | Hout/Wout 公式、四层空间循环 + channel 累加 |
F.pad 参数顺序是 (left,right,top,bottom) |
10. 最后五分钟速记
Hot 100:
sum/count -> hashmap/prefix
substring -> sliding window
sorted pair/triple -> two pointers
tree -> define DFS return
graph/matrix -> visited + BFS/DFS
optimal value -> DP state first
MHA:
[B,T,C] -> [B,H,T,D] -> scores [B,H,T,T] -> softmax -> merge
Conv:
output size = floor((input + 2*pad - dilation*(kernel-1) - 1)/stride + 1)