☰
算法入门到实战:从排序、动态规划到机器学习与工业应用
2026/9/27 1:27:59 网站建设 项目流程

1. 算法究竟是什么:先撕掉那层神秘滤镜

我做了十来年技术,被问得最多的一个问题就是:“算法到底学来干嘛?我又不去搞人工智能。”每次听到这句话,我都想反问一句:你手机里的地图导航是怎么给你算出最短路的?你刷短视频时为什么越刷越懂你?你去银行办业务,那个排队系统为什么能保证你前面的人不会比你多等太久?

这些全是算法。

说穿了,算法就是“解决某个问题的具体步骤”。你今天中午纠结吃什么,其实也在跑一个“选择算法”——先定预算,再排除忌口,最后挑评分高的,这就是一套完整的决策流程。只不过计算机里的算法比这严谨得多:输入什么、每一步做什么、什么时候结束、输出什么,全都定义得清清楚楚。

那为什么“算法”这个词听起来这么吓人?因为市面上讲算法的资料,要么一上来就甩一堆数学符号,要么直接贴代码,搞得像天书。但我要说句实在话:算法跟数学有关系,但跟“会做饭”的关系更大。你会不会照着菜谱做菜?知道什么时候该大火什么时候该小火?知道调料放多了怎么补救?这些就是对问题的理解能力和处理经验。算法不是背出来的,是用出来的。

我见过太多初学者,买一本《算法导论》翻了两页就放弃,因为里面全是渐进复杂度证明。我也见过不少半路出家的工程师,工作上把排序、查找用得很溜,但你要问他为什么这个场景用哈希表而不用数组,他能给你讲得头头是道。区别在哪?就在“理解”两个字——理解了算法的适用场景、边界条件和复杂度代价,才能在实际问题里做对选择。

所以这篇文章我不打算罗列什么十大必学算法,而是想带着你,通过一批真实的、覆盖各领域的算法实例,抽丝剥茧地看清楚:算法是怎么被设计出来的,它解决了什么问题,为什么这样设计,以及换成你会怎么做。目的就一个:让算法这层窗户纸被捅破,你以后再看任何算法资料,都不会再觉得那是另一个世界的东西。

2. 先把地基打牢:经典算法背后的核心思想

2.1 排序算法:不只是“把数组排好”那么简单

排序算法是很多人的算法启蒙课。但我想先纠正一个误区:你不需要把十种排序的代码全都背下来,你需要搞清楚的是它们各自的性格。

拿最常被拿来对比的三个来说:冒泡排序、快速排序、归并排序。

冒泡排序的思路最直观:从左往右,两两比较,大的往后沉,小的往前浮。每轮下来,最大的数必然被推到末尾。它的问题是太“老实”,哪怕数组已经排好了,它仍然要老老实实跑完两层循环,时间复杂度稳定在 O(n²)。所以它适合数据量极小、代码可读性优先的场景,比如教学演示,或者你手头就几十个元素,排序性能无关紧要。

快速排序是实战里的宠儿。它的核心是“分治”:随机选一个基准值,把数组拆成小于基准和大于基准两部分,再递归处理。平均时间复杂度 O(n log n),原地排序,空间占用极小。但注意“平均”两个字——如果你每次选基准都选到最大或最小值,快排会退化到 O(n²),所以很多工程实现里会做“三数取中”之类的优化。这也是我常说的:算法不是死的,工程上全是微调。

归并排序则是另一种性格:稳定、可控、最坏情况也是 O(n log n)。代价是需要额外的 O(n) 空间来合并临时数组。它的经典应用场景是外部排序——比如你要给一个几十GB的文件排个序,内存装不下,归并排序的分治思路就能配合磁盘分批读写,把排序分成多个小文件再逐层合并。这就是算法从“课本”变成“生产力”的最典型例子。

我个人的建议是:冒泡要会写,因为它帮你理解“交换”的本质;快排要会调,因为它是无数框架源码里的标配;归并要吃透,因为它背后的“分治+合并”思路,能迁移到很多看似跟排序无关的问题上。

2.2 查找算法:二分查找为什么这么“神”

如果说排序是给数据安排座位,查找就是告诉你“你要找的人坐在哪”。最值得深挖的查找算法,一定是二分查找。

二分查找的要求很简单:数据必须是有序的。然后每次取中间值和目标比,大于目标就搜左半边,小于目标就搜右半边,每次把搜索范围砍掉一半。你想想,如果有一百万个有序元素,用线性查找最坏要查一百万次,二分查找最多连二十次都不用——这个差距就是“暴力解题”和“动脑子解题”的差距。

但二分查找最坑的地方是边界条件。我面试过不少人,问二分查找,能写出个大概,可一追问“你这个 while 是 left < right 还是 left <= right?”“mid 算出来之后到底要不要加一减一?”就露馅了。这些细节不是玄学,而是有没有真正理解“循环不变量”这个概念。简单说,你要自己先想清楚:我定义的搜索区间是闭区间 [left, right] 还是开区间 [left, right)?定义清楚了,边界处理自然是对的。这个思维习惯,比背模板重要一百倍。

