十大基础算法清单:排序、二分、动态规划与KMP一网打尽
“算法”这两个字常年挂在搜索热榜上今天有人问冒泡排序明天有人问粒子群后天热搜又变成KMP和动态规划。我经常被读者问同一个问题如果要补算法基础到底先把哪十个吃透我给过的答案一直在变但里面有一批名字从来没有下去过。这篇文章想把这份清单讲明白它们是什么、解决什么问题、为什么是它们以及踩过哪些坑之后你才能底气十足地说一句“这个算法我会了”。这份清单主要面向三类人准备校招和算法面试的应届生、打信奥或ACM的竞赛党、以及转行自学编程想补基础的人。我不打算把所有算法都塞给你而是先选出十个覆盖面最广、复用率最高的基础算法每一个都讲清楚原理、代码、适用边界和常见误区。基础打牢之后粒子群、卡尔曼滤波、深度学习这类方向算法你自然知道该从哪里切入。1. 这份十大清单是怎么定的标准比名单本身更重要网上搜“十大算法”能搜出各种互相打架的版本。有的把傅里叶变换列进去有的把整数分解当成核心还有的干脆把机器学习里的梯度下降也算进来。这些说法不是不对而是它们的选取标准完全不同。我选这十个标准只有三条跨领域复用率高、面试和竞赛考频高、能作为学习更复杂算法的垫脚石。1.1 我给的基础算法名单编号算法一句话定位1二分查找有序结构上把复杂度降到对数级的万能钥匙2快速排序分治思想最出名的代表作3归并排序稳定排序和逆序对问题的基石4贪心算法用局部最优推导全局最优但前置条件很多5动态规划用状态定义和转移方程处理重叠子问题6回溯算法给暴力搜索加剪枝的通用框架7DFS/BFS图与树的遍历基础也是状态空间搜索的起点8Dijkstra算法非负权图最短路问题的标杆9KMP算法字符串匹配的线性复杂度解法10哈希表O(1)查找的工程基础面试和开发都躲不开你可以明显看出来这十项里有些严格说不算“算法”比如哈希表更像数据结构DFS/BFS更像遍历方法。但实际面试和工程里不会有人跟你抠这个边界它们就是你在解决具体问题时顺手就要用的基本功。我见过的候选人算法题卡住往往不是因为不会套模板而是这些基本功里有某个环节想不清楚。1.2 为什么粒子群、卡尔曼、PID这些没进来热搜词里有一大堆看起来很高级的算法粒子群、模拟退火、卡尔曼滤波、PID、YOLO、NSGA-II。如果按“工程里用得多”的标准它们当然重要。但如果你把基础算法和方向算法混在一份清单里学习节奏就乱了。粒子群和模拟退火属于启发式优化算法适合在搜索空间巨大、精确解算不出来的场景。卡尔曼滤波是状态估计方向的核心PID是控制领域的常客它们都需要一定的工程背景和数学基础。我的建议是先把这份榜单里的十个基础算法练熟再根据你的方向去选专业算法。基础不牢的时候接触粒子群很容易变成调包侠参数不知道为什么要改结果不对也不知道往哪个方向排查。2. 二分查找从有序数组到单调答案的万能钥匙二分查找是个很有意思的算法。代码不到十行看起来谁都会写但一到面试手写就翻车。LeetCode上关于二分的题目非常多二分答案这种思路还被用到了“最小化最大值”“最大化最小值”这类问题上。可以说二分是整个算法体系里性价比最高的一招。2.1 三个反复出现的边界Bug先给一段标准实现大家可以逐行对照自己的写法def lower_bound(nums, target): left, right 0, len(nums) # 左闭右开区间 while left right: mid left (right - left) // 2 if nums[mid] target: left mid 1 else: right mid return left # 第一个 target 的下标我见过最多的坑有三个第一个mid (left right) // 2在极端情况下可能溢出。left right如果接近整数上限结果会变成负数。写成left (right - left) // 2不只是风格问题是实打实的防错。第二个循环条件到底用left right还是left right。这取决于你的区间定义。如果你维护的是左闭右闭区间初始right len(nums) - 1循环条件用left right如果你维护左闭右开初始right len(nums)循环条件用left right。最忌讳的是把两种写法混在一起一边用左闭右开的初始值一边用左闭右闭的更新逻辑。第三个mid到底加不加一。判断nums[mid] target时说明mid不可能是答案所以leftmid1反之mid有可能是答案所以rightmid。这个逻辑的根源是“循环不变量”你要始终清楚答案在哪个区间里。2.2 二分答案把最值问题变成判定问题二分的本质不是“在数组里找数”而是“在满足单调性的候选答案里搜索”。很多看起来和二分无关的题目其实都能用二分答案来做。举一个经典例子给定一个数组要求分成 m 段让“每段和的最大值”尽量小。直接求这个最小值很难但如果给你一个候选答案 X问“是否存在一种分段方式使得每段和都不超过 X”这个判定问题就很简单贪心地从头往后扫超过 X 就切一刀最后看段数是否不超过 m。def feasible(nums, m, limit): cnt 1 cur 0 for x in nums: if cur x limit: cnt 1 cur x else: cur x return cnt m def split_array(nums, m): left, right max(nums), sum(nums) while left right: mid (left right) // 2 if feasible(nums, m, mid): right mid else: left mid 1 return left这套模板我建议直接背下来因为“最大值最小化”和“最小值最大化”这两类题目在面试和竞赛里出现的频率非常高。二分答案的关键点就一个判定函数必须单调。如果答案是 X 时可行那么所有比 X 大的答案都应该可行这时候才能二分。这个单调性如果不成立二分就是错的。浮点数二分稍微有一点不同一般用固定迭代次数而不是精度。比如求平方根循环 100 次足够把 double 的精度压满比纠结while (right - left eps)更省心因为你不用去猜 eps 会不会太小导致死循环。3. 排序面试不问冒泡但你得懂冒泡之上的选择排序算法是算法学习绕不开的第一座山。很多人背了一堆排序的名字和复杂度真到用的时候还是只会sort()。这不够。你得知道它在内部经历了什么这样才能在“内存敏感”“需要稳定”“数据几乎有序”这些特殊场景下做出正确选择。3.1 快排、归并、堆排到底该怎么选先看一张对比表算法平均时间复杂度最坏时间复杂度额外空间稳定性冒泡排序O(n²)O(n²)O(1)稳定快速排序O(n log n)O(n²)O(log n)不稳定归并排序O(n log n)O(n log n)O(n)稳定堆排序O(n log n)O(n log n)O(1)不稳定很多初学者不理解为什么有这么多排序选一个最牛的用不就行了吗问题在于“最牛”不存在。快速排序平均最快但最坏情况下退化成 O(n²)归并排序稳定且最坏也是 O(n log n)但要 O(n) 额外空间堆排序不需要额外空间但常数大而且不稳定。工程上的做法是混着用C STL 的sort就是内省排序快排为主递归深度过深就切到堆排序区间小于 16 个元素时切到插入排序。Python 的Timsort则是归并排序和插入排序的混合体专门利用数据中已经有序的子序列。如果你需要手写一个快排核心部分在 partition。为了规避最坏情况随机选 pivot或者用三路切分处理大量重复元素的场景。这里给一个简洁版本def quick_sort(nums, left, right): if left right: return pivot nums[(left right) // 2] i, j left, right while i j: while nums[i] pivot: i 1 while nums[j] pivot: j - 1 if i j: nums[i], nums[j] nums[j], nums[i] i 1 j - 1 quick_sort(nums, left, j) quick_sort(nums, i, right)3.2 排序的衍生功力逆序对、topK、非比较排序排序本身是手段真正值钱的是它的衍生应用。逆序对数量就是经典例子在归并排序合并左右两个有序数组时如果右边元素先被拿下来说明左边剩下那些元素都比它大它们都和它构成逆序对。这样一次归并就能统计出一批逆序对总复杂度仍然是 O(n log n)。def merge_sort_count(nums, left, right): if right - left 1: return 0 mid (left right) // 2 cnt merge_sort_count(nums, left, mid) merge_sort_count(nums, mid, right) tmp [] i, j left, mid while i mid and j right: if nums[i] nums[j]: tmp.append(nums[i]) i 1 else: cnt mid - i tmp.append(nums[j]) j 1 tmp.extend(nums[i:mid]) tmp.extend(nums[j:right]) nums[left:right] tmp return cnttopK 问题则是堆排序的经典应用。求第 K 大或前 K 大维护一个大小为 K 的小顶堆每次遇到比堆顶大的元素就替换掉。复杂度 O(n log K)当 K 远小于 n 时非常划算。很多人忽略非比较排序但计数排序、基数排序、桶排序在特定场景下能打到 O(n)。比如给 0 到 100 分的成绩排序直接开一个 101 长度的数组统计频次就行完全不需要比较。这里提醒一句别把非比较排序当成银弹它的前提是数据范围可枚举、分布相对均匀否则空间开销会失控。4. 分治与回溯拆问题剪搜索分治和回溯都属于“递归思想”的具体化。分治是把一个问题切成互不相干的子问题分别解决再合并回溯则是把所有可能走一遍中途发现走不通就退回上一步。两者看起来都是“递归”但解决的问题类型完全不同。4.1 分治三步走以及归并排序为什么是教科书分治有三个步骤分解、解决、合并。分解要保证子问题相互独立合并要把子问题的结果正确处理。归并排序完美呈现了这三个步骤它的核心逻辑全部在“合并”这一步两个已经有序的数组线性扫描一遍就能合并成一个有序数组。最近点对问题是另一个很好的例子。平面上给一堆点求距离最近的两个点。暴力是 O(n²)分治做法是按 x 坐标排序后从中间切一刀分别递归求左右两半的最近距离 d然后只需检查距离中线不超过 d 的那些点。把这些点按 y 坐标排序后每个点只需要和它后面固定数量的点比较就能在线性时间内完成合并检查。整体复杂度 O(n log n)。我自己判断一道题能不能用分治就看两个标志子问题能不能独立求解合并子问题的结果是不是比暴力求解更便宜。如果子问题之间有很多重叠那就不是分治的领地而是动态规划的地盘。斐波那契就是个典型用递归分解会重复计算大量子问题所以它更适合 DP 而不是分治。4.2 回溯暴力搜索的优雅打开方式回溯算法的模板非常固定核心就四个字选择、撤销。写一个 N 皇后问题的框架你就能感受到它的骨架def backtrack(路径, 选择列表): if 满足结束条件: 记录结果 return for 选择 in 选择列表: 做选择 递归进入下一层 撤销选择新手最容易犯的错就是递归完之后忘记“撤销选择”。比如用 Python 写组合总和path是全局列表往深处递归之前append了元素递归返回后如果不pop下一层分支就会带着上一层脏数据跑结果全乱。这个错误在 Java 和 Python 里特别容易踩因为 list 是引用类型。剪枝是回溯的效率核心也是最考验题感的地方。最常见的剪枝有两类一类是排序后跳过重复元素避免生成重复组合另一类是当前累加和已经超过目标就提前返回。下面是组合总和 II 的一个剪枝示例def combination_sum2(candidates, target): candidates.sort() res [] path [] def dfs(start, rest): if rest 0: res.append(path[:]) return for i in range(start, len(candidates)): if candidates[i] rest: break # 剪枝后面更大不可能凑出 rest if i start and candidates[i] candidates[i - 1]: continue # 剪枝同一层跳过重复值 path.append(candidates[i]) dfs(i 1, rest - candidates[i]) path.pop() # 撤销选择 dfs(0, target) return res回溯和 DFS 的关系其实是包含关系回溯本质上就是 DFS 在状态空间树上的搜索。你可以把每一层递归想象成树的一层把“可选列表”想象成当前节点的儿子节点。剪枝就是提前砍掉肯定不可能的子树。5. 贪心 vs 动态规划一对容易混淆的兄弟贪心和动态规划经常放在一起讲因为它们都要求“最优子结构”也就是最后一步的最优解依赖于前一步的最优解。但两者的策略完全不同贪心每一步只做“当前看起来最好”的选择做完就不回头动态规划会保留所有可能状态的取值最后再从里面挑最优。5.1 贪心算法的“局部最优”陷阱我遇到太多人一看到题目说是“最优”“最大”“最少”就直接上贪心结果错得莫名其妙。贪心不是不能错而是你必须能证明——至少能构造直觉这个局部最优是不是一定导向全局最优。找零钱就是一个经典的反例。假设有面值 1、3、4 的硬币要凑出 6 元。贪心策略优先选最大面值会选 411用了三枚硬币。但最优解是 33只要两枚。这个例子充分说明贪心选择性质一旦不成立算法就是错的。那什么时候贪心是对的呢看两个条件贪心选择性质和最优子结构。前者指“每一步的局部最优选择最终能组成全局最优解”后者指“子问题的最优解能递推出原问题的最优解”。比如区间调度问题按结束时间最早排序每次选一个结束最早的区间然后跳过所有和它重叠的区间这样最终选出的区间数量一定最多。它的直觉是越早结束的区间越不可能和后面的区间冲突给后面留的空间越大。跳跃游戏也是一个很好的贪心案例。判断能不能跳到最后一个位置只需要维护一个max_reach变量每走一步更新能到的最远位置一旦max_reach覆盖到终点就返回 True。5.2 动态规划状态定义才是灵魂动态规划的难点从来不是写代码而是找状态定义和转移方程。我教新手时常用一条推导路径先写暴力递归再从递归参数里抽象出状态然后加缓存变成记忆化搜索最后把递归改成递推。这条路径每一步都很机械能覆盖大多数 DP 题。以 0/1 背包为例dp[i][c]表示前 i 个物品、容量为 c 时能获得的最大价值。转移方程只有两个方向不选第 i 个物品或者选第 i 个物品并获得value[i]。一维滚动数组的代码如下def knapsack(weights, values, capacity): dp [0] * (capacity 1) for i in range(len(weights)): for c in range(capacity, weights[i] - 1, -1): dp[c] max(dp[c], dp[c - weights[i]] values[i]) return dp[capacity]为什么第二层循环必须倒序因为 0/1 背包要求每个物品只能用一次。倒序遍历时dp[c - weights[i]]还是上一轮没选当前物品的结果正序遍历的话dp[c - weights[i]]可能已经被本轮更新过等于当前物品被重复使用了。这个细节面试里经常考我在实际写代码时也见过好几个人在这栽跟头。如果你是初学者我强烈建议从“爬楼梯”和“斐波那契”入手理解 DP 的基本形态然后用“最长递增子序列”理解状态不是单个变量而是多个维度。之后再去硬啃背包问题你会发现 DP 没那么玄乎核心就是你把问题抽象成状态的视角。5.3 怎么判断该用贪心还是 DP这个问题没有银弹但有一个很实用的判断方式当前选择会不会影响未来的选择空间。还是用找零钱那个例子你选了 4 之后后面只能用 1 和 1 去补已经错失了用两张 3 凑 6 的可能。这类“选择会影响未来”的题目贪心大概率失效应该考虑 DP。但如果“当前怎么选并不会影响剩余问题的结构”比如区间调度里选了最早结束的区间后剩下的是一个更短的区间集合贪心就有机会。我个人的经验是一道题如果一眼看不出贪心怎么证明就先写暴力递归或状态搜索。因为在递归搜索的代码里你能看到状态是怎么转移的进而更容易提炼出状态定义和转移方程。这比凭空想 DP 要可靠得多。6. 图遍历与最短路径BFS/DFS 到 Dijkstra 的一条线图算法是基础算法里应用面最广、也最能拉开差距的部分。很多人把 DFS、BFS、Dijkstra 当成三个孤立的算法其实它们是一条线从“暴力遍历”到“按层遍历”再到“按距离遍历”。理解这条线你会觉得图论不再是一堆模板而是一套可以自己推演的体系。6.1 DFS 和 BFS从“能否走通”到“最少几步”图的表示方式优先用邻接表。Python 里可以用字典键是节点值是邻居列表C 里可以用vectorint adj[N]。DFS 适合处理连通性、路径枚举、拓扑排序这类问题。它的代码可以递归也可以显式用栈关键在于记录“已访问”状态否则会在环里无限循环。BFS 则天然适合求无权图的最短路因为 BFS 按层展开第一次到达某个节点的步数一定是最少的。给一个 BFS 求迷宫最少步数的模板from collections import deque def min_steps(maze, start, end): m, n len(maze), len(maze[0]) dist [[-1] * n for _ in range(m)] dist[start[0]][start[1]] 0 q deque([start]) directions [(1,0), (-1,0), (0,1), (0,-1)] while q: x, y q.popleft() if (x, y) end: return dist[x][y] for dx, dy in directions: nx, ny x dx, y dy if 0 nx m and 0 ny n and maze[nx][ny] ! # and dist[nx][ny] -1: dist[nx][ny] dist[x][y] 1 q.append((nx, ny)) return -1这里dist数组同时承担了“是否访问过”和“最短距离”两个职责少一个判断条件都会出问题。我见过不少人在 BFS 里忘记判断越界或者忘记判断障碍物结果调试半天。6.2 Dijkstra把普通队列升级成优先队列BFS 能求最少步数是因为每走一步代价都是 1。如果边的权重不同BFS 的“按层”就不成立了这时需要 Dijkstra 算法。它的核心思想是每次从未确定的节点里选一个距离起点最近的然后用它去松弛邻居。为什么 Dijkstra 要求所有边的权重非负因为算法基于一个贪心假设当前距离起点最近的未确定节点它的最短距离已经被找到了。如果存在负权边这个假设会被打破——某个节点现在看起来远但通过一条负权边过来反而更近。负权图只能用 Bellman-Ford 或 SPFA它们的思路是反复松弛所有边来逼近真实距离。堆优化的 Dijkstra 模板import heapq def dijkstra(graph, start): n len(graph) dist [float(inf)] * n dist[start] 0 pq [(0, start)] while pq: d, u heapq.heappop(pq) if d dist[u]: continue # 过期的堆元素跳过 for v, w in graph[u]: if dist[u] w dist[v]: dist[v] dist[u] w heapq.heappush(pq, (dist[v], v)) return dist经常有人问为什么堆里会出现过期的元素因为同一个节点可能被不同路径多次更新每次更新都会往堆里 push 一个更大或更小的距离值。当某个节点已经被确定最短距离后堆里残留的旧距离元素就是过期元素直接跳过即可否则可能重复处理同一个节点导致复杂度退化。A* 算法可以理解为 Dijkstra 的启发式版本在选择下一个要扩展的节点时不用单纯的dist[u]而是用dist[u] heuristic(u)。启发式函数估计当前节点到终点的距离方向性越强搜索越快如果启发式为 0A* 就退化成 Dijkstra。这也是面试里常被问到“A* 和 BFS 的区别”时最核心的回答角度BFS 只按层扩展A*/Dijkstra 按代价扩展A* 额外带方向信息。7. KMP字符串匹配里的线性魔法字符串匹配在工程里太常用了find、index、search这些方法每天都在调用。但你想过它们的内部实现是什么样的吗朴素算法失配时主串要回退模式串也要从头开始复杂度 O(nm)。KMP 的厉害之处在于它让主串指针永不回头失配时模式串跳到合适的位置继续匹配整体复杂度降到 O(nm)。7.1 前缀函数KMP 真正的核心KMP 最难理解的部分不是匹配过程而是前缀函数pi的构建。pi[i]表示模式串pat[0..i]的最长相等真前后缀的长度。比如模式串ABABC它的前缀函数就是[0, 0, 1, 2, 0]AB的前后缀没有相等ABA有长度为 1 的相等前缀后缀 AABAB有长度为 2 的 AB。构建前缀函数的代码def build_pi(pat): m len(pat) pi [0] * m j 0 for i in range(1, m): while j 0 and pat[i] ! pat[j]: j pi[j - 1] # 回退到上一个可能匹配的前缀 if pat[i] pat[j]: j 1 pi[i] j return pi匹配过程和构建前缀函数在形式上非常像只是把“模式串和自己匹配”变成“模式串和文本串匹配”def kmp_search(text, pat): pi build_pi(pat) j 0 for i in range(len(text)): while j 0 and text[i] ! pat[j]: j pi[j - 1] if text[i] pat[j]: j 1 if j len(pat): return i - j 1 return -1很多新手看不懂j pi[j - 1]这行到底是干嘛的。我的理解方式是当pat[i]和pat[j]失配时我们希望找到一个更短的、已经匹配过的前缀让它继续和当前位置的字符比较。pi[j-1]正好表示“pat[0..j-1]”这个已匹配部分的最长相等真前后缀长度也就是下一轮可以继续利用的匹配长度。这个过程本质上是“模式串自己和自己匹配”。7.2 KMP 的常见坑和延伸应用下标从 0 开始还是从 1 开始是写 KMP 最容易混乱的地方。我的建议是统一用 0 下标并且把前缀函数定义为“长度为 i 的前缀子串的最长相等真前后缀长度”这样写代码最不容易出错。如果你习惯了 C 里从 1 开始的前缀函数写法切到 Python 时一定要把下标逻辑重新过一遍。KMP 还有一个很有用的延伸求字符串的最小循环节。如果len % (len - pi[-1]) 0说明字符串由一个长度len - pi[-1]的子串重复多次构成。比如abcabc的pi[-1]是 3所以最小循环节长度是 3。这个性质在竞赛和面试中都出现过。虽然实际工程里你几乎不会手写 KMP标准库的正则表达式和字符串查找算法内部可能已经是更复杂的 BM、Sunday 或双指针方法的组合但 KMP 的思想——把已匹配过的信息提炼出来复用——是理解所有字符串算法的关键一跳。8. 哈希表与双指针被低估的日常基本功这一节讲的两样东西严格说一个算数据结构一个算解题技巧但它们在算法题和工程应用里出现的频率高到必须放到基础清单里。面试里“两数之和”刷屏滑动窗口相关的题目常年霸占 HOT 100背后靠的都是这两板斧。8.1 哈希表为什么能 O(1)以及常见的坑哈希表的本质是数组随机访问的推广。数组可以通过下标 O(1) 拿到元素哈希表通过哈希函数把任意 key 映射成下标。冲突不可避免工程上的解决办法主要有链地址法和开放寻址法。简单说链地址法把冲突元素挂成一个链表开放寻址法在冲突时往后探测空位。Cunordered_map主要用桶加链表冲突多时可能升级成红黑树Python 的dict则使用开放寻址加二次探测。真正坑人的是哈希表的加入时机和删除时机。在算法题里用哈希表去重、计数、记录下标都是常规操作。但工程上要记住三点第一不要用可变对象当 keylist、dict这类对象作为键会导致哈希值不稳定第二如果你自定义了类的__hash__和__eq__两者必须保持一致的语义第三遍历哈希表时不要直接删除元素尤其 C 里要格外小心迭代器失效。8.2 双指针和滑动窗口把 O(n²) 降成 O(n)两个最常见的双指针模式分别是相向双指针和同向双指针也就是滑动窗口。相向双指针用于有序数组比如两数之和的经典解法左指针指向最小右指针指向最大如果和偏大就右指针左移偏小就左指针右移。同向双指针则维护一个窗口窗口的左边界和右边界只向一个方向移动。以“无重复字符的最长子串”为例滑动窗口的模板def length_of_longest_substring(s: str) - int: used set() left 0 ans 0 for right, ch in enumerate(s): while ch in used: used.remove(s[left]) left 1 used.add(ch) ans max(ans, right - left 1) return ans这套模板的思路是右指针不断向右扩展如果窗口内出现重复字符就收缩左边界直到没有重复。窗口始终保存一个“合法状态”答案就在这个滑动过程中更新。很多人写滑动窗口容易把 left 和 right 的关系搞混我的习惯是始终把窗口定义成左闭右开区间[left, right)这样所有下标更新都遵循同一个规则不容易乱。双指针的另一个变体是快慢指针用于链表中检测环比如 Floyd 判圈算法快指针每次走两步慢指针每次走一步如果两者能相遇说明链表有环。这个思想的变体在链表类题目里非常多面试中经常作为找环入口的前置问题出现。9. 学完十大之后下一步怎么走前面八个章节把十大基础算法的主体内容讲完了但我知道很多人拿到这份名单后还是会困惑背完这些模板之后我的算法能力到底提升了多少会不会只是从“什么都不会”变成“背了一堆模板”我的看法是基础算法的价值不在模板本身而在它们训练出来的四种能力抽象建模能力、复杂度分析能力、边界条件敏感度、以及把复杂问题拆成已知问题的迁移能力。你学二分时反复推敲边界条件学 DP 时反复打磨状态定义学 KMP 时理解如何利用历史信息复用计算——这些能力迁移到任何方向算法上都适用。9.1 从基础算法到方向算法当你把这份清单吃透之后再看热搜里的那些方向算法眼光会完全不一样。学机器学习梯度下降本质上是“沿最速下降方向迭代优化”K-Means 和 EM 算法依赖的轮换迭代思想你在基础算法里已经见过学控制与信号处理卡尔曼滤波核心是预测加观测修正PID 是误差驱动的反馈控制它们都需要你具备“状态”和“转移”这两个 DP 基本概念学启发式优化粒子群、遗传算法、模拟退火本质是在搜索空间里做“启发式跳跃”这跟回溯搜索、剪枝是同一个思维体系的不同分支就连深度学习里的 YOLO 这类视觉算法底层也离不开卷积、池化这些基础算子的复杂度分析和数据流理解。也就是说基础算法不是终点而是通向这些方向的桥。你不需要先把十大算法全刷满 100 道题再转向我通常建议的是这份清单里的每个算法至少上手写过 3 到 5 道题并且能把其中一道题给完全不懂的人讲明白再开始接触方向算法。9.2 三个月吃透基础算法的练习建议如果按三个月来安排我给的路线大致是这样第一个月主攻数组、字符串、哈希表、链表、栈、队列把这些基础数据结构和对应的双指针、滑动窗口、哈希技巧练到顺手第二个月主攻二叉树、二分、排序、DFS/BFS开始建立递归思维和搜索思维第三个月主攻回溯、贪心、动态规划、图最短路和 KMP这时你已经可以开始挑战 HOT 100 里的中高难度题了。平台选择上LeetCode Hot 100 对面试党足够信奥党可以直接刷 CSP-J/S 真题和《算法竞赛入门经典》的例题。我个人的三点建议是第一给每类算法整理一张自己的模板卡写上核心代码框架、适用条件、复杂度、易错点第二刷题别只追求 AC 数量把一道题用二分、贪心、DP 三种思路各做一遍比重复刷十道简单题有用第三错题复盘时只问两个问题——我的状态定义或边界条件哪里出了问题以及下次怎么写才能避免同样的问题。我带过的人里进步最快的从来不是刷题最多的而是肯把一道题掰开揉碎讲清楚的人。当你闭上书能把 KMP 的前缀函数推导一遍能自己说出为什么背包一维数组要倒序遍历能画出一个滑动窗口从空到满的完整变化过程这份“十大基础算法”才真正消化成了你自己的东西。