基础算法

先推演形成思路后再实现
5最小 coding
3系统路径索引
3项目事实索引
基础 coding 是当前主线,每次只选一项。先明确题意并在纸上完成推演,形成可以解释的思路后再写代码。系统路径和项目材料只在真实面试触发时调用,不背稿、不打卡;不扩题库、不追竞赛最优解、不新增学习平台。

当前节奏:不设单题完成期限,同一道题可以跨多次推演。实现用于验证完整思路,不通过即时反馈逐个猜测和修补 case;单次结果不外推为长期能力判断。

当前验收:先检查题意、返回值和关键搜索状态是否一致;命名、注释、goto、代码长度、性能和测试框架暂不处理。单题正确性站住后可以进入下一题,结构优化与自动测试统一在五题完成后回看。

第五题:LRU Cache

用 Go 实现一个固定容量的 LRU Cache:

type LRUCache struct {
    // 字段自行设计
}

func NewLRUCache(capacity int) (*LRUCache, error)
func (c *LRUCache) Get(key int) (int, bool)
func (c *LRUCache) Put(key int, value int)

范围:单线程、纯内存;可以使用 Go 标准库,不使用第三方库。不扩 TTL、持久化、并发、分片和容量动态调整。

基本用例:

cache, _ := NewLRUCache(2)

cache.Put(1, 10)
cache.Put(2, 20)
cache.Get(1)       // 10, true;1 成为最近使用
cache.Put(3, 30)  // 淘汰 2
cache.Get(2)       // 0, false

cache.Put(1, 11)  // 更新已有 key,1 成为最近使用
cache.Put(4, 40)  // 淘汰 3
cache.Get(1)       // 11, true
cache.Get(3)       // 0, false
cache.Get(4)       // 40, true

还需自行覆盖:容量为 1、重复更新同一 key、读取不存在的 key,以及构造容量为 0 或负数。

验收边界:解释每一步之后谁是最近使用、谁是最久未使用;跑通上述用例和四类边界;说明复杂度。先保证正确性,不扩展功能。

2026-07-22 练习记录:已用 map[int]*Node 与手写双向链表完成固定容量 LRU;链表头表示最久未使用,链表尾表示最近使用,命中、更新与淘汰路径均保持平均 O(1)。第一版运行题面主样例符合预期。

review 修正:第一版没有拒绝非正容量,删除唯一节点时也会解引用空的头尾指针。随后补上构造参数校验,并让删除操作覆盖唯一节点、头、尾和中间节点;容量为 1 的命中路径实际运行得到 10, truego vetgofmt 检查通过。

当前边界:修正后没有重新执行完整主样例,也没有继续运行重复更新、未命中不改变顺序、容量 0 与负数等其余边界。静态检查未发现新的正确性问题,但这些 case 不记为已经通过;本题按当前决定停止追加验证,阶段结束。

学习材料:链表:从指针到索引,从本题的唯一节点边界展开到环形链表和跳表。

第四题:Token Bucket

用 Go 实现一个由调用方传入逻辑时间的单机令牌桶:

type TokenBucket struct {
    // 字段自行设计
}

func NewTokenBucket(
    capacity int,
    refillPerTick int,
    startTick int64,
) (*TokenBucket, error)

func (b *TokenBucket) Allow(nowTick int64, n int) bool

本题中的 tick 只是单调递增的整数时间单位。不要调用 time.Now(),也不要用 sleep 驱动测试。

纸上样例:

b, _ := NewTokenBucket(5, 2, 100)

b.Allow(100, 3) // true,剩余 2
b.Allow(100, 3) // false,仍为 2
b.Allow(101, 3) // true,先补到 4,再消费,剩余 1
b.Allow(103, 5) // true,先补到容量上限 5,再消费,剩余 0
b.Allow(103, 1) // false,仍为 0
b.Allow(102, 1) // false,时间倒退,状态不变
b.Allow(104, 0) // false,无效请求,状态不变

需要自行补出的最小用例:构造参数非法;空桶后逐步恢复;长时间后只补到容量上限;一次请求恰好耗尽;请求量大于容量。

验收边界:能逐次写出 tokens 和上次更新时间的变化,并说明 capacity 为什么允许突发、refill rate 为什么限制长期速率。

停止条件:独立推演并跑通上述边界后停止。不扩真实时钟、浮点速率、并发锁、分布式限流、持久化或动态配置;不设完成期限。

2026-07-22 练习记录:约 30 分钟写出第一版,题面样例运行符合预期,但因为急于先交付,遗漏构造参数校验,并把“补充令牌”写回对象、却只在消费成功时更新时间,形成令牌与时间检查点的部分提交;失败后再次调用可能重复计算同一段时间。review 后补上参数校验,并改为先在局部变量计算候选状态,只有消费成功时才同时提交令牌和时间。

