ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

打造高效算法刷题本:从设计模板到知识体系

打造高效算法刷题本:从设计模板到知识体系

算法刷题本这三个字,拆开看不像什么新鲜事,不就是题目、讲解、代码三段式吗?但真能把这三块同时做扎实的人,我身边并不多见。大部分人的刷题记录,要么是一堆来不及看的题解链接,要么是粘贴板上贴了一整页代码却没留下任何思考痕迹。我自己这个刷题本前后折腾了快一年,从最初只会抄答案,到后来逐渐形成一套适合自己的整理方法,中间踩过的坑基本都踩了一遍。如果你也准备系统刷算法题,或者正在备技术面试、算法竞赛,那这篇总结应该能帮你少走一点弯路。

本文分享的重点不是某一道具体题目的答案,而是围绕“题目 + 题目讲解 + 代码实现”这套结构,讲清楚刷题本应该怎么设计、怎么分类、怎么写讲解、怎么写代码,以及后期怎么维护才能不烂尾、真正变成可复用的知识资产。适合准备面试的开发者、想冲竞赛的学生,也适合每一个刷题刷到迷茫、感觉“题做了很多但脑子还是空”的人。

1. 整体设计:刷题本不是题库,是写给自己的教材

1.1 刷题本要解决的真实问题

先说清楚刷题本到底在对抗什么。我复盘过自己刷题的前三个月,发现一个特别扎心的规律:一道题当时看懂了,两周之后再见,除了隐约记得“好像是用二分”,其他全还给答案了。这种“假学习”的现象不是个例,根源在于我们的大脑很容易把“看懂了别人的思路”误认为“自己会了”。所以刷题本的第一个职责,就是对抗遗忘,让每一道题都留下可检索、可复习的思考脚印。

第二个问题是思维碎片化。今天刷一道字典序问题,明天刷一道区间合并,如果只是按随机顺序记录,那刷到第两百题的时候,笔记里就是一堆彼此孤立的点,形不成线、更织不成网。真正有效的刷题本应该以专题为骨架,把同一种算法或者同一个数据结构的题放到一起,让题与题之间产生关联,这样复习的时候你看到的不只是一个题,而是一类题的通法。

第三个问题更现实:当你积累了三百道题之后,复习成本会高到惊人。如果没有统一的格式、清晰的标签和准确的难度标注,你根本不知道考前应该看什么。所以刷题本本质上是一份给自己用的“二次教材”,它的核心价值不是收藏,而是筛选、压缩、重组。我在设计整个笔记库时,给自己定了三条铁律:专题聚合大于随机记录、能讲给别人听才算真懂、随时能找到才算有效积累。后面所有模板和习惯,都是围绕这三条展开的。

1.2 整体目录结构与单题模板

有了定位,接下来就是结构设计。我的刷题本用 Markdown 加 Git 管理,目录长这样:

algorithm-notes/ ├── 00-template.md # 单题笔记模板 ├── 01-binary-search/ │ ├── README.md # 本专题总结与索引 │ ├── search-first-ge.md # 查找第一个大于等于 target 的下标 │ ├── peak-index.md # 山峰数组峰值查找 │ └── split-array-sum.md # 二分答案专题 ├── 02-two-pointers/ │ ├── README.md │ └── ... ├── 03-dynamic-programming/ │ ├── README.md │ └── ... └── 99-review/ ├── weekly-review.md # 每周随机点题复习 └── bug-list.md # 常错边界清单

每个专题文件夹下,第一份一定是 README,用来汇总这一专题里所有题的套路、易错点和“一句话思路”,相当于专题的压缩包。单题笔记则统一使用模板,我的模板经过三次改版,最终稳定成这个结构:

