我从6月中旬开始更新这个系列,到今天已经是第56天了。说实话,当初起名“更弱智的算法学习”的时候,纯粹是为了自嘲——因为我真的见过太多“三天精通动态规划”“七天拿下力扣周赛”的爽文式教程,看完只会让人更焦虑。我想记录的是一个人真实的学习过程:会卡壳,会忘记,会AC了但看不懂题解,也会在深夜怀疑自己是不是脑子不够用。到现在56天过去,回头看这种“弱智式”的记录反而成了最珍贵的资产。它证明了算法学习不是一条直线,而是一条螺旋上升的路。这篇文章就想把这一路走来的实操干货、踩坑记录和心态变化做个系统的梳理,给同样在摸索的朋友一些可以参考的经验。
1. 为什么叫“更弱智”:先把学习心态摆正
1.1 承认自己会卡壳,反而学得更快
我见过太多人学算法死在第一步:看题解觉得懂了,关掉页面自己写,卡了半小时就开始怀疑人生。其实卡壳太正常了。我的经验是,每道题第一次独立写不出来,才是真正的学习机会。那些一遍就AC的题,往往过一个月就忘了;反而是卡了几天、翻了题解还似懂非懂、最后终于想通的题,会真正长在脑子里。
这个系列的定位就是记录这种“卡壳与破解”的过程。我不追求每天刷很多题,而是每天聚焦一到两个核心思路,把“为什么这么做”讲透。比如斐波那契数列,青铜解法是递归,白银解法是记忆化搜索,黄金解法是滚动数组,钻石解法是矩阵快速幂——每个层级的跨越,都是对问题本质的一次重新理解。
1.2 用“教”的方式来学
有一个很有效的学习方法:把一道题讲给完全不懂的人听。如果你能让他听明白,说明你是真懂了;如果讲着讲着自己都绕进去了,那说明还有盲区。我写这个系列的时候就用了这个逻辑:每学习一个算法,我都要假设读者是个零基础的小白,从最朴素的暴力解法讲起,再一步步优化。
讲的过程会逼你把每个细节都搞清楚。比如二分查找,大家都会背模板,但为什么是left + (right - left) / 2而不是(left + right) / 2?因为后者可能溢出。为什么用<=还是<?因为区间定义不同。这些细节如果你只是做题,可能永远注意不到,但一旦你要讲给别人听,就必须直面它们。
2. 前面55天我到底学了什么:一条清晰的进阶路径
2.1 从数组和链表开始建立地基
前两周是最难熬的。数组、链表这些数据结构听着简单,但真要动手写代码,边界条件能让人崩溃。我记得有一道简单的链表反转题,我第一次写的时候居然用了整整一个晚上,反复在纸上画图才搞清楚指针怎么指。但正是这个“笨功夫”让我后来学二叉树、图论的时候轻松了很多。
这个阶段我认为核心要掌握三个基本能力:遍历的思想(循环不变量)、递归的思维(函数自己调自己)、复杂度的估算(大O表示法)。这三个能力不是学来的,是练出来的。所谓的数据结构入门,本质上就是反复练习这三种思维模式。数组的遍历是最简单的,链表开始引入指针和内存的概念,栈和队列则让你第一次接触“操作受限”的线性结构——这种限制反而是优势。
2.2 二叉树是很多高级算法的试验田
大概从第三周开始进入二叉树,这是整个算法学习中最关键的一环。二叉树的遍历(前序、中序、后序、层序)几乎覆盖了所有递归场景,而递归又是后续学习回溯、动态规划的基础。我自己的感觉是,二叉树学得好不好,直接决定了后面学图论和动规能走多远。
二叉树的题目还有一个特点:代码量少,但思维量很大。我见过很多人能背出三种遍历的递归模板,但一遇到“找最近公共祖先”“把二叉树展开为链表”这种需要变形的题就懵了。原因在于对递归的理解停留在了背诵层面,没有真正理解“递归是处理子问题”的本质。如果你现在也卡在这一层,我的建议是不要急着刷题,先回到最原始的递归定义,理解函数调用栈的变化过程。
2.3 回溯算法:暴力枚举也能很有章法
回溯算法是我个人认为最接近“暴力解法”的算法,它的核心思想就是穷举,只不过是有策略地穷举——走不通就回头。全排列、组合、子集这三大件搞明白之后,八皇后、数独这类经典题就只是套模板的问题。
这里有一个关键的心法:画递归树。我学回溯的时候,每道题都会先把递归树画出来,标清楚每一层在做选择,然后是进入下一层递归,再是撤销选择。画完递归树,代码基本上自己就浮出来了。面试中回溯题很容易被要求剪枝优化,这时候递归树又变成了分析剪枝条件的工具——哪些分支不可能产生有效解,可以直接砍掉。
2.4 动态规划和贪心:最考验“题感”的两个算法
从day30左右开始进入动态规划,这个阶段是我整个系列里掉头发最多的时期。动态规划不是算法,而是一种思维方式的转型——从“怎么一步步做”到“怎么把大问题拆成小问题,再从小问题的答案推出大问题的答案”。
我的经验是,动规入门分为三个台阶:第一步是能识别出这是一道动态规划题(有重叠子问题和最优子结构),第二步是能定义出状态(dp数组代表什么含义),第三步是能写出状态转移方程。很多人在第一步就卡住了,因为识别题目类型本身就需要一定题量作为支撑。我的做法是:每个动规类型(01背包、完全背包、线性动规、区间动规)都集中刷10道以上的题,形成对题型的“肌肉记忆”。这种填鸭式的练习看似“弱智”,但确实是目前我见过最高效的入门方式。
3. 实操篇:一套可复制的刷题方法论
3.1 每日学习流程:两小时也能有高质量产出
我给自己定的节奏是每天两小时,不多不少。前15分钟回顾昨天的题目,不看代码,在白纸上重新写一遍思路;接下来的1小时学习一个新知识点,对应做2到3道题;最后15分钟整理笔记,记录今天的“爽点”和“卡点”。
这里有一个容易被忽略的细节:回顾比学新知识更重要。很多人刷题只求量,今天做5道新的,明天开始做另外5道新的,过一周回头看,上一周的题全忘了。我自己在day20之后调整了策略:每周日不学新知识,只做本周出现过的题目,要求自己不看题解复现AC。这个方法很笨,但对巩固理解立竿见影。
3.2 题目分级:把一道题吃透比浅尝十道更值钱
我自己的实践是给题目打三颗星的难度:
- 一颗星:看完题解能写出来,但独立做有难度。这类题过一遍即可,不需要恋战。
- 三颗星:独立写了一个小时以上才AC,或者对照题解理解了多个解法。这类题必须复盘,一周内重做一遍。
- 五颗星:完全没思路,看懂题解都费劲。这类题是“学习区”的核心,要拆成多个晚上慢慢消化,必要时写一篇详细题解记录下来。
我把这个分级写在刷题笔记的最前面。现在所有的题目打上星级,方便每周复习的时候按优先级来。
3.3 错误记录本:把每次RE调试的绝望变成财富
刚开始刷题时我特别容易在同一个错误上栽跟头:数组越界、空指针、整数溢出、边界条件写错。后来我专门建了一个错误记录文档,每次报错调了超过5分钟就记一笔,记上错误类型、报错信息、出错的代码片段和修复方式。
这个文档积累到第56天已经有一百多条记录了。定期翻阅会发现很多有意思的规律:比如我80%的递归错误都是忘了写终止条件;90%的二分错误都是死循环(left指针没移动)。带着这些规律去做题,排查bug的速度快了很多,因为这些已经变成了条件反射。
4. 常用模板与套路:背下来就能用
4.1 二分查找的三种写法
二分查找是面试高频考点,但它的边界条件极其容易出错。我整理了三种写法:
# 写法一:闭区间 [left, right] def binary_search(nums, target): left, right = 0, len(nums) - 1 while left <= right: mid = (right - left) // 2 + left if nums[mid] == target: return mid elif nums[mid] < target: left = mid + 1 else: right = mid - 1 return -1 # 写法二:查找第一个不小于 target 的位置 def lower_bound(nums, target): left, right = 0, len(nums) while left < right: mid = (right - left) // 2 + left if nums[mid] >= target: right = mid else: left = mid + 1 return left闭区间写法适合标准查找,左闭右开写法适合lower_bound/upper_bound这类变体。每道二分题先明确区间定义,再动手写循环,基本不会出错。
4.2 回溯问题模板:选择、递归、撤销
回溯问题的代码框架非常固定,掌握一个模板就能通吃大部分组合类问题:
def backtrack(路径, 选择列表): if 满足结束条件: 记录结果 return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择这个模板的“灵魂”不是那三行代码,而是画递归树的过程。每次我发现写不出来,就回到纸上画树,把每个节点的状态标清楚。另外注意剪枝条件的添加位置——它应该在进入递归之前,而不是进入之后,否则就变成了“事后补救”,效率低很多。
4.3 动态规划状态定义的常用套路
状态定义是最难的环节。我总结了一些套路性的规律:
- 线性序列问题(如最长上升子序列):
dp[i]通常表示以第i个元素结尾的某种最优值。 - 双序列问题(如编辑距离):用二维数组
dp[i][j]表示第一个序列前i个与第二个序列前j个的某种关系。 - 背包问题:
dp[i][j]表示前i个物品放入容量为j的背包能获得的最大价值。 - 区间问题:
dp[i][j]表示区间[i, j]上的某个最优值,通常从小到大枚举区间长度。
状态定义对了,转移方程是顺水推舟的事;状态定义错了,转移方程永远写不出来。所以我会花大量时间在状态定义上,宁可在草稿纸上想清楚再写代码,也不急着键盘敲起来。
5. 踩坑记录:这些bug我调了一整晚
5.1 递归超时却没有死循环
有一道斐波那契数列的题,我用了递归写法,实测计算到n=40就卡得不行。当时我以为是代码死循环了,翻来覆去看了一个小时,才发现纯粹是指数级时间复杂度导致的超时。这个经历教会我:遇到运行慢的问题,第一步应该做复杂度分析,而不是盲目找bug。递归版本的斐波那契复杂度是O(2^n),n稍微大一点就会爆炸,这种问题应该用记忆化搜索或者迭代来解决。
5.2 全局变量引发的“灵异事件”
在写回溯算法时,我把结果列表定义为全局变量,然后在函数内部对它进行修改。结果发现同一份输入跑多次得到的结果居然不一样,查了很久才发现:上一轮递归留下的状态没有清理干净,导致全局变量被污染。后来我形成了一条铁律:递归或回溯中,每一次进入新的一层时,状态必须是全新的;必须用“拷贝”或“恢复现场”来维护变量,绝不要依赖全局变量在递归中自动归位。
5.3 骚操作:测试用例不行,但提交却AC了
有几次我在本地跑示例输入,答案和自己的预期不一样,但提交到判题系统却AC了。这种最迷惑。后来我知道了:力扣这类平台判题用的测试用例,本来就包含多个case,你本地只看到第一个case的输出,而AC说明全部case都通过了。这也提醒我,自己测试时一定要覆盖多种边界情况(空数组、单元素、全相同、逆序等),不要只看题目给的示例。
6. 从day56之后:下一步的进阶方向
6.1 从“会做题”到“会选算法”
刷题到一定程度后,你会发现难点不是“怎么写代码”,而是“该用什么算法”。比如看到“最大值最小化”想到二分答案,看到“方案数”大概率是动态规划,看到“子串”可能要滑窗或KMP,看到“连通性”就想到并查集或DFS/BFS。这个识别能力需要大量题目积累,也是我从现在开始刻意训练的。
6.2 系统学习高级数据结构
基础算法掌握之后,可以逐步接触线段树、树状数组、并查集、平衡树等。这些结构不是面试必备,但对提升思维深度非常有帮助。我给自己定的计划是:day60开始接触树状数组,day80左右上手线段树,每周末用一道中等难度以上的题目练手。
对我来说,这个系列的核心价值不在于教会了多少人算法,而在于用第56天的长度证明了:只要不放弃,哪怕是用“最弱智”的方式慢慢学,也能一天比一天靠近目标。
最后分享一个小技巧:我在笔记的每一页都标注了日期和当天的心情指数(1到10)。回看第5天“心情2,完全想放弃”的记录,对比今天的状态,会由衷觉得——那些卡壳的夜晚,原来真的没有白费。