二分查找的厉害之处还在于,它不只能查数组,还能解决很多“看起来跟查找无关”的问题。比如你想找“某个值是否存在于某串数据里”,可以把“判断结果”当成一个单调函数,然后二分这个输入范围。最经典的例子是:求一个数的平方根,或者在一个有序数组里找第一个大于等于目标值的位置。这就是我说的,算法的价值不在于它的名字,而在于它背后的“减而治之”思想,能不能被你迁移到新问题上。

2.3 贪心算法:局部最优到底能不能带来全局最优

贪心算法的思路简单得让人怀疑:每一步都做当前看起来最好的选择,指望最终结果也是最好的。它之所以能成为一个正儿八经的算法设计策略,是因为确实有一类问题,局部最优的选择累加起来,恰好就是全局最优解。

最典型的例子是活动选择问题:你有一堆活动,每个活动有开始时间和结束时间,问你最多能参加多少个不冲突的活动。正确解法是:按结束时间从小到大排序,每次选择结束最早且与已选活动不冲突的那个。为什么这个贪心策略是对的?因为结束早的活动给后面留了更多时间空间,这是可以严格证明的。

但贪心算法的另一面是,它很容易被滥用。你看起来每一步都顺手,结果一到最后发现全局解压根不是最优的。经典的坑是“找零钱问题”:假设有 1 元、5 元、11 元三种面额,要凑出 15 元,贪心会先拿 11 元,再拿 4 个 1 元,总共 5 枚;可最优解明明是 3 枚 5 元。这就是贪心失效的场景。所以我说,贪心策略的第一步不是写代码,而是证明或至少说服自己:这个问题的局部最优确实能推出全局最优。没想清楚这一点,代码写完也是错的。

在实际工程里,贪心算法更多是作为一种“近似策略”在用——比如路由算法里的最短路径选择、任务调度的优先级排序。很多问题的最优解是 NP 难问题,求不出来,那就用贪心先求一个“还不错的解”兜底。这就是理论和工程之间的真实差距:教科书教你找最优,工程教你在可接受的时间内找“足够好”。

2.4 动态规划:把大事拆成小事,再记住小事的答案

动态规划可能是所有算法思想里,最值得花时间钻研的一个,因为它太实用了。字符串编辑距离、背包问题、最短路径、序列比对,几乎到处都是它的影子。

动态规划的核心逻辑可以用一句话概括:大事化小,小事化了,了了的事记住,不再重复算。你用递归去解斐波那契数列,算 f(n) 时要反复算 f(n-1)、f(n-2),指数级爆炸;可如果你用一个数组,按顺序从 f(0) 一路算到 f(n),每个值只算一次,效率直接提到线性。这就是“记忆化”的威力。

但真正让初学者头疼的,不是理解这个思路,而是怎么把一个问题抽象成状态和转移方程。我的经验是,做动态规划题目,先别急着写代码,先问自己三个问题:

  • 这个问题的“状态”是什么?比如背包问题里,状态就是“当前考虑前 i 件物品、背包容量为 j 时,能获得的最大价值”。
  • 状态之间怎么转移?换句话说,我从上一个状态怎么推导到下一个状态?
  • 初始状态和边界条件是什么?

这三个问题想清楚,动态规划题就解掉一大半了。很多教材喜欢直接给状态转移方程,看起来像从天而降。实际工作里,状态转移方程恰恰是最需要反复推敲、甚至画表验证的部分。我自己做动态规划,从来都是先在纸上画一个二维表格,一格一格填数,填到一半自然就理解规律了。这个方法我强烈推荐给所有初学者,比盯着代码看效率高得多。

还有一个常见误区是:所有“递归 + 记忆化”都能改写成“迭代 + 表格”。理论上确实可以,但工程上不一定值得。记忆化递归写起来直观、不容易出错,迭代方法省了递归栈的开销,但代码可读性通常差一些。我的建议是:先求正确,再谈优化。等你对问题本身有把握了,再考虑用迭代改写来压性能。

3. 从“课本里的算法”到“行业里的算法”:八九十种实例一次讲透

接下来这部分是整篇文章的重头戏。我按行业领域把算法实例做了归类。不是为了凑“100 个”这个数字,而是想告诉你:同一个算法思想,在搜索引擎、在自动驾驶、在金融风控、在生物信息里,可以换一副完全不同的面孔。

3.1 排序与搜索算法的工业级应用

  • 数据库索引的 B+ 树:你发的每一条 SQL 查询,背后都有一棵 B+ 树在帮你快速定位数据块。它不是二分查找的数组版,而是为了配合磁盘页大小设计的“多路搜索树”,一次磁盘 IO 能读更多节点,查找次数更少。
  • 搜索引擎的倒排索引:你输入“算法 实例”,搜索引擎不是满世界翻网页,而是先在倒排索引里查“算法”这个词出现在哪些文档里,再查“实例”出现在哪些文档里,然后做集合交并运算。这个过程里的核心就是快速查找和归并排序。
  • Top-K 问题的堆排序:让你从一万个数字里找最大的十个,你不会把整个数组全排序,而是维护一个大小为 K 的小顶堆,比堆顶大就入堆。堆排序思想在这里的应用,能把时间复杂度从 O(n log n) 降到 O(n log K),数据量一大,差别非常明显。

3.2 图论算法:路网、社交网络与依赖分析