# 题目名称(自评难度:入门/中等/挑战) ## 考点标签 数据结构:数组 | 算法范式:二分查找 | 易错点:边界条件 ## 来源与场景说明 在线评测平台 / 某模拟项目 / 面试高频场景 ## 我的第一想法 (拿到题目后自己最初的思路,哪怕很笨) ## 卡壳点 (哪里卡住了,为什么卡住) ## 最优解法推导 (从暴力解到最优解的每一步优化,重点写动机) ## 边界条件 (空输入、单元素、溢出、重复值) ## 代码实现 (带关键注释的代码块) ## 复杂度分析 时间:O(?) | 空间:O(?) ## 复盘日期 YYYY-MM-DD(方便安排复习)

很多朋友看到这个模板的第一反应是“每道题都写这么多,太费时间了”。我的体会是:前三十题确实慢,但你一旦写熟,大部分字段只需要几分钟。而且真正值得写详细的不是每道题的全部内容,而是“卡壳点”和“最优解法推导”这两块,因为这两个字段记录的是思考过程,不是答案本身。复制粘贴答案没有任何成长,把思考过程写下来,才算把这道题变成自己的东西。

2. 选题与归类:刷题数量不等于知识密度

2.1 从“刷多少”到“怎么选题”的思维转变

我刚决定刷题时也犯过“数量崇拜”的毛病,总觉得刷到五百题、一千题就稳了。后来发现,这个目标本身就很有问题:刷题刷的是算法思维,不是题目编号。有些人刷三百题比另一些人刷八百题的效果好得多,差别就在于选题方式。我最开始是“看到什么刷什么”,热门榜单按顺序往下做,结果就是前一百个题里大量重复的简单题反复刷,难题和中档题反而没覆盖到,知识密度低得可怜。

后来我把选题策略改成了三维度控制:第一个维度是知识点覆盖度。核心算法和数据结构,每个至少要保证五到八道题,形成一定量。第二个维度是难度梯度。每个专题里入门、中等、挑战的比例控制在三比五比二左右。为什么挑战题只占两成?因为挑战题通常是多个套路组合,如果基础不牢直接上难题,大概率只能抄答案,收获反而不如把中等题吃透。第三个维度是形态多样性。同一个算法尽量在不同的载体上练,比如二分查找,不光要在数组里练,还要在旋转数组、答案区间、矩阵搜索里练,这样才能把“二分”从一道题升级成一种条件反射。

选题还有一个隐藏原则:优先选“暴力解到最优解有明显演进路线”的题。这种题最适合放进刷题本,因为它的讲解能写出完整的思考链条,而不是只会一句“这里套个模板就完事”。我在每个专题里都会刻意找两到三道这样的“示范题”,把它们写得很深,其他同套路的题则可以写得精简一些。

2.2 专题划分与标签体系的设计

选题解决了“刷什么”,接下来是“放哪”。我的分类体系是用两条轴交叉:一条轴是数据结构,包括数组、链表、栈和队列、哈希表、树、图、堆、字符串;另一条轴是算法范式,包括枚举、贪心、递归回溯、DFS/BFS、二分、双指针、滑动窗口、动态规划等。一道题在笔记里只归属一个专题文件夹,但通过标签系统可以跨专题引用。

标签是我后期才意识到的重要设计。没有标签的时候,我想找一个“考察了边界溢出”的题,得把所有笔记翻一遍。加了标签之后,一条查询命令就能筛出来。我的标签设计成这样:

标签维度常用示例作用
数据结构数组、链表、树、图决定用什么容器组织数据
算法范式二分、双指针、动态规划决定用什么策略解题
自评难度入门、中等、挑战用于安排复习优先级
状态标记已掌握、需复习、重点配合周复习做调度
易错点边界、溢出、重复元素用于错题回顾与 bug 清单

这个标签体系不用一开始就设计得特别完美,可以先随手打,然后每两周做一次“标签重排”,把所有没有标签或者标签混乱的题梳理一遍。重排的过程本身也是一次低成本复习,你会发现很多题当时打错标签,其实是思路没理清,重新归类之后你对这个算法的理解反而更清楚了。