第三题:Producer / Consumer

用 Go 实现:

func processJobs(
    jobs []int,
    workers int,
    queueSize int,
    handle func(int) int,
) ([]int, error)

一个生产者提交任务,多个消费者并发调用 handle。本题只处理内存中的有限任务,不涉及网络、持久化或分布式队列。

样例:

square := func(v int) int { return v * v }

processJobs([]int{3, 1, 2}, 2, 1, square)
// []int{9, 1, 4}, nil

processJobs([]int{2, 2, 3}, 3, 0, square)
// []int{4, 4, 9}, nil

processJobs([]int{}, 2, 1, square)
// []int{}, nil

processJobs([]int{1}, 0, 1, square)
// nil, error

processJobs([]int{1}, 1, -1, square)
// nil, error

验收边界:先证明任务没有丢失、重复或错位,并能解释生产者、消费者、队列和关闭责任分别属于谁。不要求取消、超时、重试、错误聚合、优先级或动态扩缩容。

停止条件:本轮先以核心时间线跑通并能解释每个关闭与等待关系为界。关闭参考后的空白重建、自动测试和结构优化统一留到五题完成后回看。

2026-07-21 练习记录:开始实现 guard/cmd/jobs/main.go。第一版把 workers 个 goroutine 各执行一次接收,日志只出现两次;由此确认自己把旧代码中 workerpool.Submit / StopWait 提供的队列、持续消费和等待语义,误认为是 WaitGroup 自带的能力。查看了旧实现,但没有复制代码。

提示边界:在请求展开上述两个问题后,已经直接获得下面的架构级关系;这部分不再记为独立推演。

input producer:发送全部任务,然后关闭 in
workers:持续读取 in,处理后发送 out,结束时 Done
output closer:等待所有 workers,再关闭 out
collector:持续读取 out,按 ID 归位;range 结束后才允许函数返回

当前边界:channel close 只表示以后不会再发送,不表示 receiver 已经处理完成;有界队列满时发送方会阻塞,输入生产和输出消费如果被串成两个阶段,可能形成循环等待。核心时间线已经在提示后跑通,本轮进入下一题;空白重建与结构复核留到五题完成后。

首次基线:Log Top N

读取若干日志行,跳过坏行,按 service 计数,输出出现次数最多的前 N 个;次数相同时按名称排序。

结果:90 分钟完成。主逻辑独立实现,使用普通补全;经反例和 review 修正题意、并列截断、EOF 与参数边界,最终通过核心边界测试并能解释复杂度。

结论:首次运行暴露了题意复述、反例先行、标准库熟练度和边界验证四个缺口。保留实际完成过程,不再把 30 分钟完成作为当前学习目标。

第二题:BFS 最短路径

用 Go 实现:

func shortestPath(
    graph map[string][]string,
    start string,
    target string,
) ([]string, bool)

graph 是有向、无权图,邻接节点的顺序有意义。

当前题意解释:按普通图语义处理:同一来源到同一目标的重复邻接记录不增加新的可达关系;不同来源到同一节点属于合法合流,不能全局去重。

graph := map[string][]string{
    "A": {"B", "C"},
    "B": {"D"},
    "C": {"D"},
    "D": {},
}

shortestPath(graph, "A", "D") => [A B D], true
shortestPath(graph, "A", "A") => [A], true
shortestPath(graph, "D", "A") => nil, false

当前只保留的 Stack / Queue 语法:

// Stack
stack = append(stack, v)
v = stack[len(stack)-1]
stack = stack[:len(stack)-1]

// Queue
queue = append(queue, v)
v = queue[head]
head++

这些代码只表示两种容器的最小进出操作,不是本题解法。

已通过反例 1:分支中的最短路径

graph := map[string][]string{
    "A": {"B", "C"},
    "B": {"X"},
    "C": {"D"},
    "X": {"D"},
    "D": {},
}

start = "A"
target = "D"
当前结果:A -> C -> D
最短路径:A -> C -> D

已通过自造用例:先经过死路,再找到有效路径

graph := map[string][]string{
    "A":  {"B0", "B", "B1"},
    "B0": {"X"},
    "B":  {"C"},
    "C":  {"D"},
    "D":  {"E"},
    "E":  {},
    "B1": {"Y"},
    "X":  {},
    "Y":  {},
}

start = "A"
target = "E"
当前结果:A -> B -> C -> D -> E, true

已通过单一反例:重复边不产生新的搜索状态

graph := map[string][]string{
    "A": {"B", "B"},
    "B": {"C"},
    "C": {"D"},
    "D": {"E"},
    "E": {},
}

start = "A"
target = "E"
期望结果:A -> B -> C -> D -> E, true
当前结果:A -> B -> C -> D -> E, true
当前行为:第二条 A -> B 由已发现状态跳过,不再手动 strip 邻接表。