图论算法是实践性极强的一类,因为现实世界的“实体 + 关系”天然就是一张图。

  • Dijkstra 算法:地图导航最短路问题的经典解法。它有个前提:边权不能为负。为什么?因为 Dijkstra 每次从“已确定最短距离”的集合里挑一个最小节点往外“松弛”,如果存在负权边,后面可能发现一条更短的路径,前面已经确定的节点就失效了。
  • 贝尔曼-福特算法:解决 Dijkstra 不能处理负权边的问题。它做 V-1 轮全量松弛,第 V 轮如果还能松弛,说明存在负权环。这个“负权环检测”能力在金融套利检测里非常有用。
  • Floyd 算法:一次性算出所有点对之间的最短路径,用三重循环做动态规划。数据量小的时候(比如几百个点)直接无脑上,写起来极其简单,比跑 V 次 Dijkstra 省心。
  • 拓扑排序:你在编译代码时,编译器必须知道每个头文件的依赖关系,先编译依赖方,再编译被依赖方。这就是一个有向无环图的拓扑排序。课堂上它看起来像“按入度删节点”,工程里它就是“构建系统到底先执行哪一步”的依据。
  • Tarjan 算法:求强连通分量,用于分析网络中的紧密社区、代码里的循环依赖。它只用一个栈和几个数组就能在线性时间内找出所有的强连通分量,是我见过最精巧的图算法之一。

3.3 字符串算法:从文本匹配到生物信息

  • KMP 算法:字符串匹配的经典算法,核心是 next 数组。它解决的问题是:当一次匹配失败时,模式串不用回到开头重新匹配,而是利用已匹配前缀的信息跳到一个安全位置。写的时候很多人被 next 数组的下标搞晕,我的建议是先理解“最长相等前后缀”这个定义,再写代码就不容易错。
  • BM 算法:比 KMP 更快的字符串匹配算法,核心是“坏字符规则”和“好后缀规则”,从右往左匹配。很多文本编辑器的查找功能底层用的就是 BM 或它的变种,因为实践中的平均性能非常好。
  • AC 自动机:KMP 只解决“一个模式串在一个文本串里匹配”的问题;AC 自动机解决的是“一堆模式串同时在一个文本串里匹配”的问题。敏感词过滤系统、多关键字搜索引擎都会用到它。它本质上是在 Trie 树上做 KMP 的 fail 指针跳转。
  • 编辑距离(Levenshtein 距离):两个字符串之间至少需要多少次增、删、改操作才能互相转换。拼写纠错、DNA 序列比对、代码 diff 检测里全都在用,是个典型的动态规划应用。

3.4 机器学习与数据挖掘算法:算法在各行各业落地的主力

这部分可能是和“100 个实例”标题里那批最新热词重合度最高的。如果你听过“机器学习算法”“深度学习算法”“聚类算法”“粒子群算法原理”这些热搜词,那么下面这十个实例正好给你串起来。

  • 线性回归:最朴素的预测算法,找到一条线让所有样本点到这条线的垂直误差平方和最小。它可以用最小二乘法直接求出解析解,也可以通过梯度下降迭代求解。数据量小、特征少的时候,线性回归仍然是非常高效的基线方案。
  • 逻辑回归:名字里有“回归”,实际做的是分类。它给线性模型的输出套了一个 Sigmoid 函数,把结果映射到 0 到 1 之间,当作概率使用。风控行业的评分卡模型,很多就是从逻辑回归演化来的,因为它不但能预测违约概率,还能解释每个特征怎么影响结果——这是深度学习很难做到的。
  • 决策树与随机森林:决策树通过一系列“如果…那么…”的规则把数据划分开。随机森林则是训练很多棵在随机特征子集上分裂的决策树,最后投票决定结果。它的优点是不需要做太多特征归一化,对缺失值也有一定耐受度,工程上经常被当作“开箱即用”的基线模型。
  • K-Means 聚类:把一堆点自动分成 K 组,每组内部尽量紧凑。它的迭代过程特别直观:随机初始化中心、分配样本、重新计算中心、重复直到收敛。冷启动时不知道 K 取多少,可以用“肘部法则”看误差曲线找拐点。这个算法在用户分群、图像压缩、异常检测里都有应用。
  • 主成分分析(PCA):降维算法。把高维数据投影到方差最大的几个方向上,保留主要信息的同时压缩维度。人脸识别里经常先用 PCA 把像素向量降维,再做分类,这样既能降低计算量,也能有效防止过拟合。
  • 支持向量机:在两组样本之间找最大间隔的超平面。它有一个神奇的操作叫“核函数”,能把低维空间里线性不可分的数据映射到高维空间使其线性可分。文本分类任务里 SVM 曾经统治了很多年,直到深度学习方法崛起。
  • 朴素贝叶斯:基于贝叶斯定理的分类算法,核心假设是特征之间相互独立。虽然这个假设在现实中基本不成立,但在垃圾邮件过滤、情感分析这类任务里,它依然又快又好。原因很简单:哪怕假设有瑕疵,只要分类结果大体正确,性价比就很高。
  • Apriori 与 FP-Growth:关联规则挖掘算法,最经典的场景就是“啤酒和尿布”。Apriori 通过逐层搜索频繁项集,FP-Growth 则用一棵频繁模式树,避免了反复扫描数据库,性能提升非常明显。
  • 百度 ERNIE、华为盘古等大规模预训练模型训练思想:这类算法的核心是“大规模 + 自监督 + 微调”。先用海量无标注文本做预训练,让模型学到语言知识,再在具体任务上做微调。虽然这些模型的结构很复杂,但其训练链路——数据清洗、分词、预训练、微调、蒸馏压缩——本身就是一套完整的算法工程体系。
  • 强化学习:让智能体在环境中通过试错积累奖励,最终学到一套策略。AlphaGo 下围棋、机器人学会走路、推荐系统里的在线策略优化,底层全是强化学习。它的核心公式是贝尔曼方程,描述的是“当前状态的价值 = 当前奖励 + 未来状态的价值期望”。