再说一下专题划分的粒度。我的原则是宁粗勿细,一个专题放五到十五题就够了。太多说明分类过粗,太少说明分类过碎。比如“动态规划”这个专题我一开始拆成了“线性 DP”“背包 DP”“区间 DP”“树形 DP”四个文件夹,结果每个文件夹下只有两三道题,复习的时候反而失去整体感。后面我把它们合并成一个动态规划文件夹,用标签区分子类型,复习效率明显提升。

3. 题目讲解的写作方法:把“会做”变成“会讲”

3.1 一条合格的讲解要回答的四个问题

刷题本里的题目讲解,难点不在于字数多少,而在于视角。很多人的笔记是这样写的:“这题用二分查找,时间复杂度O(log n)。”没了。这本质上只是把题解里的一句话抄了过来,完全没有信息量。

一条真正有用的讲解,至少要回答四个问题。第一,这道题在考什么?是单调性、最优子结构,还是边界条件的精细控制?第二,最笨的办法是什么?很多人跳过暴力解直接写最优解,这是大忌,因为暴力解才是整个思考链条的起点,它能告诉你问题的天然复杂度在哪里。第三,暴力解卡在哪里?是重复计算太多、搜索空间太大,还是状态数量爆炸?这一句“卡点”就是优化的动机。第四,怎么一步步从卡点走到最优解?这才是我理解的“讲解”。

除了这四个问题,还有两个加分项:边界条件的系统性梳理,以及“为什么这个复杂度已经不能更低”的下界分析。这两点做得好,讲解的深度会直接上一个台阶。我在模板里加“我的第一想法”和“卡壳点”这两个字段,就是为了逼自己在看答案之前把前三个问题先过一遍,记录下真实的思维状态。哪怕第一想法是错的,这个记录也比事后抄一段官方解析有价值得多。

3.2 从暴力到最优:一道二分题讲解的完整示例

理论讲完,还是用一个具体例子演示一下我的讲解长什么样。就以“在一个升序数组中找到第一个大于等于目标值的位置”为例,这是一个很常见但又特别适合练手的二分变式。

这道题的题目复述是:给定一个升序排列的整数数组 nums 和一个整数 target,返回第一个满足 nums[i] >= target 的下标。如果不存在,则返回数组长度 n。我拿到题目后的第一想法是:直接从左往右扫一遍,遇到大于等于 target 就返回,走到底都没有就返回 n。这是最直觉的解法,时间复杂度 O(n)。卡壳点在于:怎么用二分?普通二分是找相等的目标值,但这个题找的是“第一个满足不等式的点”,边界该怎么取舍?

接下来是推导过程。因为是升序数组,数组天然具有单调性,所以可以用二分不断收缩区间。关键在于维护一个“不变量”:区间 [left, right) 内可能存在答案,且答案一定在右边界内。于是每一步比较 nums[mid]:如果它小于 target,说明 mid 及其左边都不可能是答案,把左边界收紧到 mid + 1;否则,mid 可能是答案,但不排除左边还有更小的答案,于是把右边界收到 mid。循环结束后 left 所指的就是第一个大于等于 target 的位置。

实现时有一个容易被面试追问的细节:为什么 mid 要写成 left + (right - left) / 2,而不是 (left + right) / 2?答案是防止两个大整数相加发生溢出。这在工程上是真实存在的风险,虽然刷题时数组长度可能不会那么大,但写成前一种形式是一种更安全的习惯。同理,右边界初始值取 n 而不是 n - 1,是为了同时处理好“答案不存在”的语义。边界用例也很清晰:空数组返回 0;所有元素都小于 target 返回 n;所有元素都大于等于 target 返回 0。复杂度自然是 O(log n) 时间、O(1) 空间。

复盘笔记里我还额外记了一笔:我第一次写这段代码时,把 right 的更新写成了 right = mid - 1,结果在数组长度为 1 的情况下直接死循环。这个错误特别典型,原因是二分模板的左闭右开区间和闭区间两种写法混用了。把这个错误记录在卡壳点里,比做十道新题都值。

