V

算法可视化

ZHEN
分享到微博分享到 XGitHub 仓库个人主页
学习工具算法复杂度速查算法学习路径
数据结构数组链表栈队列树堆哈希表图字典树并查集LRU 缓存跳表线段树B+ 树布隆过滤器树状数组
经典排序算法冒泡排序鸡尾酒排序双调排序选择排序插入排序二分插入排序希尔排序归并排序自顶向下归并快速排序三路快排双轴快排堆排序计数排序基数排序桶排序
图算法Dijkstra 最短路Kruskal 最小生成树Prim 最小生成树Bellman-Ford 最短路拓扑排序Floyd 多源最短路强连通分量2-SAT最大流二分图匹配LCA 倍增欧拉路径
动态规划编辑距离0-1 背包完全背包最长公共子序列最长递增子序列硬币找零方案数石子合并旅行商 TSP树形 DP数位 DP换根 DP
回溯与搜索N 皇后子集生成全排列组合总和迷宫寻路岛屿数量单词搜索数独A* 寻路
字符串KMP 字符串匹配Rabin-KarpBoyer-MooreManacher后缀数组LCP / height 数组AC 自动机Z 函数
数学与数论埃氏筛线性筛欧几里得算法快速幂扩展欧几里得中国剩余定理欧拉函数米勒-拉宾FFTPollard's Rho
计算几何凸包旋转卡壳最近点对线段相交扫描线求交
查找二分查找二分边界旋转数组搜索二分答案三分查找

拓扑排序

图算法 · 有向无环图(DAG)的依赖排序

什么是拓扑排序

有些事情有先后依赖:先修完「高等数学」才能上「概率论」,先编译完 A 库才能链接 B。把这些「谁必须在谁之前」画成一张有向图(边 u→v 表示「u 必须在 v 前」),拓扑排序就是给出一个满足所有依赖的线性顺序——排在前面的绝不依赖后面的。前提是图无环(有环就互相依赖、无解)。

Kahn 算法怎么做

一个直观办法(Kahn 算法):先数每个点的入度(有多少条边指向它)。入度为 0 的点没有任何前置依赖,可以直接输出;输出它之后,把它「删掉」——它指向的每个后继入度减 1。不断重复「取一个入度 0 的点输出、后继减度」,直到所有点输出完。

下面固定一张 6 个点的 DAG。点「下一步」逐步看:当前取出的入度 0 点琥珀高亮、输出后变绿,它的出边黄色高亮、后继的入度徽标下降——某个后继降到 0 时又成为下一批可输出的点。走到底得到一个合法拓扑序(本图为 C→A→E→B→D→F)。右侧代码随每一步同步高亮。

A1B2C0D1E0F3

先数每个点的入度(指向它的边数)

1function topoSort(edges: [number, number][], n: number): number[] {
2 const indeg = Array(n).fill(0);
3 for (const [u, v] of edges) indeg[v]++;
4 const order: number[] = [];
5 const done = Array(n).fill(false);
6 while (order.length < n) {
7 let u = -1;
8 for (let i = 0; i < n; i++)
9 if (!done[i] && indeg[i] === 0) { u = i; break; }
10 if (u === -1) break;
11 done[u] = true;
12 order.push(u);
13 for (const [a, b] of edges) if (a === u) indeg[b]--;
14 }
15 return order;
16}
当前点-
已输出-
剩余6
入度[A]1
入度[B]2
入度[C]0
入度[D]1
入度[E]0
入度[F]3
1 / 14

注意:入度 0 的点可能同时有多个,取哪个都对——拓扑序通常不唯一(这里固定「取下标最小的」让过程确定)。复杂度 O(V + E):每个点、每条边各处理一次。若某步找不到入度 0 的点但还有点没输出,就说明图里有环,不存在拓扑序。

拓扑排序在哪里用

课程/先修:安排一个不违反先修关系的修课顺序。
构建/编译:按依赖决定模块编译、任务执行顺序(Make、打包器都用它)。
电子表格:单元格公式依赖的重算顺序。也用于检测循环依赖(有环即报错)。

Kahn 用「入度 + 队列」实现——想复习队列怎么进出,可回看 数据结构 · 队列;想看图怎么存、怎么遍历,回看 数据结构 · 图。