3.5 智能优化算法:向自然界借来的“黑魔法”

  • 遗传算法:模拟生物进化过程。一个候选解就是一个“个体”,一组候选解组成“种群”,通过选择、交叉、变异不断迭代,最终逼近最优解。这种算法不要求问题有导数、凸性等严格性质,适用于那些传统数学优化方法很难啃的“黑箱优化”场景。
  • 粒子群算法:模拟鸟群觅食。每个粒子都有一个位置和速度,每次迭代时,粒子朝两个方向飞去:一个是个体历史最佳位置,一个是全局最佳位置。它代码量极少、收敛速度快,在神经网络权重初始化、控制器参数调优里经常见到。
  • 模拟退火:模拟金属退火过程。它接受新解的规则有点“反直觉”:如果新解更差,有一定概率仍然接受它,而且这个概率随着温度降低而减小。这恰恰是它能跳出局部最优的关键。旅行商问题这种 NP 难问题,在找不到精确解时用模拟退火求近似解非常合适。
  • 海星优化算法:海洋生物启发式算法的一种,模拟海星的再生和觅食行为。这类“新式元启发式算法”每年都有大量论文出现,性能各有胜负。我在工程里使用它们时会非常谨慎,因为这些算法的“全局搜索能力”和“收敛速度”之间的平衡很难保证,实际效果经常不如简单调好超参数的经典算法。
  • MPPT 算法:光伏发电里的最大功率点追踪,常用扰动观察法和电导增量法。它的任务不是求数学最优解,而是让光伏系统实时输出最大功率。这类算法嵌在嵌入式设备里,对实时性和鲁棒性的要求很高,和学术界的元启发式优化几乎不在一个跑道上。

3.6 基础算法:你可能天天用却从没当它是算法的东西

  • 进制的世界:MD5、SHA 这些哈希算法,把任意长度的输入映射成固定长度的输出。它们不是加密算法,而是摘要算法,理论上是不可逆的。你下载文件时看到的校验值、登录系统时存储的密码哈希,都是这类算法的实际应用。
  • CRC 算法:循环冗余校验,用来检测数据传输过程中的错误。Modbus 通信协议里就有一个 CRC-16 算法,别小看那一串位移和异或操作,它在工业现场守护着每一帧命令的完整性。
  • PID 算法:工业控制领域最伟大的算法之一,没有“之一”也可以。温控器保持恒温、无人机悬停、电机转速稳定,全都在用 PID。P 是对当前误差的反应,I 是对历史误差的累积,D 是对误差变化趋势的预测。三个参数怎么调?我的经验是先调 P 让系统不振荡,再加 D 抑制超调,最后用 I 消除稳态误差。
  • 滑动平均滤波:传感器读数据会有抖动,最简单的处理就是取最近 N 次读数的平均值。烟雾传感器、温湿度计、IMU 姿态数据,几乎都要过这一层滤波。别看它简单,处理实时性要求高的高频噪声,它比复杂滤波器更稳。

3.7 一些你可能没听说过的算法角落

  • 匈牙利算法:解决“任务指派问题”——有 N 个工人、N 个任务,每个工人做每个任务的成本不同,怎么分配让总成本最低。这个算法看起来小众,但我在地图匹配、车辆调度、甚至图片特征点匹配里都用过它。
  • 混合整数线性规划(MILP):线性规划的加强版,部分变量必须取整数。生产排产、路径规划、资源调度里的很多问题最终都要建模成 MILP,再用求解器去跑。这个“算法”单看名字很学术,但它的建模思想对实战非常重要:先搭约束、再设目标函数、最后交给求解器,你能控制的其实是前三步。
  • BPTT 算法:循环神经网络的反向传播方式。因为网络是沿时间展开的,误差也要沿时间反向传播,所以叫 Backpropagation Through Time。理解 BPTT 的关键是“展开”:把循环网络按时间步展开成一个很深的神经网络,然后按普通反向传播处理。
  • YOLO 算法:目标检测里大名鼎鼎的“You Only Look Once”,核心思想是把检测任务当成回归问题,一次性在整张图上预测所有目标的边界框和类别。速度快到能实时跑视频流,是安防、自动驾驶、工业质检里的常客。
  • 指纹识别算法:从指纹图像里提取细节点(脊线端点、分叉点),再与库里模板做特征匹配。核心技术包括图像增强、方向场估计、二值化和细节点提取,每一步都依赖大量图像处理算法。看着古老,但在门禁、手机解锁、司法鉴定领域依然稳如磐石。