3.3 一题多解与复杂度分析记录的加分写法

刷题本对同一道题如果存在多种经典解法,我通常会单独开一个小节做对比分析。拿“数组中出现次数超过一半的元素”这道经典题为例,讲解部分我会列三种解法:哈希表计数、排序取中位、摩尔投票。光列解法不算完,还要写出数组在三者之间切换的思考过程。

哈希表计数是最好想的思路,时间 O(n) 但空间也是 O(n),问题在于额外占用了存储。排序取中位则利用了“多数元素一定在中间位置”的性质,时间升到 O(n log n),但可以做到 O(1) 额外空间。摩尔投票的思路更妙,它让不同元素互相抵消,最后活下来的就是多数元素,时间 O(n)、空间 O(1),代价是需要你先理解“抵消”背后的不变量。

这类一题多解的笔记,我还会在末尾画一个对比表,列出解法、时间复杂度、空间复杂度、适用场景四列。比如哈希表适用于数据范围大但内存充足的场景,摩尔投票要求“存在出现次数超过一半的元素”这个前提,排序法不适合数据流场景。提醒一下,这里说的“画表”不需要用什么复杂工具,Markdown 表格足够,重点是逼自己思考解法之间的取舍关系,而不是把它们并列摆在一起就完事。这种对比思考,面试里被追问“为什么不用另一种解法”的时候特别管用。

4. 代码实现里的工程细节:不是能过样例就叫完成

4.1 代码结构和注释是留给未来的自己看的

刷题本里的代码,跟赛场上提交的代码是不同的物种。赛场上追求的是最短时间写对,而刷题本里的代码要承担“教学示范”的责任,三个月后的你要能一眼看懂并快速复用。所以我在代码画面上有四个硬性要求:命名自解释、关键分支必须有注释、核心函数单独拆分、不写炫技缩写。

命名自解释这点最容易被忽略。写题练习时变量名可以用 i、j、k,但放进刷题本的代码我会改成 left、right、mid、count、cur_sum 这种有明确语义的名字。很多同学觉得这是浪费时间,其实不然:一个清晰命名的版本,复习时根本不用读注释就已经能猜出代码意图,这才是高效复习。我推荐的模板是给核心函数写一小段头注释,注明输入输出和边界语义,然后在策略发生切换的关键分支写一两句注释,完全没有必要逐行注释。

举个例子,同样是二分边界处理,两个版本差别很大:

def find_first_ge(nums: list[int], target: int) -> int: # 返回第一个 nums[i] >= target 的下标,不存在则返回 n left, right = 0, len(nums) while left < right: mid = left + (right - left) // 2 if nums[mid] < target: left = mid + 1 else: right = mid return left

这段代码我在刷题本里还额外写了一句注释:“右指针定义成开区间,所以更新时 right 直接取 mid,不写 mid - 1。”看上去只是一句自言自语,但这句话就是曾经让我死循环的教训,写上之后,以后复习到这道题就再也不会踩同一个坑。

4.2 边界条件与自测用例的检查清单

代码能过样例不等于代码正确。我在这个刷题本里给自己强制加了一条规矩:每道题写完后,至少列出五个边界用例,然后逐个在脑子里跑一遍。这份边界检查清单我固定放在代码下方,不同数据结构题型会有不同侧重。拿数组二分类的题来说,我通常检查以下用例:

用例类型具体输入期望结果
空输入[],target=5返回 0
单元素[5],target=5返回 0
元素重复[1, 3, 3, 3, 5],target=3返回 1
目标在开头[2, 3, 4],target=2返回 0
目标不存在且位于区间内[1, 4, 6],target=5返回 2
全小于目标[1, 2, 3],target=9返回 3

这种测试清单的维护成本很低,但收益极高。我统计过自己刷题本的 bug 记录,大概四成错误都出在边界上,尤其是单元素、空输入、全部大于或全部小于这三类。把边界用例固化成习惯之后,很多 AC 之后才发现的隐藏错误,在写代码阶段就能被消灭掉。

