1. 从“更弱智”说起:为什么我建议你用笨办法学算法
先交代一下背景。这是我连续记录算法学习笔记的第51天,标题里“更弱智”三个字不是自谦,是我反复试错后总结出来的方法论:把自己当成一个“弱智”去学算法,反而学得最快。
为什么这么说?我见过太多人打开《算法导论》,翻到红黑树那一章,硬啃两小时,看懂了旋转的每一步,合上书第二天全忘光。问题不在智商,在于用看小说的方式去学算法——只满足于“看懂”,从不逼自己“做出来”。
我给自己的规矩很简单:任何算法,必须做到能用大白话讲清楚它干了什么,能徒手写出核心代码,能说出它适合什么场景、不适合什么场景。如果三件事里有一件做不到,就当自己没学会,回家重学。这个过程很笨、很慢、很“弱智”,但效果出奇地好。
过去51天,我按一个相对完整的图谱过了一遍:从最基本的排序、查找,到字符串匹配、图论、动态规划,再到机器学习、深度学习里常用的优化算法、聚类算法、回归算法,后面又补了PID控制、CRC校验、粒子群优化这类工程味道很浓的算法。这篇文章不是要重讲某几个算法——那是教科书和视频课的事——我想聊的是,从day1走到day51,哪些算法之间的脉络是相通的,哪些坑是完全可以绕开的,以及那个“更弱智”的学习框架到底怎么落地。
不管你是在校学生、准备面试的求职者,还是工作中被迫接触算法的开发,这套思路应该都能派上用场。文章会比较长,我尽量按学习顺序而不是目录顺序来写,因为后者是教科书逻辑,前者才是人脑真正记住知识的逻辑。
2. 先把整个算法地图摊开:51天我到底学了什么
2.1 算法不是一堆孤立的技巧,而是四类问题的解法集合
很多人学算法最大的误区,是把它当成“一道一道的题”。今天刷一道两数之和,明天刷一道反转链表,刷完200题,遇到新题还是懵。我学到day20左右才意识到,问题出在没有建立问题类型的概念。
冷静下来把所有算法归归类,会发现整个算法世界其实就四件事:
- 让数据有秩序:排序算法、堆、二叉搜索树、哈希表,本质都是为了让数据变得“好找”。这一类是地基,面试里80%的题绕不开它们。
- 在结构里找路:图、树、深度优先、广度优先、拓扑排序、最短路径(Dijkstra、Floyd)、最小生成树(Prim、Kruskal),解决的是“怎么从一个点到另一个点”的问题。A*算法、粒子群这些寻优策略,本质上也是找路,只是路径空间变成了抽象的参数空间。
- 用穷举干掉不确定性:动态规划、回溯、贪心、剪枝。核心思路是把大问题拆成重叠子问题,或者用暴力搜索加合理剪枝逼近最优解。
- 从数据里学规律:线性回归、随机森林、聚类、强化学习、深度学习优化器,这些是机器学习/深度学习算法,解决的是“从样本中归纳规律”的问题。
想通这个分类后,我后面30天的学习节奏明显变快了,因为很多算法在“问题本质”上是同一个东西。比如KMP算法和状态机,比如PID算法和PID调节器,再比如匈牙利算法和二分图匹配,它们都不是孤立的知识点。
2.2 day1到day51的路线图:从小白到有条不紊的摸索路径
我的实际学习顺序是这样的,你可以参考,但没必要完全照抄:
第一阶段(day1-10):基础数据结构 + 排序数组、链表、栈、队列、哈希表,然后是冒泡、选择、插入、归并、快排、堆排序。这一阶段的目标不是背代码,而是理解“比较和交换”为什么能排好序,理解时间复杂度的意义。
第二阶段(day11-20):树、图、搜索二叉树、平衡树、二叉堆、图的邻接表表示,DFS/BFS,拓扑排序、Dijkstra、Floyd。这一阶段会有很强的“顿悟感”,因为你发现很多看起来花里胡哨的算法,背后就是“排队遍历”和“贪心选最小”。
第三阶段(day21-30):字符串 + 动态规划 + 贪心 + 回溯KMP、Trie树、LCS、背包问题、最长递增子序列、子集枚举、剪枝。这一阶段最容易劝退,因为动态规划刚开始就是反直觉的,你必须接受“先算小、再算大”的思路。
第四阶段(day31-40):工程类算法 + 数学类算法MD5、CRC校验、PID控制、快速幂、矩阵快速幂、Tarjan求强连通分量、匈牙利算法。学到这你会发现,很多工业软件里跑得飞快的算法,基础也就是你前30天学的那些东西。
第五阶段(day41-51):机器学习 + 优化算法线性回归、逻辑回归、决策树、随机森林、K-Means聚类、梯度下降、Adam优化器、强化学习的基本框架(Q-Learning、策略梯度)、遗传算法、粒子群算法、模拟退火。这块单独拎出来说一句:真正的算法工程师和普通开发的分水岭就在这里,你得理解最优化的思想。
这条路线不算最优,但它是真实可行的。如果你时间有限,至少前三个阶段要扎实,后面两个阶段可以按需补充。
2.3 “更弱智”学习法的核心:费曼技巧 + 手写实现 + 场景绑定
现在说方法。我用的框架,其实就是“费曼技巧”在算法领域的变形,加上两个硬性要求:
第一,每个算法要能用讲给小学生听的语气说出来。不是背定义,而是真的理解。比如什么是快速排序?我会说:“我选一个中间人,比它小的站左边,比它大的站右边,然后两边各自再找中间人,重复到只剩一个人。”这就算过关。如果你发现自己必须用到“分治”“递归”这种词才能解释,说明还没吃透。
第二,必须不看任何资料手写实现。这一条会暴露一切“以为自己会了”的幻觉。我统计过,真正能做到“看懂——合上书写出来——跑通——再默写一遍”的算法,占比不到六成。剩下的四成,比如KMP的next数组、Tarjan的low值更新、匈牙利算法的增广路径,都需要反复拆解。
第三,给每个算法绑定至少一个“真实场景”。不是为了面试造轮子,而是为了在脑子里留下锚点。冒泡排序像气泡往上冒,堆排序像排行榜实时更新,KMP像在书里跳着找关键词(匹配失败时知道跳过前面的已知匹配部分),PID像空调温度调节器——温度高了就降功率,低了就升,偏差大就猛调,偏差小就微调。
这条“三件套”路线我用了51天,真实感受是:前期慢,中期突然加速度,后期能很快抄底新算法——因为很多新算法本质上是你学过的旧算法的变体。
3. 手把手拆几个“看起来很唬人”的算法
3.1 归并排序和快速排序:理解分治思想的第一次顿悟
先谈排序。很多人觉得排序简单,但排序是算法思想的微缩宇宙,值得反复琢磨。
拿归并排序讲明白“分治”。它的思路是:把数组从中间切成两半,左半边自己排,右半边自己排,然后合并两个有序序列。这里最隐蔽、最关键的操作是最后一步“合并”——它需要额外的临时数组,这就是归并排序空间复杂度O(n)的原因。时间复杂度是稳定的O(n log n),意味着不管你给的数据乱成什么样,性能不波动。
归并排序的时间复杂度推导值得一提:每一层递归,所有元素总共被“看”一遍,复杂度为O(n);递归深度为log n层,总复杂度O(n log n)。这是最好的例子。
快速排序就“野”一点,它的核心是选一个基准(pivot),把小于基准的放左边,大的放右边。平均情况O(n log n),但最坏情况O(n^2)——当你每次选的基准都恰好是最大或最小元素的时候,比如对一个已经有序的数组用固定位置选基准,就会退化。所以工业级实现会做“三数取中”,或者随机选基准,就是为了防这种退化和可能的对抗攻击。快排不用额外数组,所以平均空间是O(log n),优于归并排序。
这两个排序放在一起对比,最值得学到的是:同样解决一个问题,权衡维度可以完全不同。归并排序稳定、可控、但占内存;快排平均快、省内存,但运气差的角落。没有“最好的算法”,只有“最合适的算法”。
3.2 KMP算法:从“暴力匹配”到“跳着查”的思维跃迁
字符串匹配是面试高频点,但很多人一听到KMP就头皮发麻。我花了两天时间才真正通透了它,方法很“弱智”:手工模拟一遍全过程,把每一步的指针移动都写下来。
暴力匹配的做法是:模式串从文本串第一位开始试,匹配失败,模式串整体后移一位,重新从头比较。问题在于,一句“重新从头比较”可能浪费了已经比对过的信息。
KMP的核心思想是:匹配失败时,模式串不要只后移一格,而是利用“部分匹配表”(next数组),直接跳过那些不可能是匹配位置的地方。举例来说,模式串“ABCDABD”在匹配到“ABCDAB”时失败,此时末尾的“AB”已经在文本串里出现过了,我们可以利用这一点,让模式串跳到“AB”开头的位置开始下一次比较,而不是从头再来。
next数组的构建是KMP的难点,很多人死在递归推导上。我的理解方式是——next[i]表示模式串前i个字符组成的子串中,最长相等的前缀和后缀的长度(不包含自身)。比如“ABCDAB”,最长相等前缀后缀是“AB”,长度2,所以当第6个字符(D)匹配失败时,我们可以直接把前缀“AB”挪到后缀“AB”的位置上去。这样,文本串的指针永远不回退,模式串指针回退到next数组指定的位置,总复杂度O(n+m)。
我强烈建议你写个例子,用笔在纸上模拟,比如用文本串: BBC ABCDAB ABCDABCDABDE和模式串: ABCDABD,把i和j每一步的动作列出来,10分钟就能通。KMP真正教你的不是代码,而是“如何从已发生的失败中提取有效信息”这个思维模型。
3.3 动态规划:为什么先算小问题能算出大问题
动态规划是我学得最痛苦、也受益最大的一块。它和内功心法很像:乍一看什么规则的没有,一旦理解透了,很多题都是套路。
动态规划的核心讲透了就一句话:大问题的最优解,可以由小问题的最优解推导出来。你要做的是三件事:定义状态、找状态转移方程、确定初始条件。
拿斐波那契数列举例:f(n) = f(n-1) + f(n-2),状态就是f(n),转移方程就是一个加法,初始条件是f(0)=0、f(1)=1。看起来简单,但很多人写代码时依然会用递归——如果直接递归,复杂度是O(2^n),n稍微大一点就卡死;而用一个一维数组或者两个变量存中间结果,复杂度是O(n)。区别只在“浪费”和“复用”的差别。
我把动态规划常见的题型归了个类:
- 一维DP:爬楼梯、斐波那契、打家劫舍、最长递增子序列(LIS)
- 二维DP:最长公共子序列(LCS)、编辑距离、矩阵路径
- 背包问题:0-1背包、完全背包、多重背包,这是面试重点,本质是一个决策二维表
- 区间DP:矩阵链乘法、回文子串拆解
- 状态压缩DP:旅行商问题的暴力美学
我建议新手入门从“最长递增子序列”开始,它很好理解,而且有两种解法(O(n^2) DP和O(n log n)贪心+二分),能让你直观感受“同样是DP,优化的空间有多大”。背包问题则一定要手写一遍状态转移表的填充过程,把表格画出来,比看十遍代码都管用。
用“弱智”的标准说:动态规划就是你从小到大地填一张表,每个格子都在回答“如果只有这些元素,最优解是多少”。最后你需要的答案,往往就在表格的右下角。
3.4 贪心、回溯、剪枝:优化里的三兄弟
讲完了DP,顺带把它表亲的贪心、回溯一起说了吧。它们的共同点都是处理“选择”问题,但策略不同。
贪心算法在每一步都选当前看起来最优的选择,期望全局最优。它最迷人的地方是只要贪心策略是对的,代码往往极短,比如哈夫曼编码、Prim算法、Kruskal算法,都是典型的贪心。但问题是“贪心策略正确”这件事需要严格证明,不然就容易踩坑。我举一个反例:找零钱问题里,如果币值是[1, 5, 11],要找15元,贪心算法会先拿11,再拿4张1元,总共5张;但最优解是3张5元。所以贪心适合有特定数学性质的场景,不能无脑套用。
回溯算法是“有组织的暴力”:走一步,不行就退回来换条路再走,直到穷尽所有可能。经典场景是八皇后、全排列、子集生成。它有一个伴生技术叫“剪枝”——在探索过程中提前判断某个分支不可能产生最优解,直接砍掉。剪枝是回溯题的核心考点,好的剪枝能把O(n!)的复杂度砍到可接受范围。我做过一次“数独求解器”,不剪枝可能跑几十秒,加上“当前行/列/宫已经出现的数字”剪枝后毫秒级解出。这个对比让我彻底记住剪枝的价值。
说到剪枝,捎带提一下决策树和随机森林里的剪枝——这俩全名叫“预剪枝/后剪枝”,原理一样,都是为了防过拟合:树枝长得太细,反而把噪声学进去了。算法学习有意思的地方就在这种跨界的对应关系,你在数据结构里学的剪枝,居然在机器学习里又碰到一遍。
3.5 从Floyd到Dijkstra、A*到PID、MD5到CRC:工程世界的算法速览
如果说前四类算法是“内力”,下面这些更像是“套路招数”,但它们在实际项目里出现频率极高,值得了解一下。
Floyd算法最“暴力”地求解多源最短路径,三重循环枚举中转点,复杂度O(n^3),但是代码很短,适合节点数少的图(比如几百个以内)。Dijkstra是单源最短路径的经典解法,配合优先队列(堆)时间复杂度O((V+E) log V)。如果你要做游戏里的寻路,A算法才是主角——它在Dijkstra基础上加了启发式函数f = g + h,g是已走路径开销,h是到终点的预估开销,通过这个“预估分”优先探索更可能通向终点的节点。我说个直白类比:Dijkstra是“无脑地把最近的点都看一遍”,A是“看一眼前方和终点大致方向,先走过去再说”。这就是为什么游戏里怪物会“聪明”许多。
PID算法则完全是工程世界的语言。它由比例(P)、积分(I)、微分(D)三个分量组成:P看当前误差,I累积历史误差,D看误差变化趋势。调PID像是调厨师做菜的火候——P是现在咸淡差多少,I是这锅菜整体偏淡还是偏咸,D是盐味在变浓还是变淡。机器人底盘循线、四轴飞行器悬停、恒温箱控温,全靠它。我建议你装一个简单仿真(比如用Python模拟一个一阶惯性环节温控系统),调三组参数,观察曲线震荡、超调、稳态误差的变化,这是穷任何文字描述都替代不了的一课。
MD5和CRC类似,是做校验的。MD5是哈希算法,把任意内容计算成固定128位摘要,以前用做完整性校验,但因为碰撞漏洞,现在不该用在安全场景了;CRC则是一种循环冗余校验,在通信协议(比如Modbus)里用来检查传输过程中有没有比特错乱。这俩算法不看原理,直接按公式实现即可,但你要理解它们的定位差异:CRC防误码,MD5当年是防篡改,后者今天应该被SHA-256替代。
3.6 机器学习与优化算法:线性回归到随机森林,梯度下降到粒子群
最后一阶段是目前学起来最兴奋的,因为算法开始离“应用”最近。
线性回归是最简单的“从数据中学规律”的算法。它做的事情就是找一条直线y = wx + b,让所有样本点到直线的误差平方和最小。求解方式有两种思路,一种是用最小二乘法直接求解析解,另一种是用梯度下降法迭代逼近。这里梯度下降是所有深度学习算法的地基,必须吃透。什么叫梯度下降?想象你在山上下雾了,看不见远处,只知道脚下哪个方向是下坡,于是你沿着最陡的下坡方向迈一步,反复迭代,直到走进谷底。学习率(步长)太大容易在谷底两侧来回震荡,太小则收敛极慢。这是我当年学机器学习时踩的第一个大坑,调学习率调到深夜,后来才理解“自适应学习率优化器”存在的意义——Adam、RMSProp这类算法会自动帮你调整每一步的步长。
随机森林是“集成的力量”的典范:训练很多棵决策树,每棵树用样本的一个随机子集训练,最后投票出结果。它比单棵决策树稳得多,几乎不会过拟合到没边,是工程界性价比极高的算法。K-Means聚类则是一种无监督学习:给你一堆数据点,不告诉你标签,算法自动把它们分成K簇,大致做法是先随机选K个中心,然后反复迭代“归属分配 + 中心更新”,直到中心不再变化。这个算法特别容易理解:它就像你把一堆球放在桌面上,来回移动三个杯子,试图让每个球都离某个杯子最近。
再往深走一点,强化学习学习的是“决策策略”:智能体在一个环境里做动作,根据环境的奖励反馈调整策略。Q-Learning的核心是一个Q表,每一行是状态,每一列是动作,表里的值代表“在这个状态下做这个动作会有多少长期收益”。它的更新公式就是著名的贝尔曼方程:Q(s, a) = 奖励 + γ * max(Q(s′, a′))。你品一品,这个公式其实就在做“动态规划”那套事——用后继状态的价值估计当前状态的价值。看到了吧,第3节的动态规划和第6节的强化学习,隔着十万八千里,本质是同一个思想。这就是我前面说“算法不是孤立的”的绝佳证据。
粒子群算法、遗传算法、模拟退火这类“元启发式优化算法”,则是另一个分支——它们不依赖梯度信息,而是模拟鸟群觅食、生物进化、金属退火的物理过程。粒子群算法尤其简单优美:一群“粒子”在解空间里飞行,每个粒子记住自己的最优位置,同时群体共享全局最优位置,速度更新公式同时受“自己最优”和“群体最优”牵引。我第一次跑通粒子群算法求函数最小值时,看到粒子们在二维空间里“嗖嗖”地聚向最优解,那种画面感极其震撼,强烈建议你也试试。
4. 工具选型与实践环境:我踩过的坑和你应该选的路
4.1 语言选择:C++、Python、Java到底学哪个
这是个老生常谈到干瘪的话题,但必须面对。我的答案很直接:
- 如果你在准备算法面试,语言不重要,逻辑才重要。选你最有把握、写得最顺的语言。面试官不关心你用C++还是Python,关心的是你能不能在白板上写出无Bug的代码。
- 如果你想深入理解底层原理,C++能逼你面对指针、内存、引用和复杂度的真相。比如实现一个红黑树,用Python你会觉得“还行”,用C++你得思考节点析构、内存泄漏、迭代器失效,这一轮下来你对数据结构的理解更深入。我能坚持下来的一个重要原因,就是很多算法用C++手写过一遍以后,再用任何语言都能很快转换。它有“一次学会,处处复用”的效果。
- 如果你重点是机器学习和数据分析,Python是事实标准。numpy、pandas、scikit-learn、PyTorch,生态齐全。但请注意:会用sklearn调库不等于会算法,我强烈建议用numpy手写一遍线性回归、K-Means和反向传播,哪怕代码再丑也有意义,因为“手写过”和“用过”是两种完全不同的理解和抵御面试追问的能力。
4.2 可视化与调试工具:让抽象概念长出肌肉
工欲善其事,必先利其器。有几个工具帮了我大忙,我按推荐程度排序:
- VisuAlgo:一个在线算法可视化网站,排序、图论、哈希表这类算法可以一步步看动画执行。我学KMP、Dijkstra、Floyd的时候全程盯着它的动画看好几遍,配合自己手写代码,理解速度翻倍。
- Python + matplotlib:写一个“排序算法动态可视化”脚本,把冒泡、快排、归并每次交换后的数组渲染成柱状图,导出GIF。看着快排三下五除二把乱序数组拍平,成就感爆棚。
- Python tutor:逐行执行Python代码,实时显示变量的堆栈状态。调回溯和DP时非常有用——你能看到递归调用栈一层层进再一层层出,“函数调用栈”不再是课本上的名词。
- LeetCode / 洛谷:刷题平台,我建议按“分类题单”刷,不要随机刷。比如LeetCode的“动态规划”题单有几十道经典题,从爬楼梯到编辑距离,做一遍差不多就掌握了DP的套路。我个人的节奏是每天3道题,顶多5道,重在总结,不追量。
4.3 笔记本和卡片:最“弱智”的工具反而是最强大的
最后说一个最容易被忽视的工具:一个实体笔记本,和一堆空白卡片。
我的习惯是每学一个算法,在卡片正面写算法名称,背面写三行话:第一行大白话讲思路,第二行写时间和空间复杂度,第三行写这个算法的“不适场景”。这三行话逼着我把“理解”压缩成“要点”,写不出来的话说明没懂。复习时我随机抽卡片,看一眼名字,翻过背面核对,这种检索式练习比反复读笔记有效太多。
51天下来,我已经积累了约60张卡片,它们成为我面试前最好的复习材料。这本笔记的唯一秘诀是:越简略越好,能用图画绝不用文字。你画一画快排的分区图,比抄十行代码更有用。
5. 常见问题与排查技巧实录
5.1 递归怎么都理解不了,该硬背吗?
不要硬背。递归不是“自己调用自己”这么简单,它的本质是“函数调用栈的压栈与出栈”。如果理解不下来,我的建议是:去找一个调试器(比如IDE自带的,或者Python Tutor),设一个断点,一步步跳进递归调用里,亲眼观察调用栈每一层发生了什么。用归并排序做实验最好,因为它有“递”又有“归”,你能看到整个执行过程像洋葱一样一层层剥开再一层层包回去。
另一个笨办法是用纸笔展开递归树。比如Fibonacci(5)这个调用,你可以画出F(5)分成F(4)和F(3),F(4)又分成F(3)和F(2)……画完之后你就明白为什么直接递归是O(2^n)——因为同一个F(3)被重复计算了两遍。有了这个视觉感受,再引入记忆化搜索、动态规划就顺理成章了。
5.2 动态规划的状态定义总是找不到,怎么办
这是新手最常见的卡点。我的经验有三个“弱智”技巧:
- 问自己:这个问题的输入是什么?输入是数组还是字符串,那就大概率以索引为状态维度;输入是区间,比如“从i到j的子串”,大概率是二维区间DP。
- 问自己:我最后一步做了什么?以爬楼梯为例,最后一步要么跨了一级,要么跨了两级,所以f(n) = f(n-1) + f(n-2)。编辑距离最后一步要么替换、要么插入、要么删除,所以状态转移方程围绕这三种操作展开。大多数DP题的转移方程都可以用“最后一步”这个视角推导出来。
- 先写暴力递归,再改DP。不少DP题先用递归+备忘录写成,再把递归改成迭代填表就完成了。这个过渡很丝滑,比直接凭空想转移方程简单得多。
5.3 图论算法学了就忘,本质原因是啥
图论算法忘得快,基本是因为没有建立“图 = 抽象关系”的直觉。图的节点可以是城市、网站、人、状态,边可以是道路、超链接、人际关系、动作。如果你只把图看成“一堆圆圈和箭头”,那你学Dijkstra就是在“背模板”,能不忘了。
我的建议是给每个图论算法配一个具体的“非典型场景”:
- DFS/BFS → 迷宫寻路、联通块
- 拓扑排序 → 课程先修顺序、编译依赖
- Dijkstra → 地铁最短换乘
- Floyd → 任意两地的最短直达价格
- Tarjan强连通分量 → 游戏中哪些账号构成一个“死循环群聊”
一旦这些场景扎根在脑子里,再去套算法代码,就不会再感到抽象了。
5.4 刷题刷了很多,但面试还是写不出来
这个问题非常真实,因为它指向“输入性刷题”和“输出性刷题”的区别。你可能看了很多题解,你以为你会了,但关上屏幕一片空白。这不是记忆力差,是缺少“从零开始构建”的训练。
我后来强制自己用“白板模式”刷题:不看IDE的语法高亮、自动补全、编译提示,直接在编辑器里新建一个空白文件,从上到下把函数签名、循环、边界条件全部敲出来,一次写对,不允许试错式地“改几行跑一下”。这个过程刚开始非常痛苦,但大约坚持三周后,写代码的连贯性和代码的准确度会明显提升。
还有一个更狠的办法:限时15分钟写出暴力解,然后再花15分钟优化。面试考点的本质不是难题偏题,而是你在有压力的情况下能不能写出可工作的代码。平时这样练,面试才不发慌。
5.5 数学基础不够,算法要不要先停下来补高数
不要停。这是我踩过得比较深的坑。我在学机器学习的时候,一度陷入“要看懂所有推导”的执念,结果花了两周去翻概率论和线性代数教材,然后发现自己把算法的Coding都落了。后来我调整策略:先知道这个数学公式是干什么的,再在需要时深入查细节。
比如SVD分解,你不需要先精通线性代数再学推荐系统,你只需要知道“SVD能把一个大矩阵拆成三个较小矩阵的乘积,用于降维”就够了。等到面试里真遇到“说说SVD在推荐里的应用”,你再补特征值、奇异值的细节也来得及。学习路径上“用”永远是第一位的,“懂原理”排在后面。
6. 51天学习法的复盘:什么可以坚持,什么必须放弃
聊了这么多算法细节,最后我得说说这51天的学习体感,可能比技术本身更值得参考。
这51天我坚持下来最重要的是三件事:
第一,每天固定时间学算法,雷打不动。我选的是早上出门前那40分钟——头脑最清醒,用来读贪心、写DP、推KMP都合适。晚上太累、周末太散,都不如早晨稳定。算法学习最怕“三天打鱼两天晒网”,因为它的知识链条特别长,断两天就跟不上。
第二,每天只学“一招”,绝不贪多。所谓“招”,指一个算法的最核心思想,比如“归并排序的分割合并”、“Floyd的中转点枚举”、“KMP的最长相同前后缀”。一天学一招,一招吃透,比一天扫十个算法再忘八个强太多。只有当你能用自己的话把这个“一招”讲出来,才算今天完成任务。
第三,每周花一天做“乱序复习”。我从抽屉里随机抽卡片,随机做3道不同类型的题,不限定章节,这样就逼自己在脑子里建立“跨算法检索”的能力——不是按目录找答案,而是按问题特征找答案。这一招是我认为51天里收益最高、最反直觉的练习。
必须放弃的是什么呢?
一是放弃“看懂等于学会”的幻觉。任何算法,只要我没能合上书写出来、没有亲手跑出正确结果,我都视作未完成。这个标准一度让我很灰心,但通过率恰恰暴露了我的学习盲区。
二是放弃“追求完美笔记”的执念。我早期花了大量时间把笔记排版得漂漂亮亮,后来发现这种笔记基本不翻开看第二遍。真正有用的笔记是黏在算法旁边的注释、随手画的示意图、调试时的报错记录。请记住:笔记是给未来的自己看的“思维快照”,不是出版书籍。
三是放弃“一刀切刷题量”的虚荣。我试过LeetCode每天10题,过瘾,但三天后几乎全忘。同样时间,与其刷10道水题,不如把1道难题从暴力解、到优化解、再到面试口述梳理流畅,这个深度大于那个数量。
如果你也在学算法,我的忠告是:别怕慢,怕的是用“看起来很勤奋”的方式原地踏步。每天一片面包,慢慢叠成一座面包山,总有一天你回头看会吓一跳。51天不算长,但它足够证明,这个“更弱智”的笨办法,真的能让你从算法门外汉,变成一个敢在面试官面前坦然写代码的人。
接下来我打算继续往深度方向走:刷穿动态规划题单、补完Tarjan的边双联通分量、把强化学习从Q-Learning推到策略梯度。这条“更弱智”的学习路还没走完,如果你也在路上,一起慢慢爬。