写到这,你应该已经看到了:所谓“100 个实例”,并不是要你记住一百个新名词,而是让你看见同一批底层思想的“换皮演出”。排序、搜索、图论、动态规划、哈希、概率模型、启发式优化,这些加起来不超过二十个的核心方法论,纵横排列组合,就衍生出了几十上百个具体的算法名称。

4. 面对这么多算法,你该怎么选、怎么学、怎么用

4.1 算法选型:先看约束,再看目标,最后才看算法

我把选型原则总结成一句话:数据有多大,算力有多强,精度要多高,时限有多紧。

这四件事决定了你该用简单算法还是复杂算法。我见过有人用深度学习跑一个普通的线性回归问题,也见过有人在一万个数据点上非要用分布式框架,都是典型的“杀鸡用牛刀”。工程里最重要的能力不是会多少高级算法,而是能用最小代价把问题解决。

具体来说,你可以按照这样的决策流程走:

  1. 先评估数据规模。数据少到能装进内存且不超过几十万条,很多 O(n²) 的算法其实完全可用,别急着上复杂优化。
  2. 再确认实时性要求。搜索引擎、广告推荐这类在线场景,必须控制响应时间;离线数据分析则可以把计算时间从秒级放宽到分钟级甚至小时级。
  3. 然后明确精度需求。医疗影像诊断、自动驾驶感知,精度不足会出事故,必须上最好的模型;但如果你只是做趋势分析,粗粒度预测就够用了。
  4. 最后算算手里有什么资源。GPU 堆得起吗?内存够不够?很多时候“最优算法”不如“可用资源下最合适的算法”。

4.2 学习路线:先搞懂原理,再抄代码,最后丢掉代码

我给所有想认真学算法的人一条实打实的路径:第一遍只理解不写码。把排序、二分、动态规划、BFS/DFS 这些核心算法的思路弄明白,如果你愿意,可以画图,可以举生活中的例子,但别急着看代码。

第二遍照着代码“抄”。抄不是目的,是让你感受“思路落地成代码”时的每一步细节。你会发现,动态规划的状态转移方程写出来不难,难的是数组下标到底怎么对齐。这个过程能逼着你把抽象的思维和具体的实现对接起来。

第三遍脱离代码自己写。把书合上,给一个完全一样的问题,自己从零开始推导、实现、测试。写不出来的地方、卡住的地方,就是你理解还不到位的地方。这不是智商问题,是熟练度问题。

第四遍是变形。把原题的条件改一改,问你“如果元素有重复怎么办?”“如果数据流是动态增删的呢?”“如果是二维数组呢?”,看你能不能把思路迁移过去。迁移能力,才是面试和实战真正的分水岭。

4.3 工具与环境:不迷信语言,不迷信框架

总有人问:学算法用 Python 还是 C++?我的回答是:都行,但不同阶段各有优势。Python 写起来快,适合验证思路;C++ 让你更接近内存和指针,适合理解底层。我建议你至少精通一种“方便写原型”的语言(Python、JavaScript 都行),再熟悉一种“性能优先”的语言(C++、Rust、Java),这样进可攻退可守。

对于 AI 方向的算法,机器学习框架(TensorFlow、PyTorch)肯定是绕不开的,但你要明确:框架只是工具,算法思想才是灵魂。很多人会用 PyTorch 调一个 ResNet,但你问他残差连接为什么要存在,他答不出来。这不叫懂算法,这叫会调库。真正的理解是:当框架提供的模型不满足需求时,你能自己设计新的结构、新的损失函数、新的训练策略。

5. 实战实录:把一道“找出最大数”的题目做成一个完整项目

聊了这么多抽象的东西,是时候来一个完整的实战环节了。我从热搜词列表里挑了一个最“朴素”的题目:依次输入 10 个数,输出其中最大的数。别笑,这道题看起来简单到不像话,但它能把“算法思维”的完整流程——分析问题、设计步骤、绘制流程图、写代码、做测试、做优化——从头到尾串起来。我带你不止走一遍,我陪你走三遍。

5.1 第一步:需求分析到底在分析什么

绝大多数人拿到这道题,第一反应是“这有什么好分析的,直接开写循环不久完事了”。但这恰恰是新手和工程师的区别。工程师拿到需求,先要做的是把模糊的描述变成精准的规格。

原题是“依次输入 10 个数”。这里的“数”是整数还是浮点数?负数算不算?“依次输入”一次输入一个、还是一行输入十个?“输出最大的数”是只输出数值,还是连位置一起输出?如果输入的数全部相同,输出什么?

我这样追问,不是抬杠,而是算法工程里最常见的现实:需求永远有模糊地带,写代码前不澄清,写完之后就是返工。在这个例子里,我做一个合理的假设:输入的是任意实数,数量恰好是 10 个,一次一个,最后输出这 10 个数中的最大值。

这个“需求确认”的过程,在真实项目里就是评审会议、原型设计、接口定义的前身。你面对的问题越小,越要养成先问清楚再动手的习惯。

5.2 第二步:设计算法的步骤和原理

算法设计的输入:一个长度为 10 的数列。

算法设计的输出:这个数列里的最大值。

怎么求最大值?你脑子里其实早就有一个最朴素的方法:把第一个数默认当成当前最大值,然后从第二个数开始,一个一个跟当前最大值比,遇到更大的就替换。这个过程用一个成语形容就是“打擂台”——擂台上先站一个人,后面上来的人逐个挑战,赢了就留下,输了就下台,最后站在台上的就是冠军。