另外还要提醒一个常见误区:很多人公布代码时会删掉自测部分,只留主函数。这其实很可惜。我建议把自测用例保留在代码块下方的“边界条件”小节里,这样复习时可以直接拿来构造测试,等于给未来的自己做了一轮回归测试。

4.3 与讲解联动:让代码和思路一一对应

代码不是孤立的存在,它要跟讲解部分形成联动。我常用的做法是在代码的关键行加数字标注,比如# ① 左指针更新、# ② 右指针保持开区间、# ③ 循环退出后 left 即答案,然后在讲解部分用同样的① ② ③去引用。这样看代码时能迅速定位到思路推导的位置,看思路时又能马上跳到对应实现,复习体验会顺很多。

联动还有一个层面是版本管理。我是用 Git 管理整个刷题本的,提交信息会写成类似“修改第X题:修正右指针更新逻辑,补充空数组用例”这样的格式。这个习惯听起来很工程师,实际操作起来很简单,却能帮你应付一个很常见的危机:某一天你发现新写法反而引入了 bug,需要退回上几版对比。没有版本记录的时候,这种回溯几乎不可能实现。我对自己的要求是每次改动只聚焦一个主题,commit 信息写清楚,这样整个刷题本的历史就是一份很完整的“算法自我进化史”。

5. 常见问题与维护技巧:刷题本从入门到不烂尾

5.1 高频问题速查表

刷题本这东西,最大的敌人不是内容难写,而是坚持不下去。我把自己在使用中遇到的典型问题整理成了一张速查表,如果你正在维护自己的刷题本,可以直接对照自查:

现象根源解决方案
刷完就忘笔记只抄题解,没有思考痕迹每道题强制写“我的第一想法”和“卡壳点”
讲解像翻译没有自己的卡壳记录,只是在复述先独立做一遍,再对照答案补充遗漏
代码过样例但出错边界用例覆盖不足按检查清单补空数组、单元素、溢出等用例
专题分类越来越乱一开始分类过细或没有标签每两周做一次标签重排
笔记越积越多不敢复习没有优先级,复习成本过高通过状态标签 + 自评难度安排复习顺序
坚持不下去目标定太大,每道题都长篇大论降低单题成本,只写关键内容,每天一题

这六类问题我基本都踩过。其中“讲解像翻译”这一点我认为最隐蔽,因为它表面上看很努力,实际上没有任何思考增量,甚至会让你的笔记变成一篇大型错题抄写现场。我在第三次改版刷题本时,把所有“读起来像是在复述题解”的段落全部删掉重写,换成自己推导时的真实犹豫,那一次删改之后整个笔记的质量完全不一样了。

5.2 让刷题本能被坚持维护的四个习惯

刷题本要长期维护,靠的不是意志力,而是让它成为一个低摩擦的习惯。我实践下来最有用的四个习惯,分享给你。

第一是“1+1”日任务。每天一个新题加一个旧题复盘。旧题复盘不是重做,而是只看“卡壳点”一栏,在脑子里把解题流程过一遍,如果卡壳点已经能顺畅想通,就把状态标签从“需复习”改成“已掌握”。这个动作每天只需要五到十分钟,但能让笔记里的状态数据一直保持新鲜,复习优先级永远准确。

第二是每周随机抽取。每周随机抽三到五道笔记里的题,遮住代码,只留题目复述,然后重新在纸上推导一遍。这个习惯的设计初衷是防止“笔记里写得漂亮,实际一上手全忘”的情况。随机抽取能打破你按顺序复习时的路径依赖,逼迫大脑从零开始建立思路。

第三是每月一次的专题 README 更新。每当一个专题新增了一些题,就刷新这个专题的 README,用半页压缩该专题的通法:适用条件、关键模板、常见卡壳点。这个月更动作是刷题本从“做题记录”升级为“知识地图”的关键一步。

