经常有朋友问我:“算法到底是什么?是不是只有数学天才或者程序员才需要学?”我通常不急着下定义,而是先反问一句:你早上出门前,是先穿袜子还是先穿裤子?如果你有一套自己固定的顺序,而且每次都能顺利出门、不会穿反,那你其实已经在使用一个“算法”了。这篇长文不搞数学劝退,也不堆术语,我想用做菜、排队、找东西这些日常场景,把算法是什么、怎么衡量好坏、有哪些常见套路、怎么变成能跑的代码、以及怎么系统学好,一次性讲清楚。
可能有人觉得“算法”这个词离自己很远,但你在手机上点外卖,背后有排序算法帮你按距离排序;你在地图上查路线,背后有路径规划算法;你用搜索框找内容,背后有检索匹配的机制。算法不是漂浮在课本里的抽象概念,而是人类把经验整理成可重复执行步骤的产物。接下来的内容,我从零开始拆解。
1. 算法不是高深公式,而是一份“怎么把事情做对”的操作说明书
很多人一听到“算法”就联想到复杂的数学公式和天书般的代码,这其实是最大的误解。算法的本质非常朴素:它就是一套为了解决问题而设计的有穷、明确、可执行的步骤序列。只要符合这几个条件,哪怕你不用电脑、不用代码,在纸上画一画,也算是在设计算法。
1.1 从“番茄炒蛋”理解算法的三个关键零件
我们可以把做番茄炒蛋当成一个算法来看。它天然包含三个关键零件:输入、处理步骤、输出。
- 输入:番茄、鸡蛋、食用油、盐、葱花,可能还有一把锅铲。
- 处理步骤:打蛋 → 切番茄 → 热锅倒油 → 先炒蛋并盛出 → 再炒番茄 → 把蛋倒回去混合 → 加盐调味 → 出锅装盘。
- 输出:一盘配着米饭吃的番茄炒蛋。
如果你把步骤顺序换一下,比如先放盐再打蛋,或者番茄还没切就倒进油锅,最后成品大概率不对劲。如果漏掉某一步,比如忘了打蛋,那端上桌的就是一碗炒番茄。这和算法很像:步骤的次序和完整性,直接决定结果的正确性。
为什么我要从做菜讲起?因为做菜这件事,每天都在告诉我们同一件事:一个复杂任务完全可以靠明确步骤被稳定复现。算法做的就是这个事,只不过它不局限于厨房,而是可以套用在数学问题、数据处理、路径搜索等无数场景中。
1.2 算法随身带的五个“证件”:有穷、确定、可行、输入、输出
计算机科学里对算法有个经典描述:一个合格的算法必须满足几个基本特性。我不打算把教材定义背给你听,但你可以把这些特性理解为算法随身携带的五张证件。
- 有穷性:算法必须在有限步骤内结束。如果设计出来的规则永远跑不完,那就是死循环,不是算法。比如“走出迷宫,直到出不去为止”听起来没问题,但如果迷宫真的没有出口,这个规则就无法保证结束。
- 确定性:每一个步骤都必须含义清晰、没有歧义。菜谱里写“加适量盐”就不是一个确定步骤,因为不同的人对“适量”理解不同。算法要求同样的输入,任何时候执行都能得到相同的结果。
- 可行性:每一步都得是执行者真正能做到的。“把水加热到一万度”对普通锅来说不可行,不能算有效步骤。
- 输入:算法可以有零个或多个输入。比如一个“生成随机数”的算法,输入可能只是一个种子值。
- 输出:算法至少要有一个输出结果。哪怕这个结果只是“成功”或“失败”,也必须明确告诉使用者发生了什么。
有两点值得注意:第一,“确定步骤”不等于“固定步骤”,算法里可以有“如果条件成立做A,否则做B”这种分支,但条件本身必须能被确定地判断;第二,算法不要求每一步都机械简单,但必须能被严格理解。
1.3 算法是思想,代码只是它的“译文”
新手最容易混淆的两个概念,就是“算法”和“程序”。我的理解是:算法是解决问题的思想,程序是这种思想在计算机上的具体表达。同一个“按身高从矮到高排队”的算法,你可以用 C 写,也可以用 Java 写,还可以用 Python 写,甚至可以不带任何电脑,带着全班同学在体育课上用两两比较换位置的方式完成。
我经常用一个类比来说明:菜谱是算法,厨师是执行者,最后端出来的那道菜是程序的运行结果。只要菜谱写得足够清晰,中餐厨师和西餐厨师都能做出同一道菜,只是手法和工具可能不同。同样,算法写好后,可以由不同的程序员翻译成不同语言,运行结果应该是一致的。
分清这两个概念之后,你的心态会好很多:你不需要先精通一门编程语言才能学算法,你完全可以先用文字或示意图把思路画明白,再去考虑怎么用代码实现。反过来,如果代码跑出来的结果不对,也不一定是算法本身有问题,可能是翻译过程中出了错。这种“先思想,后实现”的思维,是很多人没意识到的学习捷径。
2. 算法好坏怎么比:两把尺子叫时间复杂度和空间复杂度
既然算法是一套解决问题的步骤,那自然要问:这套步骤好不好?怎么判断它好不好?最朴素的方法是掐着秒表让两个算法分别跑一遍,看谁更快。但这里面有个问题:同一个算法,在不同电脑、不同数据规模下,表现可能完全不同。所以我们需要两把更抽象的尺子:时间复杂度和空间复杂度。
2.1 为什么不直接掐秒表
假设你有一个“把一万个数字从小到大排好”的算法。在最新款电脑上跑,可能是 0.1 秒;在一个老旧的设备上跑,可能要 10 秒。难道说明算法变差了吗?没有。硬件不同而已。再换一批数据,如果这一万个数本来就已经排好序,有的算法会跑得特别快;如果它们是完全乱序,有的算法又会特别慢。我们需要的不是一个会受环境干扰的“秒数”,而是一个能描述算法本身趋势的指标。
所以,计算机科学家们约定:不去数具体的执行秒数,而是数这个算法在最坏情况下大概要执行多少次基本操作。这里的基本操作可以是“比较一次大小”“做一次加法”“移动一次数据”。于是我们得到一个关于数据规模 n 的函数。比如一个算法要执行 3n + 2 次操作,另一个算法要执行 5n² + 2n 次操作,后者在 n 变大时会迅速变慢。
当然,实际分析时不需要精确到每一项。我们更关心的是“随着 n 不断变大,操作次数朝什么方向增长”。这个“增长方向”就是时间复杂度。
2.2 大O表示法:别被吓到,它只是“增长速度”
大O表示法是描述算法时间消耗最常见的方式。它不关心系数,也不关心低阶项,只看当 n 趋向无穷大时,操作次数的主导增长项。
- O(1):常数时间。不管数据规模是一百还是一百万,操作次数都固定。就像你直接打开冰箱门拿自己知道放在哪的那瓶牛奶,不需要翻遍整个冰箱。
- O(log n):对数时间。每次都能排除掉一大半数据,典型例子是二分查找。n 越大,增长速度越慢,非常理想。
- O(n):线性时间。操作次数和数据规模成正比。就像你在一排抽屉里找钥匙,每次只能看一个抽屉,最坏情况要把所有抽屉翻一遍。
- O(n log n):线性对数时间。很多优秀排序算法处于这个级别,比如归并排序。
- O(n²):平方时间。双重循环两两比较往往属于这类。数据规模翻一倍,时间变成原来的四倍。
为了更直观,我列一张表:
| 记号 | 通俗含义 | 典型场景 |
|---|---|---|
| O(1) | 与数据规模无关,固定几步搞定 | 数组按下标取元素 |
| O(log n) | 每走一步,问题规模少一半 | 在有序数组中查找 |
| O(n) | 从头到尾扫一遍 | 在无序数组中找最大值 |
| O(n log n) | 分半处理,再线性合并 | 归并排序、快速排序平均情况 |
| O(n²) | 双层循环,两两组合 | 冒泡排序、朴素嵌套循环 |
这里有个新手常犯的错误:总觉得 O(1) 一定比 O(log n) 好,O(log n) 一定比 O(n) 好。实际上,大O描述的是规模趋近无穷时的趋势,在小规模数据下,一个常数很大的 O(1) 算法,完全可能比 O(n) 算法更慢。就好比“打电话问朋友要密码”是 O(1),但如果每次都要打一通 10 分钟的电话,还不如自己花 30 秒翻一下本子。
2.3 同一个问题,不同算法差距能有多大
只讲理论太虚了,我们来算一笔账。假设一个有序数组里有一百万个元素:
- 线性查找:最坏情况要检查一百万个元素,也就是 O(n)。
- 二分查找:每次比较把范围减半,从 100 万减到 50 万、25 万、12.5 万……大约只需要 20 次比较。
20 次和 100 万次,这就是选对算法的力量。如果再放大到十亿个元素,二分查找也只需要大约 30 次,线性查找却可能要查十亿次。这种差距不是靠“电脑速度快一点”能弥补的,因为增长速度完全不同。
再来看排序。冒泡排序的平均复杂度是 O(n²),归并排序是 O(n log n)。当 n 等于十万时,O(n²) 意味着约 100 亿次操作,O(n log n) 大约只有 170 万次。虽然这些数字不是严格的真实时间,但足以说明:在数据规模上去之后,算法的选择决定了你是秒开还是等半小时。
当然,二分查找有一个前提:数组必须有序。如果你面临的是一个无序数组且排序代价太高,线性查找反而是合理的选择。算法选择从来不是“贵的就一定好”,而是“在给定条件和约束下找到最合适的方案”。
2.4 时间不够,空间来凑:经典的空间换时间
时间很重要,但并不是唯一指标。算法还有一个维度叫空间复杂度,也就是跑起来需要额外占用多少内存。很多时候,为了把时间从“慢得无法接受”降到“快得流畅”,我们愿意多花一部分内存,这就是“空间换时间”。
举一个最典型的例子:哈希表。如果不加任何结构,你要在一个数组里找一个指定值,只能从头线性扫,O(n)。但如果你在插入数据的同时,再维护一张“值 → 位置”的哈希表,查找时直接按索引去取,时间就变成 O(1)。代价是什么?是额外的存储空间。
另一个例子是“记住已经算过的结果”。计算斐波那契数列时,如果按最直接的递归方式,F(5) 会重复计算 F(3) 多次,规模一大就爆炸。但如果用一个数组把每个 F(i) 记录下来,每次直接取之前的结果,时间能从指数级降到线性级,代价只是多存 n 个数字。这种思路再往前一步,就是后面说的动态规划。
理解了这两把尺子,你已经比很多只会写代码的人高出一个层次。因为你能解释清楚:一个算法为什么快、快在哪里,一个算法为什么慢、慢在哪里。
3. 六种常见算法范式:解题套路比想象中少
有一类问题初学者会特别困惑:我连题都读完了,却完全不知道从哪里下手。其实算法世界里真正被反复使用的思维模式没有想象中那么多。我把最常见的几种范式拿出来说,它们就像解题的“套路”,学一个,就能解一批题。
3.1 穷举法:最笨,也最可靠
穷举法的思路是:把所有可能的情况都试一遍,从中挑出答案。比如找出一组数字中的最大值,你可以从第一个数字开始,挨个和当前最大值比较,最后留下最大的。道理直白,代码也好写,唯一的缺点是慢。
你可能觉得“穷举”太低级了。但在算法设计里,穷举永远是最重要的兜底方案。很多复杂问题没有现成巧法时,先暴力跑一遍能帮你确认问题的正确答案长什么样,然后再想办法优化。而且穷举也不是必须蠢蠢地全试,可以通过提前判断去掉明显不可能的情况,这个操作叫“剪枝”。哪怕写的还是穷举,效率也能提升不少。
3.2 贪心算法:每一步都选当下最好的
贪心的中文名字很形象:在每一步决策时都选择当前看起来最优的选项,期望最终结果也最优。它的思维是“走一步看一步,做眼前最好的选择”。
但贪心很容易翻车。举个例子:有 1 元、3 元、4 元三种面额的硬币,要凑出 6 元。按贪心思路,先取最大的 4 元,剩下 2 元只能用两个 1 元,一共 3 枚。可是最优解明明是 3 + 3,只需要 2 枚。这说明:局部最优不一定能得到全局最优。贪心能用的场景必须满足某种特殊性质,比如“每一步的最优选择不会影响后面步骤的选择”。
那贪心什么时候能用?经典例子是活动安排:你有一整天时间,面对很多场讲座,每场有固定的开始和结束时间,最多能完整参加几场?贪心策略是“每次选结束时间最早的讲座”,这种情况下它是可行的。所以不要一看到“最优”字样就无脑贪心,先问自己:局部最优能不能推导出全局最优?如果证明不了,就要考虑更稳的算法。
3.3 分治算法:大事化小,小事化了
分治法的思想有三步:把大问题拆成几个小问题,分别解决小问题,再合并结果。最有名的应用是归并排序。
假设你有一摞乱序扑克牌。最简单的分治做法是:把这摞牌从中间分成两半,分别把两半排好序,最后再把两个有序序列合并成一个有序序列。递归做下去,直到每份只剩一张牌时,它天然有序,不需要再排。合并两个有序序列也容易:每次从两个序列的头部挑一个更小的放入新序列。
归并排序的时间复杂度是稳定的 O(n log n),无论输入是正序还是逆序,表现都不差。它的核心价值在于:很多时候,“把整体排序”难,“把两个已经有序的部分合并”却很简单,所以分治能把复杂问题拆到不可再分,再逐层合并回来。你只要记住:“分”要能拆成同样类型的子问题,“治”要能把子问题的解顺畅合并成原问题的解。
3.4 动态规划:记住结果,别重复劳动
动态规划是对“重复计算”的正面反击。它把一个问题拆成重叠的子问题,然后把每个子问题的结果存起来,后面不再重复算。
还是用斐波那契数列举例。递归公式是 F(n) = F(n−1) + F(n−2)。如果你按 F(6) 递归去展开,会发现 F(3) 被反复算了多次。数据一肥大,这种重复计算会让程序慢到怀疑人生。动态规划的做法是:从底部开始,用一张表记录 F(0)、F(1)、F(2)……直到 F(n),每个值只算一次,取表里的结果拼接。
再举一个更生活化的例子:上楼梯。假设每次可以走 1 阶或 2 阶,想到达第 n 阶,共有多少种走法?实际上,到达第 n 阶的最后一步,要么来自第 n−1 阶跨 1 阶,要么来自第 n−2 阶跨 2 阶。所以方案数 F(n) = F(n−1) + F(n−2)。这也是动态规划。
动态规划通常有三个关键点:重叠子问题(问题可以被拆成重复出现的小问题)、最优子结构(整体最优解包含局部最优解)、状态转移方程(描述子问题之间怎么递推)。很多人卡在“状态转移方程”上,我的建议是:先别急着写公式,用小例子在纸上推一遍,从 F(1)、F(2)、F(3) 一个个往后列,规律自己会冒出来。
3.5 回溯算法:此路不通就退回重新走
回溯算法的思想特别像走迷宫:你从入口出发,沿着一条路一直走,走不通就退回上一个岔路口换一条路再试。它本质上是一种“带后悔功能的深度优先搜索”。
经典问题是八皇后:在 8×8 棋盘上放 8 个皇后,要求它们互相不能攻击。一个皇后的攻击范围为同行、同列、同斜线。回溯的做法是:逐行尝试,在当前行从左到右试每一列,如果这个位置和已放的皇后不冲突,就放下去,然后继续下一行;如果放到某一行发现所有列都被冲突,就返回上一行,把上一行皇后的位置往右挪一位,再重新试。这个过程会不断“剪枝”,也就是一旦发现当前局面已经没有希望,立刻放弃该分支。
回溯算法非常适合解约束满足问题:数独、N皇后、图的着色、括号生成等。初学回溯时最难理解的是“状态回退”:递归进入下一层之前修改了某个状态,等递归返回后要记得恢复。这个顺序一旦搞混,结果就全乱。我会建议在纸上画出递归树,用“前进、返回、改状态、再前进”的方式跟着走一遍,比光看代码有效得多。
4. 从生活问题到一段能跑的代码:完整实现一个算法的真实过程
前面聊了那么多思想和理论,可能你还是觉得“我还是不知道怎么动手”。这一节我带你完完整整走一遍:从一个实际需求开始,到写出能运行的代码,再测试边界情况。这个过程才是日常工作中真正每天都在发生的“算法落地”。
4.1 把需求想清楚:最大连续子数组和
我选一个经典问题,但它足够小,适合展示完整思路:给定一个整数数组,请你找出和最大的“连续子数组”,并返回这个最大和。所谓连续子数组,就是原数组中连续的一段,不能跳着取。
举个例子,数组是 [-2, 1, -3, 4, -1, 2, 1, -5, 4],最终答案是 6,因为连续段 [4, -1, 2, 1] 的和是 6。这个问题在现实中有很多变体:比如分析股票价格变化,找出连续几日涨幅最大的时间窗口;比如分析传感器数据流,找出异常累积最明显的区间。
在写代码之前,先约定输入输出:输入是一个整数数组,输出是最大连续子数组的和。那空数组怎么办?我下面先实现一个默认数组非空的情况,遇到空数组时抛出异常或返回约定的 0,关键是一开始就要讲清楚规则。这个约定过程就是算法的“确定性”要求。
4.2 先写伪代码:不要急着打开编辑器
很多人一拿到题就开始敲代码,我的习惯是先写“伪代码”,用普通人能看懂的自然语言描述步骤。伪代码可以帮你把“思路”和“实现细节”分开。
最直接的暴力思路是:枚举所有可能的起点 i 和终点 j,计算从 i 到 j 的和,然后不断更新最大值。
best = 负无穷 for i in 0..n-1: for j in i..n-1: sum = 0 for k in i..j: sum = sum + arr[k] best = max(best, sum) return best这个三层循环的逻辑完全正确,但它有三层嵌套,时间复杂度是 O(n³)。如果数组有 1000 个元素,就大约要执行 10 亿次运算,明显不理想。但它的好处是容易想、容易写、不容易错。在实际工作中,如果数据量不大,先跑通一个暴力解完全没毛病。优化要建立在“正确”的基础上,而不是一上来就追求高端方法。
4.3 线性扫描实现:从 O(n³) 到 O(n)
上面暴力解慢,是因为每次算 sum 都从零开始加一遍。仔细观察可以发现:当 j 往后移动时,新的连续和完全可以由上一个连续和接着加一个元素得到,而不是重新算。于是我们得到一个更聪明的做法,叫线性扫描。
核心思路是:遍历数组时,维护两个变量:
current:以当前元素为结尾的连续子数组的最大和。best:到目前为止见过的最大和。
每次遇到一个新元素x,我们面临两种选择:要么把x接到前面的子数组后面,即current + x;要么从x重新开始一段新的子数组。取二者中较大者,更新current,再把它和best比较。写成 Python 就是:
def max_sub_array(nums): best = float("-inf") current = 0 for x in nums: current = max(x, current + x) best = max(best, current) return best这个代码短得惊人,但它背后是一个典型的动态规划思路。如果你用 dp[i] 表示以第 i 个元素结尾的最大子数组和,那么状态转移方程就是:
dp[i] = max(nums[i], dp[i-1] + nums[i])每次只要记录上一轮的current,就能递推全数组。这正好印证了前面的第 3 节:重叠子问题存在于“前一段的最大和”中,我们把它记下来,避免重复计算。
4.4 用边界条件检验算法是否正确
写出代码只是第一步,真正考验算法的是边界。我用几个测试用例跑一遍:
print(max_sub_array([-2, 1, -3, 4, -1, 2, 1, -5, 4])) # 预期 6 print(max_sub_array([-1, -2, -3])) # 预期 -1 print(max_sub_array([5])) # 预期 5 print(max_sub_array([1, 2, 3])) # 预期 6第一个例子对应 [4, -1, 2, 1] 的和 6。第二个全是负数,可能有人会误以为答案是 0,但按照“必须选非空连续子数组”的约定,正确答案是所有负数里最大的那个,也就是 -1。算法里current = max(x, current + x)保证了它不会把更小的负数累加下去,best会在遍历中抓到最大的负数。第三个和第四个是退化情况,只有一个元素和全正数,线性扫描都能正确覆盖。
注意:如果数组可能为空,
best会保持负无穷,这显然不是好结果。实际工程里应该提前约定:空数组返回 0,或者抛出一个明确异常。算法输入输出的确定性,一定要在实现之前想好。
4.5 从暴力到线性:优化不是炫技
我经常和一些初学者强调:能想出 O(n) 解当然好,但不要因此看不起暴力解。从三层循环到线性扫描,不是靠灵光一现,而是通过观察“重复计算”然后想办法消除。暴力解让你理解问题,优化解让你理解结构。
如果数组长度是 10 万,O(n³) 的暴力解基本不可能跑完,而线性扫描只需要遍历 10 万次,几乎是瞬间完成。这个差距,就是复杂度分析在实际问题中的价值。以后你遇到一个陌生问题,先不管美观,先写一个正确的笨解法,再一点点优化,这条路是走得很踏实的。
5. 想真正学会算法:三个常见误区加一条稳妥路线
讲到这里,你已经把算法的核心概念过了一遍。但我知道很多人真正关心的问题是:那我该怎么继续学下去?说实话,算法这个领域劝退过很多人,其中相当一部分不是智商不够,而是走错了路。我总结三个最常见的误区,再给一条我自己验证过很多次的学习路线。
5.1 误区一:一上来就啃数学证明,把自己劝退
算法教材里写满了正确性证明、复杂度推导,这些东西当然重要,但它们不该是初学者第一眼看到的内容。我见过太多人翻开一本经典算法书,第一周就被归纳证明和渐近分析吓退了。
我的建议是:第一遍学算法,以“能看懂思路、能写出代码、能跑通用例”为目标。比如你刚接触二分查找,先用一个有序数组在纸上模拟:中间的元素比目标大,就砍掉右半;比目标小,就砍掉左半。你完全可以在不懂数学证明的情况下体会到“每次排除一半”的快感。等后续你有经验了,再回头补证明,那时候你会发现原来那些符号都是对过程的精确化表达,一点都不吓人。
5.2 误区二:只刷题不归纳,做了几百道还是不会
很多同学喜欢用“刷题数量”衡量算法能力,结果刷了几百道,遇到新题还是懵。这背后的原因不是题目做少了,而是没有把题目变成自己的套路。
我建议每做一道题,都问自己三个问题:这道题属于什么类型?用到了哪种算法范式?还有没有其他解法?当你做完一定数量的题目后,试着按类型归档:看到“数组里找两个数满足某种关系”,先想哈希表;看到“有序数组查找”,先想二分;看到“求最优解且有重叠子问题”,先想动态规划。这种归纳能力比多刷十道题有用得多。
例如最大子数组和这道题,如果只看答案,你记住了一个线性扫描代码,但如果你把它归档到“动态规划:状态转移方程是 dp[i] = max(nums[i], dp[i-1] + nums[i])”,你以后遇到“最长递增子序列”“打家劫舍”这类问题时,就会下意识寻找转移关系,而不是束手无策。
5.3 误区三:只看不写,觉得看懂了就是会了
算法是动手技能,不是阅读技能。看视频、看文章时你觉得自己全都懂了,但只要一合上页面,让你从头写一遍,很可能卡在第一行。这个现象太正常了,因为“看懂”是别人替你把逻辑走通了,你的大脑还没有建立自己的路径。
所以每次学完一个算法,一定要亲手把它写出来,最好用笔在纸上先模拟一遍过程,再打开编辑器敲一遍代码。我还有一个土办法:把一个算法讲给身边的人听。如果你能让他听懂,说明你真的理解了;如果你讲着讲着发现自己都说不圆,那就说明还有漏洞需要补。这种输出式学习方法,比反复看书管用得多。
5.4 一条可行的学习顺序
我给零基础学习者推荐这条路线,按顺序来不容易崩溃:
- 先学基础数据结构:数组、链表、栈、队列、哈希表、树、图。算法是长在数据结构上的,不知道数组怎么按下标取元素、不知道链表怎么遍历,后面很多算法根本无从谈起。
- 再学排序与检索:冒泡排序、插入排序、选择排序、快速排序、归并排序、二分查找。不要觉得“现在编程语言都有现成排序函数”,就不需要学排序。排序是理解时间复杂度、递归、分治思想的最好土壤。
- 再学算法范式:穷举、贪心、分治、动态规划、回溯。前面我讲的那些套路,值得每一个都亲手实现一遍,并且找对应的练习巩固。
- 最后培养复杂度直觉:看到一段代码,能大概说出它是 O(n)、O(log n) 还是 O(n²)。一开始不需要严格推导,能区分量级就够了。
5.5 每周吃透一个算法,比一天刷十道更有用
最后分享一点我的个人经验。我见过很多同学给自己定“每天刷五道题”的目标,坚持两周就放弃了,因为目标太粗,反馈也来得太慢。后来我改用另一种方式:每周只研究一个主题,比如“本周主角是二分查找”。
我会先找一个实际问题,比如在一份已经按时间排序的温度记录中,找到第一次超过某个阈值的日期;然后用纸笔写出二分思路,再用代码实现;最后再找两三个变体题目,比如“在旋转过的有序数组里找目标值”。周末的时候,我会把自己对这一周算法的理解写成笔记,确保下次再见到这类问题能立刻反应过来。坚持三个月左右,算法的“套路感”就慢慢长出来了。
算法不是一门靠死记硬背的学科,它更像一套思维工具。当你带着“解决实际问题”的眼光去学,而不是抱着“应付面试”的心态去背,你会发现那些看似抽象的概念,其实每一条都是从真实需求里长出来的。希望这个长文能把你的第一块基石放稳,剩下的路,咱们边写边学。