已通过反例:合流导致重复抵达

graph := map[string][]string{
    "A": {"B", "C"},
    "B": {"D"},
    "C": {"D", "X"},
    "D": {},
    "X": {"E"},
    "E": {},
}

start = "A"
target = "E"
期望结果:A -> C -> X -> E, true
此前错误队列形态:D, D, X
当前队列形态:D, X
当前结果:A -> C -> X -> E, true
当前行为:D 首次被发现时进入 Queue 并记录前驱;C 再次抵达 D 时跳过,X 仍按原顺序进入 Queue。

补课入口:图:从结构到搜索。先补图与树、重复边与合流、入度与出度、节点生命周期和控制动作作用域。

当前边界:已经明确重复记录应由搜索状态处理,但当前实现仍用前驱表和展开表共同判断,尚未把“已发现”和“已展开”统一成稳定不变量。

已通过反例 2:图中存在环

graph := map[string][]string{
    "A": {"B"},
    "B": {"A"},
    "D": {},
}

start = "A"
target = "D"
当前行为:能够结束并返回 nil, false;2026-07-20 版本也已避免 A 经 B 回到 A 时再次入队。

已完成纸上复核:起点自环不再重复入队

graph := map[string][]string{
    "A": {"A", "B"},
    "B": {},
    "D": {},
}

start = "A"
target = "D"
黑盒预期:nil, false
白盒预期:
  入队:A, B
  展开:A, B
当前纸上推演:
  入队:A, B
  展开:A, B
当前行为:展开 A 时直接忽略 A -> A;返回 nil, false。
验证边界:专项分支已经加入当前实现,但该 case 尚未固化成自动测试。

结构停车点:当前实现用前驱表、展开表和自环特判共同维持搜索状态,正确性已经覆盖现有黑盒样例与自环纸上推演;“节点入队时即标记为已发现”的统一不变量、状态精简和代码结构调整留到五题完成后处理。

2026-07-13 记录:首次独立思考 30 分钟后未完成;第二版写出了基于 Stack 的遍历,简单样例通过,但它是 DFS。

2026-07-14 记录:第三版接入了 Queue 的最小语法,并通过递归拼出连续路径;实际搜索仍由递归 DFS 推动,最短路径和有环反例尚未通过。当前继续独立完成,不查解法;需要时用纸笔推演,到达单次时间上限后停止。检查点、反例和验证结果用于校准过程,不记录修改方案。

2026-07-14 第二次记录:第四版移除递归,由 Queue 推进访问并加入访问记录;当前 6 个样例通过 5 个,起终点相同、不可达、简单环和单链路径已通过。分支反例仍返回 A -> B -> C -> D,不满足最短路径 A -> C -> D。30 分钟到点后停止,没有查看完整答案。

方法复盘:在没有先定义各份状态含义的情况下进入实现,后续转为逐个修补 case,偏离了先推导状态语义的顺序。当前继续先解释同一节点重复到达时各份状态仍表示什么,再决定是否写代码。

2026-07-15 调整:停止即时训练,不再要求一开始就写代码或在限定时间内完成。当前目标只保留一个:最终独立推演出完整思路,再用一次实现验证;完成时间随学习过程决定。

2026-07-16 记录:第五版已由 Queue 推进访问,分别记录访问状态和路径来源,并能从终点反向还原路径;原分支反例与自造分支图已通过。当前正确性缺口收敛到重复边和重复到达时的状态处理,尚未查看标准答案。

2026-07-16 第二次记录:已区分同一来源的重复边与不同来源的合法合流;前者通过稳定去重的单一反例,后者暴露出此前一直用树的心智模型理解图。曾加入队尾条件让两个样例通过,但无法从不变量解释,未接受为答案。当前停止硬改,先补最小图模型;已建立交互式教程,仍未查看标准实现。

2026-07-20 记录:当前 9 组样例输出均符合预期,分支、死路、重复边、合流和简单环已经进入同一版实现;不再通过 strip 修改邻接输入,开始用搜索状态处理重复抵达。新增展开状态后,A -> B -> A 不再重复入队;自环仍暴露状态写入时机的缺口。继续先收敛正确性和状态解释,再处理结构简化,尚未查看标准实现。

2026-07-20 第二次记录:已为起点自环增加专项处理,纸上白盒推演恢复为 入队 A, B;展开 A, B。BFS 的正确性阶段到此结束,进入下一题;当前实现不冒充最终标准结构。五题完成后,以 table-driven test 固化分支、不可达、重复边、合流、普通环和自环,再统一“已发现 / 已展开”状态并回收结构债。

补充记录:1 / 2 / 5 / 10 过桥

四个人过桥分别需要 1、2、5、10 分钟;一次最多两人同行,必须携带同一只手电,同行耗时取较慢者。

