
我第一次认真琢磨“算法”这两个字不是在学校课堂上而是在一次真实的程序性能调优里。当时有个接口要把几万条记录逐个比对数据量一上来响应时间从几十毫秒一路飙到几秒用户开始投诉我对着日志一筹莫展。后来把无脑遍历的写法换成带索引和排序的思路接口直接快了一个数量级。那一刻我才明白写代码和写好算法的差别有时候不在语法而在你选择用什么步骤去解决问题。这篇笔记是《算法入门》系列的第一篇专门把最底层的问题聊透算法到底是什么它能解决什么问题适合刚接触编程的大学生、要准备蓝桥杯或LeetCode刷题的人、以及算法工程师面试前想建立整体知识框架的求职者。我会尽量少拽数学公式多用生活例子和实际代码讲清楚定义、复杂度、常用算法类型最后再分享几条我自己踩过坑之后总结出来的学习建议。1. 先搞清楚算法到底是个什么东西1.1 给朋友泡一杯咖啡就是一套算法假如你招待朋友要泡一杯拿铁你会不会下意识按照某个顺序操作先称咖啡豆再研磨烧水到合适温度萃取浓缩打奶泡最后拉花。这一串动作有明确的先后顺序有输入豆子、水、牛奶有输出一杯咖啡有约束水温不能太高时间不能太长也有终止条件咖啡液接够就停。如果哪一步乱了或者省略了成品就会跑偏。计算机算法本质上也是这么一回事。它是一套“解决问题的操作步骤说明书”把数据从一端送进去经过有限步骤的处理在另一端给出结果。很多初学者一看到“算法”两个字就联想到高深论文其实你每天在生活里做的决策、排序、查找都在无意识地使用算法思维。只不过计算机比人更死板它不会自动补全你没说清楚的步骤所以我们必须把这套说明书写得非常精确。1.2 教材定义太抽象我用“菜谱”来理解教科书里的标准定义是算法是对特定问题求解步骤的一种描述是指令的有限序列。展开来看它要求解决问题的方法满足有穷性、确定性、可行性和输入输出等条件这些细节我放到第二部分细说。我更喜欢的类比是“菜谱”。菜谱规定好食材、火候、调料和出锅时机程序员写算法就是在给计算机写菜谱。菜谱好不好直接决定菜好不好吃算法好不好直接决定程序跑得快不快、占的内存多不多。于是就有了一个经典对比暴力枚举和二分查找。假设有一个按升序排列、长度一万的数组你想知道目标值在不在里面。暴力枚举从第一个元素挨个往后找最坏要看一万个元素二分查找每轮砍掉一半区间最多只要十四次左右。同一个问题方案不同时间开销差了好几个数量级。这就是我们为什么要学算法不是为了显示自己多有技术含量而是为了用更少的算力解决更大规模的问题。2. 一个合格算法的基本特性以及怎么判断它好不好2.1 五个基本特性有穷、确定、可行、输入、输出先看有穷性。算法必须保证在执行有限步骤之后结束。你写一个while循环如果逻辑上永远跳不出去那它就不是一个合格算法而是一个故障程序。再看确定性。算法的每一步都应该是明确的不能有“随便拿一个合适的元素”“找个合适的人”这种模糊表达。计算机没有任何常识来帮你猜“合适”是什么意思你必须把规则定义到机器能执行的程度。可行性指的是每一步都能在有限时间内完成。你不能在算法里写“从全宇宙所有星球里挑一个当结果”因为这一步谁都做不完。输入输出也容易理解算法可以没有输入比如固定计算某个常数但它必须有输出。哪怕是输出一行日志也得给外界一个反馈。这里有个初学者容易混淆的点算法不等于代码。同一个算法可以用C写用Python写甚至用自然语言描述。代码只是算法的载体之一。所以我在学习时一直提醒自己我学的是一个思路而不是某一种语言的固定写法。2.2 大O记号给算法的效率贴上标签评判一个算法好坏最核心的两个指标是时间复杂度和空间复杂度它们都用大O记号来表示比如O(1)、O(n)、O(n^2)、O(n log n)。怎么理解大O它不关心你的机器快不快只关心随着输入规模n的增大算法耗时增长的趋势是什么样的。访问数组下标是O(1)因为不管数据多大一次定位就搞定顺序查找是O(n)数据翻倍耗时大约也翻倍双重循环嵌套里常见的暴力算法是O(n^2)数据一变大耗时增长得相当肉眼可见归并排序、堆排序这种高效算法普遍在O(n log n)听起来多了个log但和O(n^2)差了十万八千里。我建议把复杂度理解成“增长曲线”而不是去背一串公式。面试算法岗的人十有八九会问这两个方案都能出结果为什么这个更好你只要能把复杂度这个底层逻辑讲清楚就已经赢过很多只会背代码的人。常见的复杂度从优到劣大概是O(1)恒定时间比如哈希表查找O(log n)对数时间比如二分查找O(n)线性时间比如顺序遍历O(n log n)接近线性但是带一点对数开销比如快排、归并O(n^2)平方级比如两层循环的暴力解法O(2^n)、O(n!)指数级和阶乘级n稍微一大就会爆炸2.3 动手算一遍从顺序查找到二分查找来看一个最简单的例子。有一个长度为n的有序数组我们要判断某个值是否存在。顺序查找的代码是这样def linear_search(arr, target): for i in range(len(arr)): if arr[i] target: return i return -1最坏情况下目标在数组末尾或者不存在循环要执行n次复杂度就是O(n)。如果数组有序用二分查找def binary_search(arr, target): left, right 0, len(arr) - 1 while left right: mid (left right) // 2 if arr[mid] target: return mid elif arr[mid] target: left mid 1 else: right mid - 1 return -1每轮循环都会把搜索区间缩小一半。最多需要执行多少次就是log2(n)次所以复杂度是O(log n)。当n等于一百万时顺序查找最坏要看一百万个位置二分查找大约只需要二十次。在实际工程里这二十次和一百万次的差距就是一次快速响应和一则用户投诉的差距。于是复杂度不只是考试概念更是工程里的金钱和时间。很多系统优化本质上都是把高复杂度的操作想办法降一个等级比如加缓存、加索引、改数据结构。想清楚这一步你就已经算正式踏进算法大门了。3. 算法世界的常用套路查找、排序、搜索与图论3.1 查找算法从线性遍历到哈希表、再到KMP查找是最基础、出现频率最高的问题类型。顺序查找最简单但性能一般二分查找对数据有序有要求哈希查找则是工程里最常用的方案因为它能利用哈希表把查找时间压到接近O(1)。想想手机通讯录里的号码你按名字直接翻到那一页而不是从第一个联系人挨个往下读。哈希函数就是帮你快速定位“那一页”的算法。字符串匹配里的KMP算法也是经典中的经典。比如在一篇长文章中找一个关键词朴素做法是从每个字符位置开始逐个向后比对文章长度n、关键词长度m最坏复杂度是O(n*m)。KMP的核心思想是利用已经匹配过的信息让主串的指针不回头把匹配过程优化到O(nm)级别。很多人在KMP的next数组推导上翻过车我的建议是先亲手拿纸笔演算几次前缀后缀的匹配过程再去背代码模板否则很容易越看越晕。查找问题并不只在数组和字符串里出现。在树结构里查找如果树是平衡的复杂度能做到O(log n)在红黑树、B树这种工程结构里查找、插入、删除都能保持稳定性能。这也是为什么数据库索引偏爱B树。数据结构与算法从来不分家这一层联系在第四章我会专门展开讲。3.2 排序算法冒泡、归并、快排、堆排序怎么选排序是每个入门者都绕不开的练兵场。冒泡排序大概是很多人写出来的第一段排序代码每一轮把相邻元素两两比较大的往后挪像气泡一样冒出水面。代码好写但复杂度是O(n^2)数据一多就会很吃力。我见过不少人拿冒泡去排序几十万元素然后抱怨运行慢其实不是语言问题是排序选型问题。归并排序的思路是“分而治之”不断把数组拆成两半分别排好序再合并起来时间复杂度稳定在O(n log n)缺点是合并时需要额外O(n)空间。快速排序同样基于分治平均复杂度O(n log n)思路是选一个基准小的放左边大的放右边再递归处理两侧。它还有各种优化版本比如随机选基准来避免最坏情况。堆排序利用堆这种数据结构维护最大或最小元素同样在O(n log n)级别而且不需要太多额外内存。我给初学者的建议是先把插入排序和冒泡排序写熟理解它们为什么慢再手写归并和快排体会递归和分治思想最后接触堆排序因为堆的概念后续在优先队列、Top K问题里会反复出现。这里给出一个具体的练法拿一个长度为8的乱序数组手动跑一遍快速排序。每选完一轮基准在纸上画出左右两个区间再递归地重复。“放到左边的总是比基准小放到右边的总是比基准大”这句话只有当你亲眼在纸上看见它才算真正理解。面试手写快排时只要脑海里的这幅图景清晰手指自然能跟上而不是死记硬背某个模板。3.3 暴力枚举、回溯与剪枝以八皇后为例暴力枚举是一种最朴素也最容易被想到的方法把所有可能性列出来再一一验证。比如找出数组里所有和为某个值的组合最简单的方案就是多重循环。但暴力枚举的代价是复杂度爆炸如果每个位置有k种选择要处理n个位置复杂度就是k的n次方n稍微一大机器根本算不完。这时候就要用回溯加剪枝。以八皇后问题为例要在8×8的棋盘上放8个皇后要求任意两个皇后不能在同一行、同一列、同一对角线上。暴力方案是把所有摆放组合都生成再过滤数量级大得离谱回溯方案是逐行放皇后每次落子前检查当前是否冲突冲突就回退到上一行重新试。剪枝则是在一条路径注定无解时提前砍掉不再继续深入从而大幅减少搜索分支。写八皇后这类题很容易让人体会到“搜索树”这个抽象概念。它不要求你真的画出一棵树的图片但心里要清楚每一步尝试会展开哪些分支哪些分支已经被证明是死路剪枝就是把这些死路提前堵上。一旦想通这一点后续学习深度优先搜索、广度优先搜索乃至更高级的启发式搜索都会顺很多。3.4 图论与更广阔的算法地图图论是算法领域里非常庞大的一块。最短路径有Dijkstra和A*最小生成树有Prim和Kruskal强连通分量有Tarjan二分图匹配有匈牙利算法。不管你是准备蓝桥杯还是算法工程师面试这些名词迟早会撞见。A*算法适合做路径规划它在普通广度优先搜索之外加了一个启发式函数优先探索“看起来离终点更近”的方向所以效率和效果都比盲目搜索好。匈牙利算法解决的是匹配问题比如有n个任务和n个人每个人擅长其中一些任务如何安排才能使总完成对数最多。这类思路在调度系统、推荐系统里都很常见。再往学术和前沿看还有粒子群算法、模拟退火这类启发式优化算法以及Transformer、深度强化学习算法如MADDPG等基于神经网络的复杂系统。它们的本质也都是设定目标遵守约束不断逼近更优解。名字确实吓人但底层思维是一脉相承的。入门阶段不需要被这些名词劝退基础数据结构、搜索、分治这些地基打牢之后再往前看会轻松很多。4. 算法和数据结构为什么总是成对出现4.1 数据结构是骨架算法是灵魂《数据结构与算法》这门课之所以总把两个词绑在一起是因为它们谁也离不开谁。一个更直白的理解是数据结构管“数据怎么存放”算法管“数据怎么使用”。存的方式直接决定了用的效率。同样是查一个学生信息如果所有学生都堆在一个无序数组里你只能线性遍历O(n)级别如果维护一个哈希表就可以接近O(1)定位如果存成一棵二叉搜索树还能支持有序区间的范围查询。数据库系统、缓存系统、搜索引擎的每一个设计背后都是这种“存”与“用”的取舍。常见的数据结构有数组、链表、栈、队列、哈希表、树、堆、图。数组和链表是基础容器栈适合“后进先出”的场景例如函数调用栈队列适合“先进先出”例如广度优先搜索的节点待处理列表图结构几乎能建模所有带关系的问题比如社交网络、交通网络。4.2 举一个具体例子BFS为什么必须配队列很多初学者在写广度优先搜索时总纠结“为什么要用队列”。因为BFS的语义就是一层一层往外扩先遇到什么节点就先处理什么节点这正好对应队列的“先进先出”。如果你手滑用了栈就变成了深度优先搜索先一头扎到底再回头整个搜索路径全乱套。递归也很有趣它本质上是在隐式使用系统栈。函数一层层调用下去再一层层返回这和手动维护一个栈来写深度优先搜索是同一件事。学树和图的遍历时我会建议同时写递归版本和显式栈版本这样你对“栈”“队列”和“遍历顺序”这三者的关系会有非常直观的感受。4.3 哈希表为什么能变快空间换时间哈希表看起来像魔法往里面存一个键值对再取某个键时仿佛瞬间就能拿到。本质上是拿空间换时间通过一个哈希函数把键映射到数组下标把“查找某个键”变成“访问某个下标”所以才能那么快。但哈希函数不可能保证每个键都映射到完全不同的位置一旦多个键落进同一个槽位就产生哈希冲突。常见解决办法有链地址法把所有冲突的元素串成链表也有开放地址法继续探测下一个空位。理解了这个设计你会发现每个优秀方案背后都藏着取舍内存用多了冲突处理复杂了但在大多数场景下这仍然是一笔划算的买卖。5. 光知道基础还不够不同领域的算法长什么样5.1 传统领域的算法图像、滤波、控制与硬件仿真算法不止存在于竞赛和面试题里工业界和学术界的算法形态更加多样。图像处理里灰度图像二值化是一个特别经典的课题其中又以大津法Otsu最具代表性。它的核心思想是自动寻找一个阈值让分割出来的前景和背景类间方差最大用户不用手工调阈值机器自己算出一个相对合理的分界。在Halcon、OpenCV里做检测项目的人可能还经常用到均值滤波、中值滤波、高斯滤波来去噪。选哪种滤波要看图像里是椒盐噪声还是高斯噪声选错了等于白做。控制领域也有自己的算法。比如太阳能电池板最大功率点跟踪的MPPT算法常见的有扰动观察法、电导增量法本质是不断调整工作电压让输出功率沿着上升方向逼近最大值也可以看作一个连续优化问题。姿态解算领域有Mahony算法能把陀螺仪、加速度计、磁力计的数据融合成稳定的姿态做四轴飞行器的人应该都打过交道。这类算法不依赖厚重的深度学习框架但在工程落地时非常吃采样率和参数调优的经验。电路仿真里还有SPICE算法它本质上是一套基于电路方程组求解的方法在芯片验证领域几乎是标配。如果你去嵌入式或硬件公司面试对方很可能不考LeetCode而是问你信号干扰怎么滤除、采样数据怎么融合这时候你能否把算法的底层思想迁移过来就是拉开差距的关键。5.2 现代AI领域的算法从Transformer到深度强化学习深度学习的流行让“算法”这个词的外延扩大了不少。图像分类、目标检测、语义分割这些任务背后都是复杂网络结构和损失函数的设计。Transformer已经成为自然语言处理和视觉任务里最常见的网络架构它的注意力机制本质上也是在“全局范围内筛选重要信息”。语义分割算法里的DBNet则通过可微分二值化把文本检测变成像素级分割问题比传统回归框方法更高效。深度强化学习算法在近几年也很热比如MADDPG适合多智能体协作环境像多无人机编队、机器人协作。这些算法构建在大量线性代数、概率论和梯度计算之上但底层仍然遵循“状态、动作、奖励、策略”这个框架。如果连最基础的搜索和动态规划都还没站稳直接冲进深度强化学习大概率会看不懂代码里每一步参数更新到底在做什么。数据挖掘领域的HDBSCAN聚类算法也很值得了解它能在有噪声的样本空间里自动识别不同密度的簇。它有一个关键参数叫“最小簇大小”怎么选需要结合数据分布反复试。算法工程师面试中聚类、降维、特征选择这类问题经常被拿来考察你对数据形态的敏感度。隐私保护方面还有差分隐私算法通过在查询输出上加入经过校准的噪声让外界无法根据结果反推出某个人的具体数据。这在大数据统计和推荐系统中越来越重要也说明算法不只是“算得快”还可以“算得安全”。5.3 从热搜词看大家都在学什么我顺手整理了一些关于“算法”的搜索热词能大致看出学习群体的需求分布。一类是基础面试向比如数据结构与算法、算法工程师面试、蓝桥杯算法题、LeetCode必刷题说明很多人还在为笔试和面试做准备。另一类是具体算法向比如KMP、堆排序、归并排序、A*、匈牙利算法、Tarjan说明大家学完基础后会开始深挖经典算法。还有一类是领域应用向比如图像二值化、MPPT、Mahony、深度强化学习说明算法学习正从纯刷题走向业务落地。这张图景对新手很有参考价值。只想应付面试那就重点攻排序、查找、二叉树、动态规划和图论做图像或嵌入式就要掌握相应领域里流传多年仍然有效的经典算法做AI应用那么深度学习、Transformer、强化学习才是真正的主战场。把需求定位清楚学习路线就不会漫无目的。6. 新手最常踩的坑以及我的实操建议6.1 别背代码学着“模拟执行”我见过太多人把算法学习变成背题背快排写法、背KMP模板、背动态规划的状态转移方程。结果换个变体就卡住面试一深问就露馅。我自己学算法时最受益的方法是拿纸笔把每一步循环画出来。以二分查找为例拿一个长度为10的数组手动写下来left、right、mid每一步的变化比较中间值和目标值的大小关系直到两个指针交错。这个过程看起来慢但一次亲手模拟胜过背十遍代码。一旦你把人脑手算的过程想明白了代码就只是这个手算过程的形式化翻译而已。6.2 从两本书、一条主线同时推进如果目标就是蓝桥杯或者LeetCode我的建议是先花一到两周过完基础数据结构再按专题刷题。排序、二分、双指针、栈与队列、树、回溯、贪心、动态规划、图论一个专题一个专题来。初期不要追求题量每天认真吃透两三道题远比刷十道然后忘光要有意义。如果平时做的是纯业务开发也别觉得算法无用。缓存失效、布隆过滤器、倒排索引、一致性哈希这些听起来花哨的工程方案背后全是算法和数据结构的实际应用。多了解一点系统设计时手里就多几个选项也不容易被各种概念牵着鼻子走。6.3 常见问题速查表问题可能的坑建议看不懂网上题解跳过了推导过程先弄清思路再独立实现一遍写出的代码超时一直用暴力枚举复杂度太高分析数据规模换成二分、哈希或剪枝思路排序代码背不下来死背某个语言实现先理解“比较与交换”再手写通用逻辑学习动力不足没有明确目标定一个具体目标比如蓝桥杯省赛拿奖LeetCode每日两题面试讲不清复杂度只会写不会说每道题写完口头复述一遍时间和空间复杂度理论和业务脱节只刷题不看应用试着把学到的算法用到真实业务代码里做优化我在带新人时经常说一句话算法学习像练肌肉不是看几集视频就能长出来的必须自己反复做动作感受每一块肌肉的发力。一道题写三遍每一遍换个理解角度效果比抄十遍别人的答案都好。那些现在看着吓人的算法名词过几个月回头看大概率会变成一句“哦原来就是那么回事”。我个人还有一个私藏的小习惯每学完一类算法我会用大白话写一段几十字的笔记把自己当成本类算法完全没学过的路人试着解释这个问题到底在解决什么、步骤是什么、为什么这么解。写完再回去读思路会清楚一大截。这个习惯陪我从入门走到面试也推荐你试试。下一期我会接着写递归与分治题目不难但坑不少到时候再分享几个我亲身踩过的调试疑难。学算法这件事入门最怕的从来不是难度而是被名词吓住只要你亲手写出来第一个像样的搜索树心里那点畏难情绪就全散了。