arganzheng · 2026-10-05
中等难度的面试题背后只有十来种模式,每种模式有一个可以背下来的骨架、两三处必错的边界、一组典型的追问;AI 岗的手撕题不考识别,考对组件的理解是否精确到能写出来、并且知道怎么验证。
| 算法篇 01–13 | AI 手撕篇 14–19 | |
|---|---|---|
| 主线 | 识别信号 → 模板 → 主讲题推演 → 变式与追问 → 两种语言的坑 | 面试怎么出题 → 形状推演 → 从零实现 → 与 torch 对拍 → 数值与形状陷阱 → 追问 |
| 共用方法 | 先说清骨架与不变量,再写代码,写完用最小的边界输入走查 |
%%{init: {"flowchart": {"wrappingWidth": 200}}}%%
flowchart TB
Q{"题面里出现什么?"}
Q -- "子数组 / 子串 / 配对" --> A["全正数 → 滑动窗口(02)<br/>含负数 → 前缀和 + 哈希(01)<br/>下一个更大 → 单调栈(03)"]
Q -- "链式 / 树形 / 图" --> B["链表:哑节点、快慢指针(04)<br/>树:返回什么 vs 更新什么(05)<br/>图:BFS / DFS / 拓扑 / 并查集 / Dijkstra(06)"]
Q -- "求最值 / 可行性 / 方案数" --> C["单调谓词 → 二分(07)<br/>第 K → 堆(08)<br/>枚举所有 → 回溯(09)<br/>最后一步可分解 → DP(11、12)"]
Q -- "设计一个支持若干操作的结构" --> D["两个结构互相索引(13)"]
| 篇 | 一句话骨架 | 必错边界 |
|---|---|---|
| 01 哈希与前缀和 | 「把见过的记下来」:(\text{sum}(i, j] = \text{pre}[j] - \text{pre}[i]) | count[0] = 1;原地哈希值 v 放下标 v − 1 |
| 02 双指针与滑动窗口 | 单调性:右扩不会让违规变合法、左收不会让合法变违规 | 含负数的「和 = k」不满足——用前缀和 |
| 03 单调栈与单调队列 | 弹出时结算:被弹出者的右侧第一个更大 = 当前元素、左侧 = 新栈顶 | 两端哨兵 0;窗口最值用单调队列 O(n) 不用堆 O(n log k) |
missing 计数使每步 O(1);接雨水水位 = min(leftMax, rightMax)| 篇 | 一句话骨架 | 必错边界 |
|---|---|---|
| 04 链表 | 哑节点统一头特判;三指针反转先存 nxt;快慢指针找中点 / 判环 |
Floyd (a = (k-1)c + (c-b));归并切分 fast = head.next |
| 05 二叉树 | 返回给父节点的是能继续向上延伸的链,答案在拐点处用全局变量更新 | 层序先记 len(q);BST 验证带上下界 (lo, hi) 不是比父子 |
| 06 图 | 五个算法:BFS、DFS、Kahn、并查集、Dijkstra;BFS 入队时标记 | Kahn 出队数 < n 即有环;Dijkstra 堆 + 懒删除 O(E log E) |
| 篇 | 一句话骨架 | 必错边界 |
|---|---|---|
| 07 二分 | 只有一个模板:在「假假假真真真」的单调谓词上找第一个真;[lo, hi)、真收 hi = mid、假收 lo = mid + 1 |
「有序数组找值」只是谓词 a[i] >= t 的特例;答案二分 O(n log V) |
| 08 堆、Top-K、区间、贪心 | 第 K 大用大小为 K 的最小堆;区间按起点排一趟扫、选最多不重叠按终点;贪心靠交换论证 | 双堆先进 small 再倒;会议室 = 起点排序 + 结束时间最小堆 |
| 09 回溯 | 做选择 → 递归 → 撤销;排列用 used[]、组合用 start |
out.append(path[:]) 必须拷贝;剪枝不改量级:排列仍 O(n · n!) |
| 篇 | 一句话骨架 | 必错边界 |
|---|---|---|
| 10 字符串 | 中心扩展(2n − 1 个中心)、KMP lps、状态机解析、竖式 res[i + j + 1]、a + b vs b + a |
atoi 溢出在乘 10 之前判 |
| 11 DP(一) | 五步法:定义状态(「前 i 个」还是「以 i 结尾」)→ 枚举最后一步 → 初始 → 顺序 → 答案 | LIS tails 只有长度是对的、不是子序列;INF = amount + 1 |
| 12 DP(二) | 倒序 = 一次、正序 = 无限;区间枚举最后被处理的元素;状态机画图再写 | 组合数外层物品、排列数外层容量(LC 377);hold 初值 −∞ |
| 13 设计题 | 单一结构做不到的复杂度用两个结构互相索引:哈希 → 双向链表(LRU)、频次桶 + min_freq(LFU)、数组 + 值到下标(O(1) 随机集) |
LFU min_freq 最多 +1 或重置为 1;树状数组 lowbit |
结论:multi-head 的四次形状变换——先 reshape(B, T, H, d) 再 transpose;softmax 减最大值;mask 用 −∞ 或 finfo.min(0 经 softmax 仍有权重、fp16 下 −1e4 不够小);online softmax 最大值变时重缩放;KV cache 缓存 K、V 不缓存 Q。
q = self.wq(x).view(B, T, H, d).transpose(1, 2) # [B, H, T, d]
att = (q @ k.transpose(-2, -1)) / math.sqrt(d) # [B, H, T, T]
att = att.masked_fill(mask == 0, float('-inf')).softmax(-1)
y = (att @ v).transpose(1, 2).contiguous().view(B, T, D) # 拼回去要 contiguous
| 账 | 数 |
|---|---|
| 参数 | (4D^2);FLOPs (8TD^2 + 4T^2D) |
| 每 token KV | (2LH_{kv}d \cdot) bytes:Llama-3-8B 128 KB;GQA 缩 (H/H_{kv}) 倍 |
结论:一层参数约 12D²(attention 4 + FFN 8);每参数一次乘加 → 前向 2N、训练 6N;CE 的梯度就是 p − y;反向 = 局部导数 × 上游梯度,写完用形状自查;micrograd 里梯度累加要 +=。
| 量 | 数 / 公式 |
|---|---|
| GPT-2 small | 124,439,808 |
| LayerNorm 反向 | (\frac{1}{\sigma}\big(d\hat x - \overline{d\hat x} - \hat x\,\overline{d\hat x \odot \hat x}\big)) |
| softmax + CE 反向 | (p - y) |
| 残差 | 梯度直接相加——所以深了能训 |
结论:BPE 编码按 merge 顺序而不是贪心最长匹配,tie-break (频次, 字典序);采样流水线 penalty → T → softmax → top-k → top-p → min-p;投机解码的输出分布恰为 p。
| 边界 | |
|---|---|
| top-p | 保留首个越界项 |
| 蓄水池采样 | 第 i 个以 k / i 替换 |
| 投机接受率 | (1 - \text{TV}(p, q)) |
结论:论文公式落成十行正确代码——DPO 四个序列 log 概率、初值 ln 2;GAE 从末尾递推 (A_t = \delta_t + \gamma\lambda A_{t+1});PPO 取 min;AdamW 衰减解耦(加进梯度的是 L2,会被 (\sqrt{\hat v}) 归一化);LoRA B = 0 初始化。
# AdamW:衰减不进 m、v
m = b1*m + (1-b1)*g; v = b2*v + (1-b2)*g*g
p -= lr * (m/(1-b1**t)) / ((v/(1-b2**t)).sqrt() + eps)
p -= lr * wd * p # 解耦:直接加在更新里
结论:模型 = 目标 + 优化——逻辑回归梯度 (X^\top(p - y)/n)、k-means 每步目标单调不增所以收敛;AUC = (正样本秩和 − (n_+(n_+ + 1)/2))/ (n_+ n_-),O(n log n);conv = im2col + GEMM。
| 量 | 公式 |
|---|---|
| NDCG 折扣 | (1/\log_2(i + 1)) |
| 卷积输出 | (H_{out} = \lfloor (H + 2p - k)/s \rfloor + 1) |
| im2col | 大 (k_h k_w) 倍 |
结论:一把锁保护什么、条件变量为什么 while——get 也要锁;两个条件变量各叫各的;异常进 Future;内存池的空闲链表嵌在块内;分块要测了再说。
| 题 | 一句话 |
|---|---|
| 线程安全 LRU | get 也要锁——它改链表顺序 |
| 有界阻塞队列 | not_empty / not_full 两个条件变量,while 防虚假唤醒 |
| 线程池 | 异常进 Future 不吞掉 |
| ring allreduce | 每 rank (2\frac{N-1}{N}V),与 N 无关 |
| 矩阵乘法 | ikj 比 ijk 快 11 倍;n = 512 时分块只加循环开销 |
| paged KV | fork 只加引用、写时复制一块 |
| 线 | 落点 |
|---|---|
| 识别信号 → 模式 | 一道题落在两个分支是常态(接雨水:双指针 / 单调栈;第 K 大:堆 / 快速选择;LC 287:原地哈希 / Floyd / 值域二分)——这正是追问的来源 |
| 不变量思维 | 窗口「每个 right 处合法」、对撞「被排除的一侧不可能更优」、tails「严格递增、tails[k] 是长度 k+1 的最小末尾」、双堆「small ≤ large 且大小差 ≤ 1」 |
| Python 与 Java 的差异 | 溢出(long、lo + (hi − lo)/2、乘 10 之前判);容器(不用 java.util.Stack、int[] 不能做 key、[[0]*n]*m 是同一行) |
| AI 手撕的方法论 | 形状推演 → 从零实现 → 与 torch 对拍(allclose 的 atol / rtol)→ 数值陷阱(−∞、减最大值、fp16)→ 追问(账:参数、FLOPs、KV) |
tails 就是一个 LIS」——只有长度对finfo.min/coding-interview.html;通关自测 23 题在系列总结