这个“打擂台”算法,就是所谓“顺序扫描求极值”,时间复杂度 O(n)。注意,在这里 n=10,这个复杂度看起来“没有技术含量”,但它是所有求极值方案的基线。没有任何已知算法能做得比 O(n) 更快,因为每个数你都至少得看一次,才能判断它是不是最大。

那这里有没有坑?有。第一个坑是:如果输入里面全是负数,你的 max 初始值如果设成 0,输出就会一直是 0,而不是输入里的最大值。所以正确的初始化方式不是“设成某个想象中的最大值”,而是“设成第一个输入值”。第二个坑是:如果用户输入的个数不足 10 个,你运行到一半再让等输入,程序就卡住了。所以实际工程里,你还要考虑异常输入。

5.3 第三步:画传统流程图和图解代码逻辑

这里先解释一下“流程图”。它本质上就是一种画图语言,用不同的图形代表不同的动作:圆角矩形代表开始或结束,矩形代表处理(比如赋值、比较),菱形代表判断(比如“当前数大于 max 吗”),箭头代表流程走向。

这个求最大数的流程图,按以下步骤画:

  1. 开始。
  2. 输入第一个数,记为 x。
  3. 令 max = x。
  4. 令计数器 i = 2。
  5. 判断 i 是否小于等于 10?如果是,继续第 6 步;如果否,跳转第 10 步。
  6. 输入下一个数,记为 x。
  7. 判断 x 是否大于 max?如果是,跳转第 8 步;如果否,跳转第 9 步。
  8. 令 max = x。
  9. 令 i = i + 1,返回第 5 步。
  10. 输出 max。
  11. 结束。

流程图最大的价值,在于它把代码执行的“逻辑走向”可视化。很多人写程序容易卡在“我到底该用 if 还是 while”这种问题上,如果你能先画一张流程草图,判断逻辑直接用菱形标出来,循环走向用箭头画出来,代码自然就顺理成章了。这也是我强烈建议初学者“先画图,再写码”的原因。

5.4 第四步:用代码实现它

用 Python 写一个最简单版本:

nums = [] for i in range(10): num = float(input("请输入第 {} 个数: ".format(i + 1))) nums.append(num) max_val = nums[0] for num in nums[1:]: if num > max_val: max_val = num print("最大数是:", max_val)

如果你不想存列表,可以一边输入一边比较:

max_val = float(input("请输入第 1 个数: ")) for i in range(2, 11): num = float(input("请输入第 {} 个数: ".format(i))) if num > max_val: max_val = num print("最大数是:", max_val)

第二种写法更节省内存,因为不需要把 10 个数全部存下来。也许你会觉得“10 个数存不存有什么区别”,但你把这个思路放大到 100 万个数、甚至流式数据场景里,就会明白“边算边丢”的内存效率有多重要。这就是算法优化的真实起点。

C++ 版本同样是这个思路:

#include <iostream> using namespace std; int main() { double x, max; cout << "请输入第 1 个数: "; cin >> max; for (int i = 2; i <= 10; ++i) { cout << "请输入第 " << i << " 个数: "; cin >> x; if (x > max) { max = x; } } cout << "最大数是: " << max << endl; return 0; }

5.5 第五步:测试与边界验证

代码写出来不代表结束,测试才是真正的开始。我会至少测这五组数据:

  • 正常乱序数据,比如 3、-1、9、7、2、0、5、-8、4、6,预期输出 9。
  • 全部为负数,比如 -5、-2、-8、-1、-9、-4、-7、-3、-6、-10,预期输出 -1。这组数据能检验你的初始化是否踩了那个“初始化为 0”的坑。
  • 全部相同,比如 7、7、7……,预期输出 7。这一步是检验逻辑在“相等”分支是否出错。
  • 边界极值,比如非常大和非常小的数混在一起,检验浮点精度是否会带来问题。
  • 输入格式异常,比如输入了字符而非数字,程序应当给出提示或报错,而不是静默产生错误结果。

在真实项目中,边界测试是保证系统质量的核心手段之一。越是看起来简单的功能,越不能跳过测试。因为这个函数大概率会被别人当成“工具函数”调用,如果它本身不可靠,所有调用它的上层应用都会跟着遭殃。

5.6 第六步:从 10 个数扩展到 1 万个数

这道题的原版是 10 个数,但算法的价值在于“泛化”。你只要把循环次数从 10 改成 n,算法思路完全不变。用 Python 读文件的场景:

max_val = None with open("numbers.txt", "r") as f: for line in f: num = float(line.strip()) if max_val is None or num > max_val: max_val = num print(max_val)

这个版本就是流式处理的雏形:一次只读一行,不把整个文件加载到内存,却能在大文件里求出最大值。你仔细想想,这跟大规模数据处理系统里“MapReduce 求最大值”的核心逻辑,本质上是同构的。算法从小问题到大规模问题的跃迁,从来不是换一种神秘算法,而是把简单算法做对、做稳、做 scalable 而已。

6. 算法学习中最容易踩的坑和避坑经验

6.1 背代码不如画流程