首次答案:5 + 1 + 10 + 1 + 2 = 19,可以完成但不是最优。

1、2 过去:2
1 回来:1
5、10 过去:10
2 回来:2
1、2 过去:2

总计:17

当前处理:先记住最优答案和步骤,不继续追问证明,不把它扩成新的训练线。

5 个最小 coding

覆盖常见数据结构与基本遍历方法

题型最小动作口述检查点停止条件结果
LRU Cache 实现固定容量的 Get / Put,维护最近使用语义。 每次操作怎样改变最近 / 最久使用顺序,以及如何满足平均 O(1) 跑通题面与边界用例;不扩 TTL、并发和分片。 阶段结束 · 主样例第一版通过;review 后修正非正容量与唯一节点删除,容量 1 路径通过;其余边界未继续执行
Token Bucket 实现 allow(n),包含 capacity、rate、tokens 和 last_refill。 突发容量与长期速率如何分开,时间推进和边界值怎么处理。 能解释并跑通边界值;不扩分布式限流。 阶段完成 · 约 30 分钟完成第一版;review 后修正参数校验和部分提交,保留延迟结算模型;题面样例通过,专项反例尚未固化为自动测试
Producer / Consumer 用 channel 或 queue 表达生产、消费、关闭与 backpressure。 谁拥有队列,满了怎么办,关闭时消费者如何退出。 核心时间线跑通后进入下一题;五题完成后再空白重建和补测试,不扩取消、重试和复杂调度。 阶段完成 · 经架构提示跑通 producer、workers、output closer 与同步 collector;已看见 backpressure、关闭协议和返回边界,尚未空白重建
BFS 最短路径 按本页第二题题面实现 shortestPath 完成后再记录思路和复杂度,不提前放入实现提示。 正确性阶段结束后进入下一题;五题完成后用 table-driven test 固化反例,再处理状态与结构优化。 阶段完成 · 优化停车
当前 9 组可执行样例输出通过;Queue、前驱还原、分支、死路、重复边、合流和简单环已进入同一版实现。起点自环完成纸上白盒复核并加入专项处理,尚未固化为自动测试;未获取标准实现。
Log Top N 解析简单日志,按 key 计数并输出 top N。 HashMap、排序或 heap 的取舍;数据大时如何流式处理。 处理空行、坏行和相同计数;不扩日志平台。 已完成 · 90 分钟
主逻辑独立实现;普通补全;反例与 review 后通过边界测试。
O(L + S log S) / O(S)

五题之后的系统候选:Raft 与复制状态机。它不是第六道 coding;若五题完成后仍需要,先从三节点选主、日志复制、多数派提交和分区恢复的状态推演开始,不承诺实现完整协议。

3 个系统路径

保留入口,按真实面试触发口述

主题最小路径必须讲清停止条件结果
文件写入到落盘 用户缓冲区 → syscall → VFS / 文件系统 → page cache → block layer → 驱动 / 设备。 write 返回与真正 durable 的区别,fsync、缓存、错误和潜在瓶颈。 5 分钟画清主路径并指出 3 个失败点。 需要时口述
应用数据到网卡 应用 → socket → TCP / UDP → 路由 / netfilter → qdisc → 驱动 → NIC;接收方向反向说明。 复制、缓冲、分段 / offload、丢包位置,以及 tcpdump 看不到什么。 5 分钟画清收发路径并给出排障顺序。 需要时口述
进程与容器隔离 虚拟内存与用户态 / 内核态;namespace、cgroup、capability、seccomp;容器共享宿主机内核。 资源限制和安全隔离不是一回事,容器与 VM 的边界差异。 5 分钟讲清机制,不展开实现一个容器运行时。 需要时口述

项目事实索引

已有产出负责自解释,现场不背标准稿

项目自然入口事实检查现场边界状态
relay-rs 为什么做;中心协调、直连与回退如何组合;哪些路径已经真实使用。 说清个人项目、AI 辅助实现、health check 等未完成边界,不包装成生产级 VPN。 从真实使用或网络路径进入;需要时展开,不背稿。 已有公开材料
NDC 旧系统迁移为什么需要 oracle、A/B、客户端验证、后端读回和容量对照。 明确 55k / 60k 是本地合成 TLS 容量对照,不外推成生产在线规模。 从迁移判断或语义等价进入;需要时展开,不背稿。 已有公开材料
toolgate / workflow 统一入口解决谁能进入;job、runner、event timeline、report 解决进入后如何被系统接住。 区分已实现事实与 workflow governance 的脱敏抽象,不把一次内部工具说成通用平台。 从入口治理或任务状态进入;需要时展开,不背稿。 已有公开材料
具体面试邀约只用于调整顺序,不改变当前学习方式;没有邀约时,继续按既定小分类记录基础练习。