第四是给自己设“烂尾保险”。如果哪一天实在不想写完整讲解,那就允许自己只写一个“一句话题解”占位,但要求必须在三天内补上完整版。这道保险极大地降低了每天的心理门槛,因为最小行动成本够低,坚持反而更容易。

5.3 把笔记变成考前冲刺材料

等刷题本积累到一定量级,它还有一个非常实际的用途:变成考前两小时的冲刺材料。方法是在每个专题的 README 里维护一张“必背索引表”,大概长这样:

考点路径最佳解法复杂度一句话思路
查找首个大于等于目标值二分(左闭右开)O(log n) / O(1)不断收缩存在答案的区间
查找多数元素摩尔投票O(n) / O(1)不同元素抵消,剩余即多数
区间合并排序 + 贪心O(n log n) / O(log n)按起点排序后依次合并重叠区

考前我只看这两页表格,不看完整的几百道题。这张表上的“一句话思路”写得好不好,最能检验你对这道题的理解深度。如果你发现自己一分钟内想不清楚那句话在说什么,那说明原题笔记中的讲解还需要修正,这本身就是一次高质量的复习反馈。把这张表维护好,刷题本就从“学习工具”变成了“应试地图”,价值完全是另一个量级。

6. 从刷题本到个人知识体系:下一步扩展

6.1 从单题笔记到专题总结

当某一个专题积累了足够多的题目之后,我会发现很多解法之间共享着同一套思维框架,这时候就应该从“逐题记录”提升到“专题总结”。以二分查找为例,做完十几道题之后,套路就开始浮现:先判断问题是否具有单调性,再确定搜索区间形态是闭区间还是左闭右开,接着写出 while 循环和边界更新逻辑,最后单独处理“不存在”的情况。把这些共性提炼出来,写进专题 README,就形成了一套属于你自己的“解题框架”。

框架形成之后的效果是很明显的:以后遇到同类的新题,你不再是从零开始摸索,而是先在框架里定位它的不同点,然后针对不同点做局部调整。我在读完自己写的二分专题总结之后再看新题,“这道题其实就是二分套上了一层额外条件”的识别速度明显加快。这就是从刷题本过渡到知识体系的第一步:题量积累到一定程度,总结沉淀出框架,再用框架去指导新题。

6.2 从刷题本到模板库与面试表达

最后一步扩展,是把刷题本里反复出现的代码模式,抽成一份独立算法模板库。模板不是抄代码,而是把核心流程的骨架和变式都写清楚。还是拿二分举例,模板库分为三段:标准二分、第一个满足条件的二分、答案二分。每个模板都标注适用前提、边界更新规则、常见陷阱。这样做的好处是,当面对新题时,你脑子里装的不是五十道零散的题,而是十几个可以灵活变换的模板。

模板库和刷题本之间的关系是:刷题本负责记录具体题目的推导和错误,模板库负责承载抽象出来的通法,两者互相引用。我在面试季做模拟练习时,还有一个附加动作:拿着模板库里的模板,不看任何资料,用三分钟口头讲解一遍模板的思路、复杂度和边界。这个过程本质上是把“会写”转化成“会讲”,而会讲才是面试真正考察的能力。把刷题本维护到这个程度,它就不再是一份考古记录,而是一套能持续进化的个人算法知识系统。

最后再分享一个小技巧。我写这个刷题本最大的转折点,发生在某一天我删掉了将近一半的笔记——那些只有代码、没有任何讲解痕迹的笔记,留着只会让我产生“我已经刷了很多题”的错觉。删除之后,刷题本瞬间变薄,但每次翻开它,我都会感觉每一条记录都有持续产出的能力。这个过程让我真正体会到:刷题本的价值从来不在数量,而在密度。哪怕你一个月只整理十道题,只要每一道都能回答清楚它考什么、怎么优化、边界在哪、当初卡在哪,这十道题带给你的成长也会远超三百道剪贴板里的题。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进