我见过太多人刷题的方法是背模板:二分查找模板、动态规划模板一个接一个背。但一到面试现场,面试官把题目改一行字,比如“找出第一个大于等于 target 的位置”改成“找出最后一个小于 target 的位置”,立刻懵住。原因很简单:模板是背出来的,不是理解出来的。

我的训练方案是:每一道题,先画一张流程图或者状态转移图,画完之后,再对照图写代码。画不出图,说明你还没理解题目,那写出来的代码再像也是烂代码。

6.2 忽视数据规模就是最大错误

有些初学者觉得“快排一定比冒泡好”,然后不管什么数据量都上快排。我举一个实际例子:一个数组只有 20 个元素,你用冒泡排序和快速排序,性能差异完全感知不到;但快排代码复杂、容易出错,万一退化到最坏情况,反而不如老老实实冒泡。

所以正确做法是先估算数据规模。LeetCode 这个平台的数据规模,往往可以反过来提示你应该用什么算法——比如 O(n²) 的算法在 n 达到 10^5 时基本跑不动,那就必须考虑 O(n log n) 甚至 O(n)。这种“根据规模选算法”的能力,是做题和实战共同需要的。

6.3 复杂度分析为什么必须学

很多人觉得复杂度分析就是“背一下 O(n)、O(log n)”这些记号,考试用得上,工作用不上。大错特错。复杂度分析的核心价值是给你一个“计算尺”,让你在不真正运行代码的情况下,粗略估算出你这套方案能不能在限时内完成。

举个真实案例:我有一次优化一个接口,原始方案是双层循环 O(n²),n 大概是一万,本地测试就花了两秒多。后来我把内层循环换成哈希查找,复杂度降成 O(n),接口响应直接降到几十毫秒。这个优化过程中,我没有改任何业务逻辑,只是用复杂度分析判断出了问题瓶颈在哪里。

那“什么时候用 O,什么时候用 θ”——热搜词里恰好有一句这个问题。我的回答是:如果强调的是“最坏情况下不会超过某个量级”,用大 O 记号;如果强调的是“这个算法无论输入好坏,复杂度都固定在这个量级”,用 Θ 记号。比如冒泡排序是 Θ(n²),因为它的比较次数始终是那个量级;而快速排序是 O(n²) 也是 O(n log n),但你更常说它的平均复杂度是 Θ(n log n)。实际交流里,大家默认说 O,除非你要精确表达“上下界都被卡住了”,才用 Θ。

6.4 多刷代码不如多聊思路

最后这条经验可能反直觉,但特别有用:写代码练手很重要,但在学算法的中后期,把你的思路用大白话讲给别人听,比闷头写一百道题更有效。你讲一次,就要把为什么这么设计、边界在哪、复杂度为什么是这个,组织成连贯的语言。在这个过程中,你往往会发现自己理解里的漏洞。这就是费曼学习法在算法学习里的应用。

7. 那些“看起来很吓人”的进阶算法:其实没你想象中难

7.1 KMP 算法破局

KMP 之所以劝退很多人,是因为教科书直接甩出“next 数组”的定义。我换一个角度讲。字符串匹配失败时,你已经知道“当前已经匹配了哪些字符”。那下一次匹配,模式串的指针回退到哪里最合理?

答案是:利用“已匹配前缀”里,最长的那段“既是前缀又是后缀”的部分。打个比方,模式串是“ABABAC”,匹配到“ABABA”时失败了,这时“ABA”既是前缀又是后缀,那么下一个匹配位置就不用从 0 重新开始,直接跳到模式串下标 3 的位置继续试就行。

这个“既长又相等的前后缀”的长度,就是 next 数组。理解了这一点,KMP 就不再是一堆奇怪的数组下标,而是一个相当朴素的“失配跳转”思想。你写不出来,只是因为还没把这个思想翻译成代码。

7.2 强化学习的核心逻辑

强化学习最劝退的地方,是它同时牵扯到动态规划、概率论、函数逼近、最优化,概念一个叠一个。但你只要抓住一条主线:智能体每走一步,先观察当前状态,再按策略选动作,环境给奖励并转换到新状态。目标是让长期总奖励最大。

这里最重要的公式是贝尔曼方程。它描述的是:某个状态的价值,等于当前立刻能拿到的奖励,加上未来所有状态下价值的折现期望。你仔细看,这不就是“把远期收益折算到现在”的递归表达吗?

有了这个视角,DQN、PPO 这些算法,本质上都是在回答同一个问题:怎么用收集到的数据,去逼近这个价值函数或策略函数。方法不同,目标一致。你如果本来懂一点动态规划里的“值迭代”,再学强化学习,会发现骨子里是同一套东西。

7.3 MD5、哈希与真实世界的身份冲突

热搜词里有“MD5 算法详细完整过程并举例”。我简单讲一下它的套路:第一步,把原始消息填充到长度对 512 取模为 448,并在末尾附上原始长度的 64 位二进制表示。第二步,把消息按 512 位分块,每块再拆成 16 个 32 位子块。第三步,用四个链接变量(A、B、C、D)做 64 轮循环运算,每一轮都包括非线性函数、左移和模加。最后,把四个链接变量拼接输出的 128 位串,就是 MD5 值。

