1. 算法课笔记的整理逻辑与核心价值
1.1 为什么算法笔记值得花时间系统整理
很多人学算法的时候有个误区,觉得刷题量上去了就行,笔记什么的都是形式主义。我一开始也这么想,直到后来回头翻自己以前刷过的题,发现大部分都只剩一个模糊印象,具体思路、边界条件、复杂度分析全忘光了。这时候才意识到,算法学习如果没有一套自己的笔记体系,刷再多题也只是在沙滩上写字,潮水一冲就没了。
算法课笔记的核心价值不在于“记录”,而在于“压缩”和“索引”。一本好的算法笔记,本质上是你把课程内容、自己的理解、踩过的坑、以及可复用的代码模板,压缩成一套高密度的知识结构。当你需要复习或者遇到类似问题时,能快速定位到关键思路,而不是从头再推一遍。
具体来说,算法笔记解决三个问题。第一是遗忘曲线的问题,人的记忆衰减是客观规律,笔记是对抗遗忘的外部存储。第二是知识碎片化的问题,课程内容按章节推进,但算法思想是交叉的,笔记能帮你建立跨章节的关联。第三是从理解到应用的鸿沟,课上听懂了不代表能写出来,笔记里如果有你自己整理过的代码模板和调试记录,这个鸿沟会小很多。
适合做算法笔记的人其实很广。如果你是正在上算法课的学生,笔记能帮你把课堂内容转化为长期记忆;如果你是在准备面试的求职者,笔记是你复习动态规划、图论这些高频考点的最快路径;如果你已经工作了但偶尔需要用到算法,笔记就是你的速查手册。不同阶段的人笔记的侧重点不同,但核心逻辑是一样的:用自己的话重新组织知识。
1.2 笔记整理的核心思路:三层结构法
我试过很多种笔记方式,最后稳定下来的是一套“三层结构法”。这个方法的灵感来自编译原理里的分层思想,把笔记分成原始层、提炼层、应用层三个层次,每层解决不同的问题。
原始层是课堂内容的忠实记录,包括老师讲的算法定义、推导过程、例题。这一层的关键是快,不要追求美观,用最顺手的方式记下来就行。我一般用纸笔或者纯文本,因为这一层的内容后续会被反复重构,格式不重要。很多人做笔记的误区是花大量时间在原始层的排版上,结果笔记做得很漂亮但从来没回头看过。
提炼层是笔记的核心价值所在。这一层要求你用自己的语言重新表述算法的核心思想,画出关键的数据结构变化过程,标注出容易出错的边界条件。比如学KMP算法的时候,原始层可能记的是“next数组的求法”,提炼层就要写成“next[i]表示模式串前i个字符的最长公共前后缀长度,求法是用双指针,j指向已匹配的前缀末尾,i扫描模式串”。提炼层还要做一件事:把算法的时间复杂度和空间复杂度推导一遍,不是抄结论,而是自己推。
应用层是代码模板和变体记录。每个经典算法我都会在应用层放一个经过自己调试的代码模板,注释里写清楚哪些地方容易写错、哪些参数需要根据题目调整。比如二分查找,我会记录三种模板:找精确值、找左边界、找右边界,每种模板的循环条件和边界更新方式都不同,这些细节在面试和竞赛中极其关键。
三层结构的好处是,复习的时候可以根据需要选择深度。快速复习看提炼层,深入理解看原始层,准备面试看应用层。而且三层之间的信息是冗余的,互相印证,记忆效果比单层笔记好很多。
1.3 工具选型:为什么我最终选择了纯文本加代码仓库
工具选型这件事我折腾过很久。用过Notion、Obsidian、语雀、飞书文档,也试过手写iPad笔记,最后稳定下来的方案是Markdown纯文本加Git仓库。这个选择背后有几个考量。
第一是可迁移性。算法笔记是长期资产,我不希望它被绑定在某个特定平台上。Markdown是纯文本格式,任何编辑器都能打开,十年后也不会过时。Git仓库则保证了版本管理和多设备同步,而且天然支持代码片段的高亮和diff对比。
第二是代码友好。算法笔记里必然有大量代码,Markdown的代码块支持语法高亮,而且可以直接复制到IDE里运行。相比之下,Notion的代码块在复制时经常带格式,Obsidian的代码块虽然也不错但搜索体验不如纯文本加grep。
第三是搜索效率。纯文本加grep的搜索速度是任何图形界面笔记软件都比不了的。我经常需要搜索“动态规划 状态转移”这样的关键词组合,grep一条命令就能定位到所有相关笔记。而且Git的commit历史让我能看到自己对某个算法的理解是如何演进的,这个回溯能力非常宝贵。
具体的工作流是这样的:课堂上用纸笔快速记录原始层内容,课后当天把原始层内容整理成Markdown格式的提炼层,代码模板在本地IDE调试通过后粘贴到应用层。整个笔记仓库按“数据结构”“算法思想”“题型分类”三个维度组织目录,每个算法一个Markdown文件,文件内用二级标题区分三层结构。
提示:不要花太多时间纠结工具。工具是服务于笔记内容的,不是反过来。我见过太多人把时间花在配置笔记软件的主题和插件上,结果笔记本身没写几页。先用最顺手的工具开始,等笔记量上来了再考虑优化工具链。
2. 核心算法模块的笔记拆解与实操要点
2.1 数据结构模块:从数组到高级结构的笔记组织
数据结构是算法的基石,这部分笔记我按“线性结构→树形结构→图形结构→高级结构”的顺序组织。每个数据结构记录四个维度:逻辑定义、物理实现、核心操作、典型应用。
数组和链表这部分看似简单,但笔记里要重点记录的是边界处理和性能权衡。比如数组的插入删除是O(n),链表的随机访问是O(n),这些结论大家都知道,但笔记里要写清楚为什么。我会在提炼层画一个内存布局的示意图,标注出数组的连续内存和链表的指针跳转,这样理解更直观。应用层则记录一些经典操作模板,比如链表的反转、环检测、合并有序链表,每个模板都标注了易错点。
栈和队列的笔记重点是单调栈和单调队列这两个变体。普通栈和队列的操作很简单,但单调栈在解决“下一个更大元素”类问题时非常高效。我在笔记里记录了单调栈的维护规则:栈内元素保持单调递增或递减,新元素入栈时弹出所有破坏单调性的元素。这个规则用文字描述很抽象,所以我在笔记里放了一个手动模拟的过程,用表格展示每一步栈的状态变化。
树形结构是笔记的重头戏。二叉树部分我记录了三种遍历的递归和迭代写法,以及Morris遍历这种O(1)空间复杂度的巧妙方法。二叉搜索树部分重点记录删除操作的几种情况讨论,这是最容易写错的地方。平衡树部分(AVL和红黑树)我主要记录旋转操作的图示和插入删除后的调整逻辑,代码模板只保留AVL的,因为红黑树实现太复杂,面试中极少要求手写。
堆的笔记我特别强调了建堆的O(n)复杂度证明和堆排序的原地实现。很多人知道堆排序是O(nlogn),但不知道建堆本身是O(n)而不是O(nlogn),这个点在面试中经常被问到。我在笔记里用数学推导证明了建堆的复杂度,推导过程写在原始层,结论和直觉解释写在提炼层。
并查集的笔记核心是路径压缩和按秩合并这两个优化。我会记录朴素并查集、路径压缩并查集、路径压缩加按秩合并并查集三种实现的代码,并对比它们的时间复杂度。应用层记录了几个经典题型:连通分量计数、冗余连接检测、等式方程可满足性判断。
2.2 算法思想模块:分治、贪心、动态规划的笔记方法
算法思想这部分是笔记的精华,也是最需要“用自己的话重新表述”的部分。我按分治、贪心、动态规划、回溯、分支限界五个思想来组织,每个思想记录“适用条件、核心步骤、正确性证明思路、经典例题”。
分治法的笔记重点是递归式的复杂度分析。主定理是必须掌握的,但我在笔记里不仅记录了主定理的三种情况,还记录了每种情况对应的直观解释。比如T(n)=2T(n/2)+O(n)对应的是归并排序,复杂度O(nlogn),直观理解是每一层合并的总代价是O(n),一共有logn层。这种直观解释比死记公式有用得多。
贪心算法的笔记核心是贪心选择性质的证明。贪心算法最难的不是写代码,而是证明贪心策略是正确的。我在笔记里记录了交换论证法、归纳法、拟阵理论三种证明思路,每种思路配一个经典例题。比如区间调度问题用交换论证法证明,霍夫曼编码用归纳法证明。这些证明过程写在原始层,提炼层只保留证明的核心逻辑和关键步骤。
动态规划是笔记中篇幅最大的部分。我按线性DP、区间DP、树形DP、状态压缩DP、数位DP五个子类来组织。每个子类记录“状态定义、转移方程、初始化、遍历顺序、答案位置”五个要素。状态定义是最关键的,我会在笔记里对比不同状态定义对复杂度的影响。比如最长递增子序列问题,定义dp[i]为以i结尾的LIS长度,转移是O(n^2);如果定义dp[i]为长度为i的LIS的最小末尾元素,转移可以用二分优化到O(nlogn)。这两种定义的对比写在提炼层,代码模板写在应用层。
背包问题是DP的经典题型,我在笔记里整理了01背包、完全背包、多重背包、分组背包四种变体的代码模板。每种模板都标注了遍历顺序的关键点:01背包内层循环倒序,完全背包内层循环正序,多重背包用二进制拆分或单调队列优化。这些细节在面试中经常被考察,笔记里用加粗标注出来。
回溯法的笔记重点是剪枝策略。回溯的本质是暴力搜索加剪枝,剪枝的好坏直接决定效率。我记录了可行性剪枝、最优性剪枝、对称性剪枝三种策略,每种策略配一个例题。比如N皇后问题用可行性剪枝,旅行商问题用最优性剪枝,组合总和问题用对称性剪枝去重。
2.3 图论模块:从存储结构到经典算法的笔记体系
图论部分的笔记我按“图的存储→图的遍历→最短路径→最小生成树→拓扑排序→连通性”的顺序组织。每个算法记录“适用场景、核心思想、复杂度、代码模板、易错点”。
图的存储我记录了邻接矩阵和邻接表两种方式,并对比了它们的适用场景。邻接矩阵适合稠密图,空间O(n^2),判断两点是否相邻是O(1);邻接表适合稀疏图,空间O(n+m),遍历邻居是O(degree)。笔记里用表格对比了两种存储方式在各种操作上的复杂度,方便根据题目数据范围快速选择。
图的遍历记录了DFS和BFS两种方式。DFS的笔记重点是递归实现和栈实现的转换,以及时间戳的应用(发现时间和完成时间)。BFS的笔记重点是层次遍历和多源BFS。多源BFS在解决“腐烂的橘子”“01矩阵”这类问题时非常高效,笔记里记录了多源BFS的代码模板:初始时把所有源点加入队列,然后正常BFS。
最短路径算法我记录了Dijkstra、Bellman-Ford、SPFA、Floyd四种。Dijkstra适用于非负权图,用优先队列优化后复杂度O((n+m)logn);Bellman-Ford适用于含负权边的图,可以检测负环,复杂度O(nm);SPFA是Bellman-Ford的队列优化版本,平均效率更高但最坏复杂度仍是O(nm);Floyd适用于多源最短路径,复杂度O(n^3),代码极其简洁。笔记里用表格对比了四种算法的适用场景和复杂度,并记录了每种算法的代码模板。
最小生成树记录了Kruskal和Prim两种算法。Kruskal基于并查集,适合稀疏图,复杂度O(mlogm);Prim基于优先队列,适合稠密图,复杂度O((n+m)logn)。笔记里记录了两种算法的正确性证明思路:Kruskal用切割性质,Prim用贪心选择性质。
拓扑排序的笔记重点是Kahn算法和DFS算法两种实现。Kahn算法基于入度,可以检测环;DFS算法基于完成时间逆序,也可以检测环。笔记里记录了两种实现的代码模板,并对比了它们的适用场景。拓扑排序的经典应用是课程表问题,笔记里记录了这个问题的两种解法。
连通性部分记录了强连通分量、割点、桥、双连通分量等概念和算法。Tarjan算法是这部分的核心,笔记里详细记录了Tarjan算法的DFS过程和low值的更新规则。这部分内容比较抽象,我在笔记里放了一个手动模拟的表格,展示每一步的dfn和low值变化。
2.4 字符串模块:匹配、哈希与自动机的笔记整理
字符串算法在面试和竞赛中出现的频率很高,但很多人学得比较零散。我在笔记里按“字符串匹配→字符串哈希→字典树→后缀数组→自动机”的顺序组织。
字符串匹配记录了KMP、Boyer-Moore、Rabin-Karp三种算法。KMP是重点,笔记里详细记录了next数组的求法和匹配过程。next数组的求法我用了两种表述:一种是“最长公共前后缀长度”,另一种是“失配后模式串指针应该跳到的位置”。两种表述在笔记里并列,帮助理解。Boyer-Moore的笔记重点是坏字符规则和好后缀规则,以及它们如何结合使用。Rabin-Karp的笔记重点是滚动哈希的计算和冲突处理。
字符串哈希的笔记核心是哈希函数的设计和冲突处理。我记录了多项式哈希的公式和模数的选择,以及如何用双哈希降低冲突概率。笔记里还记录了如何用哈希解决“最长重复子串”“回文子串”等问题。
字典树的笔记重点是插入、查询、前缀匹配三个操作的代码模板。字典树的空间优化(用数组代替指针)也记录在应用层。字典树的经典应用是“单词搜索”“自动补全”“异或最大值”等问题,笔记里记录了这些问题的解法。
后缀数组的笔记重点是倍增法构造和height数组的应用。后缀数组的构造比较绕,我在笔记里放了一个手动模拟的表格,展示每一轮倍增后sa和rank的变化。height数组的应用记录了“最长重复子串”“不同子串个数”等经典问题的解法。
自动机部分记录了AC自动机的构造和匹配过程。AC自动机是字典树加KMP思想,笔记里记录了fail指针的构建(BFS)和匹配过程。AC自动机的经典应用是多模式串匹配,笔记里记录了这个问题的代码模板。
3. 实操过程与核心环节实现
3.1 从课堂记录到结构化笔记的完整流程
我以“动态规划”这一章为例,完整走一遍从课堂记录到结构化笔记的流程。这个流程我用了很多次,已经比较成熟,你可以直接参考。
第一步:课堂快速记录。老师讲动态规划的时候,我会在纸上快速记下关键内容:DP的定义、状态转移方程的例子、老师强调的易错点。这一阶段不追求完整,只求快。比如老师讲最长递增子序列时,我记的是“dp[i]=以i结尾的LIS长度,转移:dp[i]=max(dp[j])+1 for j<i and nums[j]<nums[i],O(n^2)”。这个记录很粗糙,但足够我课后回忆起课堂内容。
第二步:当天整理提炼层。课后当天,我会把课堂记录整理成Markdown格式的提炼层。这一步的关键是用自己的话重新表述,而不是照抄课堂记录。比如上面那个LIS的例子,我会写成:
最长递增子序列(LIS)问题:给定数组nums,求最长严格递增子序列的长度。
状态定义:dp[i]表示以nums[i]结尾的最长递增子序列长度。
转移方程:dp[i] = max(dp[j]) + 1,其中0 <= j < i且nums[j] < nums[i]。
初始化:dp[i] = 1,因为每个元素本身就是一个长度为1的递增子序列。
答案:max(dp[i]),遍历所有i。
复杂度:时间O(n^2),空间O(n)。
优化思路:维护一个数组tails,tails[i]表示长度为i+1的递增子序列的最小末尾元素。遍历nums,用二分查找在tails中找到第一个大于等于nums[i]的位置并替换。时间优化到O(nlogn)。
这个提炼层的内容比课堂记录丰富很多,而且加入了我自己的理解(优化思路)。这一步花的时间最多,但价值也最大。
第三步:编写应用层代码模板。提炼层完成后,我会在本地IDE里把代码写一遍,调试通过后粘贴到应用层。LIS的代码模板我记录了两种:O(n^2)的DP版本和O(nlogn)的二分优化版本。每个版本都加了详细注释,标注了易错点。
# O(n^2) DP版本 def lengthOfLIS(nums): if not nums: return 0 n = len(nums) dp = [1] * n # dp[i]表示以nums[i]结尾的LIS长度 for i in range(1, n): for j in range(i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) # O(nlogn) 二分优化版本 import bisect def lengthOfLIS(nums): tails = [] # tails[i]表示长度为i+1的LIS的最小末尾元素 for num in nums: pos = bisect.bisect_left(tails, num) if pos == len(tails): tails.append(num) else: tails[pos] = num return len(tails)第四步:记录易错点和调试经验。这一步是笔记的精华。LIS的易错点我记录了三个:第一,dp数组初始化是1不是0;第二,二分优化版本中bisect_left和bisect_right的区别(找第一个大于等于的位置用left,找第一个大于的位置用right);第三,二分优化版本只能求长度,不能直接求具体序列,如果需要求序列还要额外记录前驱。
第五步:关联其他知识点。LIS和很多问题有关联,比如“俄罗斯套娃信封问题”是二维LIS,“最长数对链”是LIS的变体,“最少拦截系统”是LIS的Dilworth定理应用。我在笔记里把这些关联问题列出来,并标注了它们和LIS的关系。这样复习的时候能举一反三。
3.2 代码模板的调试与验证方法
算法笔记里的代码模板必须经过验证,否则就是纸上谈兵。我的验证方法分三步:手动模拟、边界测试、对拍验证。
手动模拟是最基本的验证方法。对于每个代码模板,我会用一个小例子手动走一遍代码,在纸上画出变量变化的过程。比如KMP算法,我会用模式串“ABABC”和文本串“ABABABC”手动模拟next数组的求法和匹配过程。这个过程很慢,但能发现很多逻辑错误。
边界测试是验证代码鲁棒性的关键。每个代码模板我都会测试以下边界情况:空输入、单元素输入、所有元素相同、已经有序、逆序、包含重复元素。比如二分查找的模板,我会测试目标值在数组开头、结尾、不存在、数组为空、数组只有一个元素等情况。这些边界情况在面试中经常被考察,提前测试过就能避免现场出错。
对拍验证是验证复杂算法正确性的终极方法。对于动态规划、图论这类容易写错的算法,我会写一个暴力解法作为对照,随机生成大量测试数据,对比两个解法的输出。如果所有测试都一致,说明代码模板是正确的。对拍脚本我一般用Python写,因为Python的随机数生成和字符串处理很方便。
# 对拍脚本示例:验证LIS的O(nlogn)解法 import random import bisect def lis_n2(nums): if not nums: return 0 n = len(nums) dp = [1] * n for i in range(1, n): for j in range(i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) def lis_nlogn(nums): tails = [] for num in nums: pos = bisect.bisect_left(tails, num) if pos == len(tails): tails.append(num) else: tails[pos] = num return len(tails) for _ in range(10000): n = random.randint(0, 20) nums = [random.randint(-10, 10) for _ in range(n)] assert lis_n2(nums) == lis_nlogn(nums), f"Failed on {nums}" print("All tests passed")对拍通过后,代码模板就可以放心地放入应用层了。我还会在模板旁边标注对拍的测试数据规模和通过率,方便以后回顾。
3.3 笔记的复习策略与迭代更新
笔记做完不是终点,复习和迭代才是。我的复习策略分定期复习、专题复习、面试前突击三种模式。
定期复习是每周花一个小时,随机翻看几个算法模块的提炼层,遮住答案自己复述一遍核心思想。如果发现某个地方卡住了,就标记下来,当天重点复习。这个习惯坚持了半年后,大部分算法的核心思想都能脱口而出。
专题复习是在遇到某个类型的题目时,集中复习相关模块。比如最近在准备面试,发现动态规划是弱项,就花一个周末把DP模块的所有笔记过一遍,重点看状态定义和转移方程的推导过程。专题复习的好处是能建立知识块之间的关联,比零散复习效果好。
面试前突击是面试前一两天,只看应用层的代码模板和易错点。这时候不需要深入理解,只需要快速唤醒记忆。我会把每个模板的代码在IDE里敲一遍,确保手感和肌肉记忆还在。
笔记的迭代更新也很重要。每次遇到新的题目或者新的理解,我都会更新对应的笔记。比如后来学到了LIS的Dilworth定理(最长递增子序列长度等于最少不递增子序列的个数),我就把这个定理补充到了LIS的笔记里。笔记是活的,随着理解的深入不断进化。
提示:复习的时候不要只看不写。看笔记是被动输入,写代码是主动输出,两者的效果差很多。我每次复习都会在IDE里把代码模板重新敲一遍,哪怕是很简单的二分查找。这个习惯帮我避免了很多“一看就会,一写就废”的情况。
4. 常见问题与排查技巧实录
4.1 算法笔记整理中的典型问题与解决方案
在整理算法笔记的过程中,我踩过不少坑,这里整理成速查表,希望能帮你少走弯路。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 笔记记了从来不翻 | 笔记太冗长,复习成本高 | 采用三层结构,复习时只看提炼层 |
| 代码模板复制后运行报错 | 复制时带了格式或缺少依赖 | 用纯文本Markdown,代码块标注语言 |
| 算法思想理解不透彻 | 只记结论没记推导过程 | 原始层保留完整推导,提炼层写直观解释 |
| 相似算法混淆 | 没有对比记录 | 用表格对比相似算法的适用场景和复杂度 |
| 边界条件总是写错 | 没有系统整理易错点 | 每个模板标注易错点,定期复习 |
| 笔记越来越乱 | 没有统一的目录结构 | 按“数据结构-算法思想-题型分类”三维组织 |
| 复习效率低 | 没有复习计划 | 定期复习+专题复习+面试前突击三模式 |
| 新学的知识没地方放 | 目录结构不灵活 | 预留“其他”目录,定期整理归位 |
这个表格里的每一条都是我实际踩过的坑。比如“笔记记了从来不翻”这个问题,我一开始的笔记是课堂记录的堆砌,又长又乱,复习的时候根本不想看。后来改成三层结构后,复习只看提炼层,效率高了很多。
“相似算法混淆”也是常见问题。比如Dijkstra和Prim算法,都是基于贪心和优先队列,代码结构也很像,但一个求最短路径一个求最小生成树。我在笔记里用表格对比了它们的区别:Dijkstra的dist数组表示从源点到该点的最短距离,Prim的dist数组表示该点到已选集合的最小边权;Dijkstra更新的是dist[v] = dist[u] + w(u,v),Prim更新的是dist[v] = min(dist[v], w(u,v))。这个对比写在两个算法的笔记里,互相引用。
4.2 算法实现中的高频Bug与调试技巧
算法实现中的Bug有一些高频模式,我整理了一份速查表,覆盖了大部分常见错误。
| Bug类型 | 典型表现 | 调试技巧 |
|---|---|---|
| 数组越界 | 运行时错误或结果异常 | 检查循环边界,用assert验证索引范围 |
| 整数溢出 | 结果变成负数或异常值 | 用long类型,或提前取模 |
| 死循环 | 程序卡住不输出 | 检查循环变量是否更新,用print调试 |
| 递归栈溢出 | 递归深度过大报错 | 改用迭代,或增加递归深度限制 |
| 浮点数精度 | 比较结果不稳定 | 用epsilon比较,或改用整数运算 |
| 初始化遗漏 | 结果偏小或偏大 | 检查dp数组、visited数组的初始化 |
| 遍历顺序错误 | 结果不符合预期 | 检查DP的遍历顺序是否满足依赖关系 |
| 哈希冲突 | 结果偶尔错误 | 用双哈希,或改用确定性的数据结构 |
“整数溢出”是我踩过最多的坑。比如在计算组合数或者累加求和时,中间结果可能超过int的范围。我的解决方案是:如果题目涉及大数运算,直接用Python(Python的int是任意精度的),或者用C++的long long,并在关键步骤取模。
“遍历顺序错误”在动态规划中特别常见。比如01背包的内层循环必须倒序,完全背包的内层循环必须正序。这个区别的原因是:01背包每个物品只能选一次,倒序保证更新dp[j]时用的是上一轮的状态;完全背包每个物品可以选多次,正序保证更新dp[j]时用的是本轮已经更新过的状态。我在笔记里用图示解释了这个区别,比文字描述直观很多。
“递归栈溢出”在DFS和回溯中经常遇到。Python的默认递归深度是1000,超过就会报错。解决方案有两种:一是用sys.setrecursionlimit调大限制,二是改用迭代写法。我一般优先用迭代,因为迭代不受栈深度限制,而且效率更高。
4.3 面试中算法题的解题框架与笔记的配合使用
面试中的算法题和课堂上的算法学习有一个关键区别:时间压力。课堂上你可以花一个小时推导一个算法,面试中你只有二十分钟写代码。这时候笔记的作用就从“学习工具”变成了“速查手册”。
我的面试解题框架分四步:理解题意、选择算法、编写代码、测试验证。每一步都和笔记有配合。
理解题意阶段,我会快速判断题目属于哪个类型。如果是数组操作,可能是双指针或滑动窗口;如果是字符串匹配,可能是KMP或哈希;如果是图论问题,可能是BFS/DFS或最短路径。这个判断依赖笔记里的题型分类索引。我在笔记的每个算法模块开头都写了一个“适用题型”列表,方便快速定位。
选择算法阶段,我会对比几种可能的解法,选择最优的。比如一道题可以用动态规划也可以用贪心,我会快速回忆笔记里两种算法的适用条件,判断哪个更合适。这个阶段笔记里的复杂度对比表非常有用。
编写代码阶段,我会直接套用笔记里的代码模板,根据题目调整参数。比如二分查找的模板,我会根据题目要求选择找精确值、找左边界还是找右边界。笔记里的模板都经过了对拍验证,直接套用可以避免很多低级错误。
测试验证阶段,我会用笔记里记录的边界情况测试代码。空输入、单元素、重复元素、有序、逆序,这些边界情况在笔记里都有记录,逐一测试即可。
提示:面试前不要只看笔记,一定要在纸上或白板上手写代码。面试中是在白板上写代码,没有IDE的自动补全和语法检查,手写的感觉和键盘输入完全不同。我面试前会手写几道经典题,保持手感。
4.4 笔记体系的长期维护与知识更新
算法笔记是一个长期资产,需要持续维护和更新。我的维护策略分三个层面:内容更新、结构优化、工具升级。
内容更新是最高优先级的。每次学到新的算法或者对旧算法有新的理解,我都会及时更新笔记。比如后来学到了“单调栈”在“接雨水”问题中的应用,我就把这个应用补充到了单调栈的笔记里。内容更新的原则是:宁可写得不完美,也不要拖延。拖延的结果往往是永远不更新。
结构优化是定期进行的。每过几个月,我会回顾一下笔记的目录结构,看看有没有需要调整的地方。比如一开始我的笔记是按“章节”组织的,后来发现按“题型”组织更实用,就做了一次大的重构。结构优化不要频繁做,否则会浪费时间在整理上而不是学习上。
工具升级是最低优先级的。只有当现有工具严重制约效率时,才考虑升级。比如一开始我用纯文本记笔记,后来代码模板多了,纯文本的代码高亮不够好,就迁移到了Markdown。工具升级的原则是:够用就好,不要为了工具而工具。
笔记的长期维护还有一个关键点:定期备份。我用Git仓库管理笔记,每次更新后commit一次,推送到远程仓库。这样即使本地设备出问题,笔记也不会丢失。Git的版本历史还能让我看到自己对某个算法的理解是如何演进的,这个回溯能力非常宝贵。
最后分享一个我个人的小习惯:我会在每篇笔记的末尾记录“最后更新日期”和“下次复习日期”。下次复习日期一般是最后更新日期加一个月。这样我翻笔记的时候能快速判断这篇笔记的新鲜度,决定是否需要重点复习。这个习惯坚持下来,笔记的质量和复习效率都有明显提升。