在实际开发中,算法不是考试题,而是一组解决具体问题的决策规则。同样一批数据,排序可以用冒泡,也可以用快速排序;同一个地图,导航可以走 Dijkstra,也可以走 A*;同一个质检任务,可以写固定阈值,也可以用 YOLO 做目标检测。算法选型最终会体现在执行时间、内存开销、结果稳定性和维护成本上。下面从分类开始,把开发中常见的算法拆开梳理:它们解决什么问题、原理是什么、关键参数怎么设置、最容易错在哪里,覆盖排序、KMP、Dijkstra、动态规划、模拟退火、粒子群、PID、FOC、反向传播、YOLO、强化学习、BM25 和 RETE 等算法。
1. 先建立算法分类观:算法解决的是哪几类问题
很多人在学算法时,把主要精力放在“记住某道题的答案”上,结果输入一变就不会做了。更好的办法是先建立一套分类观念:拿到问题后,先判断它属于哪一类,再候选这一类里的典型算法,最后结合数据规模和约束条件做验证。算法本质上不是代码片段,而是问题类型到解决方案的映射。
1.1 算法不是代码,而是问题类型的映射
举个例子:
- 给一组数字排序,属于排序类问题,候选方案有快速排序、堆排序、归并排序。
- 在一个长文本里查找模式串,属于字符串匹配类问题,候选方案有 KMP、BM。
- 在带权图中找最短路径,属于图论类问题,候选方案有 Dijkstra、Bellman-Ford。
- 在有限资源下最大化收益,属于优化类问题,候选方案有贪心、动态规划、回溯搜索。
- 用历史数据预测未来,属于机器学习类问题,候选方案有线性模型、决策树、神经网络。
这种分类能力比背代码更重要,因为真实项目很少直接告诉你“这里应该用 KMP”,它只会给你一段日志、一批数据和一组性能指标。
1.2 八类常见算法速览
| 算法类别 | 要解决的核心问题 | 典型算法 | 典型场景 |
|---|---|---|---|
| 基础数据结构与排序查找 | 数据如何组织、有序、快速命中 | 快速排序、堆排序、二分查找、哈希表 | 订单列表、TopN、去重 |
| 字符串匹配 | 在文本中定位模式串 | KMP、BM | 文本编辑器、日志匹配 |
| 图论与路径搜索 | 在关联关系中求最短路径或可达性 | Dijkstra、A*、DFS/BFS | 地图导航、任务调度 |
| 经典优化 | 在有限选择中求最优解 | 贪心、动态规划、回溯+剪枝 | 背包、路径规划、排课 |
| 启发式优化 | 在搜索空间过大时求近似最优解 | 模拟退火、粒子群 | 组合优化、参数调优 |
| 工程控制与信号处理 | 让设备输出稳定或完成信号变换 | PID、FOC、重采样、图像增强 | 电机控制、音频处理、ISP |
| 机器学习与深度学习 | 从样本中学习规律并泛化 | 反向传播、CNN、YOLO、强化学习 | 分类、检测、自动决策 |
| 搜索引擎与规则引擎 | 相关性排序和规则匹配 | BM25、RETE | 搜索召回、规则风控 |
实际项目往往不是单一算法,而是多个算法组合。搜索系统会先用 BM25 召回候选文档,再用排序模型精排;AGV 导航会先用 A* 搜索路径,再用 PID 控制底盘按路径运动。
1.3 为什么顺序是“分类 -> 候选 -> 约束 -> 验证”
正确做法是先给问题分类,再列出候选算法,再根据约束条件筛选,最后用最小用例验证。
约束条件通常包括:
- 数据规模:是几千条还是几亿条。
- 实时性:是离线计算还是在线响应。
- 精确性:必须全局最优还是近似解可接受。
- 硬件资源:CPU、GPU、内存、显存限制。
- 维护成本:这个算法团队是否能长期维护。
这套流程可以直接用在技术方案评审里,避免一上来就写代码。
2. 数据结构与经典算法:排序、字符串匹配与快速幂
2.1 快速排序:平均复杂度要牢记,最坏情况也要警惕
快速排序的平均时间复杂度是 O(n log n),在大多数语言的内置排序实现中都占据核心地位。它通过分治策略,把数组拆成小于基准值和大于等于基准值的两部分,再递归排序。
public static void quickSort(int[] arr, int left, int right) { if (left >= right) { return; } int pivot = partition(arr, left, right); quickSort(arr, left, pivot - 1); quickSort(arr, pivot + 1, right); } private static int partition(int[] arr, int left, int right) { int pivot = arr[right]; int i = left; for (int j = left; j < right; j++) { if (arr[j] < pivot) { swap(arr, i, j); i++; } } swap(arr, i, right); return i; } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }这段代码里,partition每次返回基准值的最终位置。需要注意,如果基准值恰好是当前区间的最小值或最大值,快速排序会退化成 O(n²),递归深度也会变大。生产级实现一般会采用三数取中、随机基准或双轴快排,避免数据分布导致性能退化。
2.2 KMP:主串指针不回退的字符串匹配
KMP 算法的核心是 next 数组。next[i] 表示模式串前 i 个字符组成的子串中,最长相等前后缀的长度。发生失配时,主串指针不后退,只移动模式串指针,因此匹配过程整体接近 O(n)。
以模式串p="abacaba"为例,手算 next 数组的结果如下:
| i | 子串 | 最长相等前后缀长度 |
|---|---|---|
| 1 | a | 0 |
| 2 | ab | 0 |
| 3 | aba | 1 |
| 4 | abac | 0 |
| 5 | abaca | 1 |
| 6 | abacab | 2 |
| 7 | abacaba | 3 |
下面是求解 next 数组的 Java 实现:
public static int[] buildNext(String p) { int m = p.length(); int[] next = new int[m]; int j = 0; for (int i = 1; i < m; i++) { while (j > 0 && p.charAt(i) != p.charAt(j)) { j = next[j - 1]; } if (p.charAt(i) == p.charAt(j)) { j++; } next[i] = j; } return next; }这里有一个实际开发中很容易踩的坑:不同教材对 next 数组的定义不同。有的定义是“失配时模式串跳转到的位置”,有的定义是“最长相等前后缀长度”。如果是后者,代码里要从next[j - 1]回退;如果是前者,回退逻辑会不一样。写代码前一定要先明确定义,否则会频繁出现数组越界或死循环。
2.3 快速幂:用二进制分解压缩幂运算
计算a^n,最直接的做法是连乘 n 次,复杂度 O(n)。快速幂把指数拆成二进制,把乘法次数压缩到 O(log n)。
public static long fastPow(long a, long n, long mod) { long result = 1 % mod; a %= mod; while (n > 0) { if ((n & 1) == 1) { result = result * a % mod; } a = a * a % mod; n >>= 1; } return result; }这段代码的思路是:从最低位开始看 n 的二进制。当前位是 1,就乘上对应的a的幂;每次迭代把a自乘,相当于把指数翻倍。这个算法在 RSA 相关计算、大数取模、矩阵快速幂中很常见。常见错误是忘记取模、没有处理n=0和mod=1的边界情况。
3. 图论与路径搜索:Dijkstra 与 A*
图论问题的核心是“节点 + 边 + 权重”,常见需求包括最短路径、可达性、拓扑排序、最小生成树等。实际项目里最常用的是路径搜索,这里拆解 Dijkstra 和 A* 两个算法。
3.1 Dijkstra:单源最短路径的经典方案
Dijkstra 解决的是单源最短路径问题,前提是边权非负。它每次从优先队列中取出当前距离最小的节点,然后松弛它的邻接边。使用最小堆实现后,复杂度约为 O((V+E) log V)。
import heapq def dijkstra(graph, start): dist = {node: float('inf') for node in graph} dist[start] = 0 pq = [(0, start)] while pq: d, u = heapq.heappop(pq) if d > dist[u]: continue for v, w in graph[u]: nd = d + w if nd < dist[v]: dist[v] = nd heapq.heappush(pq, (nd, v)) return dist这段代码有两个关键点。第一,if d > dist[u]: continue用于丢弃优先队列里的过期记录,避免同一个节点被反复处理。第二,Dijkstra 不能处理负权边。如果图中存在负权边,已经出队的节点可能被再次更新,得到错误结果,此时应该使用 Bellman-Ford 或 SPFA。
3.2 A*:在 Dijkstra 上引入启发式信息
A* 在 Dijkstra 基础上增加启发式函数,通过f(n) = g(n) + h(n)优先扩展“看起来更接近目标”的节点。其中g(n)是从起点到当前节点的实际代价,h(n)是当前节点到目标的估计代价。h(n) 必须满足可采纳性,否则 A* 不保证找到最优解。
A* 常见应用包括 AGV 路径规划、游戏寻路、地图导航辅助搜索。实际工程里,地图会被栅格化,再结合 JPS、跳点搜索等策略进一步提速。选型时如果希望保证最优路径,优先考虑 A*;如果更关注实时性和搜索速度,可以接受次优路径,也可以考虑采样类方法或分层规划。
4. 经典优化问题:贪心、动态规划与回溯剪枝
优化问题通常描述为“在若干可选决策中,求目标函数的最大值或最小值”,同时受约束限制。根据问题结构,可以选择不同的算法。
4.1 贪心:局部最优不一定全局最优
贪心算法每一步选择当前看来最好的选择,并且不再回溯。它适合具备“贪心选择性质”的问题,比如霍夫曼编码、部分背包、活动安排问题。但在 0/1 背包问题中,直接按单位价值贪心往往得不到全局最优解。
判断是否能用贪心,有一个简单的自检方式:能否证明“每一步局部最优,最终组合就是全局最优”。如果无法证明,说明问题可能具有重叠子结构,应该考虑动态规划或搜索。
4.2 动态规划:用状态转移覆盖重叠子问题
动态规划适合具有“重叠子问题”和“最优子结构”的问题。以 0/1 背包为例,dp[i][j]表示前 i 个物品装入容量为 j 的背包所能获得的最大价值。
def knapsack(weights, values, capacity): n = len(weights) dp = [[0] * (capacity + 1) for _ in range(n + 1)] for i in range(1, n + 1): w, v = weights[i - 1], values[i - 1] for j in range(capacity + 1): if j >= w: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w] + v) else: dp[i][j] = dp[i - 1][j] return dp[n][capacity]用一维滚动数组可以压缩空间,但遍历容量时必须倒序。如果正序遍历,同一个物品会被重复放入,结果会变成完全背包。这是动态规划初学者最常犯的错误。
4.3 回溯剪枝:暴力搜索也能有高质量实践
回溯算法在搜索树上遍历所有可能解,适合没有高效动态规划解法的组合问题。没有剪枝时,组合爆炸会直接拖垮程序。常见剪枝方式包括:
- 可行性剪枝:当前路径已经不可能满足约束,提前返回。
- 最优性剪枝:当前路径的代价已经大于已知最优解,提前返回。
- 对称性剪枝:去掉等价分支,减少重复搜索。
典型例子是 N 皇后、排列生成、排课问题。剪枝本身是一种工程能力,同样的问题,剪枝条件写得好,运行时间可能相差几个数量级。
5. 启发式优化算法:模拟退火与粒子群
当搜索空间过大、精确算法无法在有限时间内给出最优解时,可以使用启发式优化算法。它们的共同特点是不保证全局最优,但能快速逼近可用的近似解。
5.1 模拟退火:用温度控制随机接受差解
模拟退火模仿固体退火过程,在搜索过程中以一定概率接受比当前解更差的解,从而跳出局部最优。温度 T 随时间下降,接受较差解的概率通常用 Metropolis 准则计算:
P(accept worse) = exp(-delta / T)其中 delta 是新解与当前解的代价差。温度越高,越容易接受差解;温度越低,算法越趋向收敛。
实现时需要关注几个参数:
- 初始温度:决定前期搜索范围,过大会让收敛很慢,过小容易过早收敛。
- 降温速率:常用
T = T * alpha,alpha 一般取 0.9 到 0.99。 - 终止条件:温度降到阈值,或连续多次迭代没有改进。
5.2 粒子群算法:用群体信息更新个体位置
粒子群算法模拟鸟群觅食行为。每个粒子有自己的位置和速度,每次迭代参考个体历史最优pbest和群体历史最优gbest更新速度:
v = w * v + c1 * r1 * (pbest - x) + c2 * r2 * (gbest - x) x = x + v参数含义如下:
| 参数 | 作用 | 常见取值 |
|---|---|---|
| w | 惯性权重,控制继承上一时刻速度的程度 | 0.4 到 0.9 |
| c1 | 个体学习因子,控制向自身经验学习 | 1.5 到 2.0 |
| c2 | 社会学习因子,控制向群体经验学习 | 1.5 到 2.0 |
| r1, r2 | 随机数,用于保持种群多样性 | 0 到 1 |
粒子群的常见坑是早熟收敛:所有粒子快速聚集到局部最优,群体失去了继续探索的能力。缓解方式包括惯性权重线性递减、引入变异机制、或者与局部搜索混合。
6. 工程控制与信号处理中的算法:PID、FOC、重采样与图像增强
算法不只在后台服务里出现。在电机控制、音频处理、摄像头成像等场景中,算法直接作用于物理信号,稳定性要求更高。
6.1 PID 控制:比例、积分、微分如何配合
PID 是工业控制中最常用的闭环控制算法,根据目标值与实际值的偏差e(t)计算控制量:
u(t) = Kp * e(t) + Ki * integral(e(t)) + Kd * de(t)/dt- Kp 决定对当前误差的即时反应,过大会产生振荡。
- Ki 消除稳态误差,过大会导致超调。
- Kd 提供阻尼,抑制变化过快,但对噪声敏感。
常见坑是积分饱和。当执行器已经达到输出上限,偏差仍然存在,积分项会持续累积,等偏差反向时,输出需要很久才能回落,表现为明显超调。工程上需要做积分限幅、输出限幅或抗积分饱和处理。
6.2 FOC:电机控制里如何把三相电流变成直流量
FOC(Field-Oriented Control)常用于无刷电机和永磁同步电机控制。它通过 Clarke 变换把三相电流从 abc 坐标系变换到静止的 alpha/beta 坐标系,再用 Park 变换把交流量变换到随转子旋转的 dq 轴上,使得电流控制从交流跟踪问题变成直流调节问题,之后就可以用 PID 做线性控制。
学习 FOC 可以先理解坐标变换的目的:它把非线性耦合问题变成线性控制问题。再看 Clarke 变换和 Park 变换矩阵,最后在仿真或开发板上跑电流环和速度环。
6.3 音频重采样与图像锐化:信号处理算法在业务中的落点
音频重采样本质是采样率转换。当输入采样率与输出设备采样率不一致时,需要插值或抽取。常见方法有最近邻、线性插值、基于多相滤波器的重采样。生产环境中更关注抗混叠滤波和计算开销。
图像锐化中,拉普拉斯算子基于二阶微分提取图像边缘信息,再叠加回原图:
output = original + alpha * Laplacian(original)alpha 表示锐化强度。这个算子实现简单,但对噪声敏感,通常先做平滑再锐化。ISP 链路中 Bayer 到 RGB 的转换常用插值和去马赛克算法,是摄像头成像质量的关键环节。
7. 机器学习与 AI 方向的核心算法
机器学习算法解决的问题是“从样本中学习规律”。这类算法的特点不是一次运行得到结果,而是通过迭代训练逐步优化模型参数。
7.1 反向传播:深度学习模型训练的地基
反向传播算法是深度学习模型训练的核心机制。它通过链式法则,从损失函数开始,从输出层向输入层逐层计算每个参数的梯度,再用梯度下降更新参数。
训练流程可以概括为:
- 前向传播:输入 x 经过隐藏层得到预测值 y_pred,计算损失 L。
- 反向传播:从输出层开始,逐层计算损失对权重的梯度。
- 参数更新:使用 SGD、Adam 等优化器更新权重。
实际工程中,PyTorch 和 TensorFlow 会自动完成梯度计算,但理解梯度如何流动仍然是排查训练发散、梯度消失、学习率过大等问题的基础。比如 ReLU 激活函数可以缓解梯度消失,但学习率过大会导致梯度爆炸。
7.2 CNN、3D CNN 与 C3D:从图像特征到视频时空特征
二维 CNN 用于图像分类和目标检测,通过卷积核提取局部特征,通过池化降低分辨率,最后通过分类器输出结果。
3D CNN 把卷积核从二维扩展到三维,输入是连续帧组成的视频立方体,可以同时建模空间和时间信息,常用于行为识别、视频分类等任务。
要区分一点:3D CNN 和 C3D 不是完全等价的说法。C3D 是 3D 卷积网络的代表结构之一,而 2D CNN 加时序建模(如 LSTM、Transformer)是另一条常见路线。选型时要在计算量、实时性和识别精度之间平衡。
7.3 目标检测算法 YOLO:把检测当作回归问题
YOLO 系列把目标检测建模为一次前向推理,直接从图像中回归边界框和类别概率,不需要先做区域候选,因此特别适合实时检测场景,比如工业质检、安防、AGV 避障。
使用 YOLO 时要注意:
- 训练数据标注质量直接影响准确率。
- 小目标检测需要更高分辨率和合适的数据增强策略。
- 推理设备是 CPU、GPU 还是边缘硬件,直接决定选用哪个版本以及是否量化和剪枝。
7.4 强化学习、联邦平均与其他前沿方向
强化学习的核心是智能体通过与环境交互获得奖励,进而学习策略。与监督学习不同,强化学习没有现成的“标准答案”,只有延迟奖励。经典方法包括 Q-Learning、DQN、PPO 等,适用于游戏、机器人控制、推荐系统决策等场景。
联邦平均算法用于联邦学习场景:多个客户端在本地训练模型,只上传模型参数或梯度到中心服务器,由服务器做加权平均,从而减少原始数据集中传输。实际工程要处理非独立同分布数据、通信开销、隐私保护等问题。
前沿方向里,PCMCI 用于从观测数据中发现因果关系,EVA-02 这类视觉 Transformer 模型在图像分类任务中持续演进。这些算法虽然复杂度更高,但基本出发点仍然是“从数据里得到可用的结构和规律”。
8. 搜索引擎与规则引擎中的算法:BM25 与 RETE
搜索和规则匹配在业务系统里非常常见,但它们的实现细节容易被忽略。BM25 解决“哪些文档与查询更相关”,RETE 解决“规则如何高效匹配”。
8.1 BM25:相关性排序的经典公式
BM25 是文本检索中常用的相关度打分函数,综合词频、文档长度、逆文档频率和超参数,给出查询词与文档的匹配分。文档 D 对查询 Q 的 BM25 分值可以写成:
score(D, Q) = sum IDF(term) * tf_norm(term, D)其中tf_norm会考虑词频饱和和文档长度归一化。k1控制词频饱和度,b控制文档长度的影响程度,常见取值为k1=1.2到2.0,b=0.75。配合倒排索引,BM25 可以在召回阶段快速筛选相关文档,再交给精排