但有一点我必须提醒:MD5 已经不被推荐用于安全场景了,因为碰撞攻击成本已经很低。现在做文件完整性校验、去重,完全可以用更强的 SHA-256。算法领域就是这样,今天还在用的东西,明天可能就既是历史又是警示。

7.4 随机森林回归:从单棵树到森林

随机森林回归的思路比理论复杂得多,其实很好理解。你先训练很多棵决策树,每一棵树的训练数据是有放回抽样出来的,每一棵树的节点分裂只在随机挑选的特征子集里找最优分裂。预测时,让所有树各自给出结果,再取平均。

它为什么效果通常比单棵树好?答案就是“三个臭皮匠顶个诸葛亮”的统计版:单棵树容易过拟合特定样本和特征,但很多棵树各自的偏差不一样,平均之后,过拟合的部分被抵消了。

8. 算法在真实行业里的落地地图:给你几个高价值方向

8.1 推荐系统与搜索:算法变现的前线

你一定听过“猜你喜欢”,它就是推荐算法的直接成果。整套链路包括:用户画像构建、召回(从亿级物品里粗筛出几百个候选)、排序(对候选做精排)、重排(保证多样性和商业规则)。排序环节,深度学习排序模型(比如 DeepFM)是主流;但冷启动阶段,靠的就是协同过滤、规则板权重这类更基础、更容易解释的算法。

搜索引擎的系统架构里,除了前面说过的倒排索引,还包括:查询理解(分词、纠错、意图识别)、召回与排序、结果多样性控制。每一环都有相应的算法在支撑。如果你想入行做算法工程师,推荐和搜索方向通常是最容易找到机会的。

8.2 工业控制与物联网:算法的最忠实信徒

工业现场的传感器数据噪声大、环境恶劣、算力受限。这里不需要大模型,需要的是 PID、滑动平均滤波、CRC 校验、Modbus 协议这种又小又稳的算法。我见过不少做软件的工程师,一听到“嵌入式”“工业控制”就觉得跟算法没关系,其实这里反而是算法需求极其密集的领域。一个温控系统要稳定在 0.1 摄氏度以内,PID 参数不调好,其他一切白搭。

8.3 生物信息与医疗影像:算法直接关系到生命

DNA 序列比对使用的 Smith-Waterman 算法,其实是字符串编辑距离的动态规划变种;医疗影像里的病灶检测,用的是 YOLO 这类目标检测算法;电子病历里的诊断分类,用的是深度学习文本模型。这些方向都需要跨学科背景,但对算法本身的依赖度非常高,属于算法价值最能被直接感知的领域。

8.4 自动驾驶与机器人:算法把所有部件串成整体

感知层用卷积神经网络识别障碍物、车道线,定位层用粒子滤波或图优化做高精度定位,决策规划层用有限状态机、轨迹规划、强化学习来生成可执行轨迹,控制层则用 PID 或模型预测控制让车辆精确跟随轨迹。不管哪一环,背后都是一堆算法的接力赛。每一个都能单独讲一小时,但核心思想还是老三样:感知、决策、控制。

9. 给想靠算法吃饭的人几句掏心窝的话

我这十来年最大的感受是:算法不是用来“秀”的,是用来“扛”问题的。你不需要在每个领域都成为专家,但你应该具备一种能力——面对一个新问题时,能快速判断它属于哪一类经典问题的变种,然后找到距离最近的成熟算法作为起点。这不是天赋,而是刻意练习出来的模式识别能力。

我的练习方法是定期做一类“算法翻译”训练:拿生活中的问题,把它翻译成一个算法可描述的问题。比如“如何在食堂窗口排队时间最短”是个调度问题;“如何在预算内配置一台最合适的电脑”是个背包问题;“如何规划假期自驾路线,玩最多的景点又不太赶”是个带约束的路径优化问题。你练得越多,算法思维就越像母语,而不是第二外语。

有几句实在话,我特别想对刚入门的读者说:

第一,别怕数学,但也别先啃数学。大多数算法在你需要的层面,“数学”其实就是加法、比较和循环。复杂的证明可以等你遇到瓶颈再回来补,千万不要为了学算法先把自己埋进数学分析里。

第二,别迷信“算法面试刷三五百题”。刷题是手段,不是目的。刷题时真正的收获,是你见过问题的常见变体、积累了解题模式。如果刷完脑子只剩代码碎片,纯属浪费时间。

第三,动手是检验理解的唯一标准。看了这么长的文章,不如你此刻花二十分钟,把那个“找最大数”的程序用你熟悉的语言写出来、画出来流程图、测试五组边界数据。完成这一步,你今天这篇文章就没白看。

关于算法还能怎么继续深入,我的建议很具体:先掌握排序、查找、递归、动态规划、图论这五个核心模块,然后用一个真实的小项目把它们串起来用一遍。比如自己写一个迷你搜索引擎、一个待办事项调优器、或者一个自动排课程序。项目不在大,在于完整地经历“需求分析—算法设计—实现—测试—优化”的闭环。

这也是我写这篇文章的初衷:希望有一天,你再看到各种算法热搜词时,心里想的不是“又来了一个新名词”,而是“哦,这大概是某个老朋友换了一身衣服”。算法世界从来没有那么多新鲜事,真正重要的从来都是能否看清问题的本质,并且选对那把对应的钥匙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询