什么是拓扑排序
拓扑排序是一种用于有向无环图(Directed Acyclic Graphs)的排序方法,其目的是将图中的所有顶点排成一个线性序列,使得对于每一条有向边(u, v),顶点 u 都出现在顶点 v 之前。拓扑排序的结果并不唯一,因为对于同一个图,可能存在多个合法的拓扑排序序列。通常在任务调度、依赖处理、课程安排等场景中应用
基本概念
AOV 网
AOV(Activity Vertex Network),即顶点活动关系网,在一个工程中,有些任务需要先决条件,比如学 Vue 之前得学会 JS,有些任务则没有先决条件,可以在任意时间进行开始。在 DAG 中,若以顶点表示任务,有向边(弧)表示任务之间的先后关系,则这样的图可以称为 AOV 网。 AOV 网就是一种可以形象地反映出整个工程中各个任务之间的先后关系的有向图
拓扑序列
在 AOV 网中,如果不存在回路,则所有任务都能排列成为一个线性队列,使得每一个任务的所有前驱任务都排列在该任务的前面,就可以把此序列叫做拓扑序列 假设 G=(V, E)是一个具有 n 个顶点的有向图,V 中的顶点序列 V1、V2、V3……Vn 满足若从顶点 Vi 到 Vj 有一条路径,则在顶点序列中 Vi 必在 Vj 的前面,则我们称这样的顶点序列为一个拓扑序列
拓扑排序
拓扑排序就是将一个有向图构造成拓扑序列的过程。 并非所有的有向图都能成功构造出拓扑序列,最终生成的顶点序列有两种情况:
- 如果此网中的全部顶点都被输出,则说明该网的不存在环(回路)的 AOV 网
- 如果此网中的节点没有被完全输出,则说明该网是存在环的
拓扑排序实现
#include <stdio.h>
#include "stdlib.h"
#include <stdbool.h>
typedef struct EdgeNode {
int vertexIndex; // 顶点下标
int weight; // 权重
struct EdgeNode *next; // 指针
} EdgeNode;
typedef struct Vertex{
int in; // 入度
int data;// 顶点值
struct EdgeNode *firstEdge; //指向边表链接
} Vertex;
typedef struct Graph{
struct Vertex *vertexes;// 邻接表
int countOfVertexes;// 顶点数
int countOfEdges;// 边数
} Graph;
void createGraph(Graph *graph){
graph->countOfVertexes = 14;
graph->countOfEdges = 20;
graph->vertexes = malloc(sizeof(Vertex) * graph->countOfVertexes);
for(int i = 0; i< graph->countOfVertexes;i++){
graph->vertexes[i].data = i;
graph->vertexes[i].in = 0;
graph->vertexes[i].firstEdge = NULL;
}
// 初始化边表信息
int edgeInfo[20][3] = {
{0,4,1},
{0,5,1},
{0,11,1},
{1,2,1},
{1,4,1},
{1,8,1},
{2,5,1},
{2,6,1},
{2,9,1},
{3,2,1},
{3,13,1},
{4,7,1},
{5,8,1},
{5,12,1},
{6,5,1},
{8,7,1},
{9,10,1},
{9,11,1},
{10,13,1},
{12,9,1}
};
int start,end;
for(int i = 0;i<graph->countOfEdges;i++){
EdgeNode *edgeNode = malloc(sizeof(EdgeNode));
start = edgeInfo[i][0];
end = edgeInfo[i][1];
edgeNode->weight = edgeInfo[i][2];
edgeNode->vertexIndex = end;
// 将新的边表节点插入到边表链的首元结点的位置
edgeNode->next = graph->vertexes[start].firstEdge;
graph->vertexes[start].firstEdge = edgeNode;
// 弧头顶点的入度加1
graph->vertexes[end].in++;
}
}
void topoSort(Graph graph) {
/*
1,新建一个顺序栈stack,栈中存储所有的未处理过的入度为0的顶点;
新建一个变量count,用于记录已经处理过的入度为0的顶点的个数
*/
int stack[graph.countOfVertexes];
int stackTop = -1; // 栈顶指针
int count = 0;
/*
2,将所有的入度为0的顶点入栈
*/
for (int i = 0; i < graph.countOfVertexes; i++) {
if (graph.vertexes[i].in == 0) {
stack[++stackTop] = i;
}
}
/*
3,遍历栈中顶点元素:
(1)处理打印当前的栈顶元素,并出栈
(2)count变量(已经处理过的入度为0的顶点个数)加1
(3)遍历以当前顶点为弧尾的所有的边:
① 获取到弧头,并将弧头入度减1
② 将减1后的入度为0的弧头顶点入栈
*/
while (stackTop >= 0) {
// 处理并出栈
Vertex currentVertex = graph.vertexes[stack[stackTop--]];
printf("%d ", currentVertex.data);
// 数量+1
count++;
// 处理各个弧头顶点
for (struct EdgeNode *edgeNode = currentVertex.firstEdge; edgeNode; edgeNode = edgeNode->next) {
// 弧头顶点入度减1,当入度为0的时候入栈
if (--graph.vertexes[edgeNode->vertexIndex].in == 0) {
stack[++stackTop] = edgeNode->vertexIndex;
}
}
}
// 4,判断拓扑排序是否成功
if (count == graph.countOfVertexes) {
printf("\n拓扑排序成功!\n");
} else {
printf("\n拓扑排序失败!\n");
}
}
int main(int argc, const char * argv[]) {
Graph graph;
createGraph(&graph);
topoSort(graph);
return 0;
}
拓扑排序的应用: 求模块依赖关系
const dependencies = {
moduleA:["moduleB","moduleC"],
moduleB:["moduleC"],
moduleC:[],
moduleD:["moduleA","moduleB"],
}
graph LR
A-->D
B-->A
B-->D
C-->A
C-->B
边表信息结构为(0:A,1:B,2:C,3:D):
[
{1,0,1},
{2,0,1},
{2,1,1},
{0,3,1},
{1,3,1},
]
更改topoSort函数中的代码为:
graph->countOfVertexes = 4;
graph->countOfEdges = 5;
int edgeInfo[5][3] = {
{1,0,1},
{2,0,1},
{2,1,1},
{0,3,1},
{1,3,1},
};
JS实现
依赖必须无环
实现1
- 使用dfs寻找最里层无依赖的模块,加入res中,并将其标记。如果所有子项都已经标记过,则直接将父项放入res中。
const dependencies = {
moduleA: ["moduleB", "moduleC"],
moduleB: ["moduleC"],
moduleC: [],
moduleD: ["moduleA", "moduleB"],
}
function getLoadOrder (dependencies) {
const visited = new Set();
const res = [];
function dfs (module) {
if (visited.has(module)) return;
for (let dependency of dependencies[module]) {
dfs(dependency);
}
res.push(module);
visited.add(module);
}
Object.keys(dependencies).forEach((key) => dfs(key));
return res;
}
console.log(getLoadOrder(dependencies)); // Output: ['moduleC', 'moduleB', 'moduleA', 'moduleD']
实现2
- 使用了深度遍历+去重
const dependencies = {
moduleA: ["moduleB", "moduleC"],
moduleB: ["moduleC"],
moduleC: [],
moduleD: ["moduleA", "moduleB"],
}
function fn(depMap) {
const result = []; // 用于存储最终的模块顺序
const _fn = (mKey) => {
const temp = [mKey]; // 临时栈,用于存储当前模块及其依赖
while (temp.length > 0) {
const chunk = temp.pop(); // 从栈中取出一个模块
result.unshift(chunk); // 将模块添加到结果数组的开头
// 如果当前模块有依赖项,并且依赖项数组不为空
if (depMap[chunk] && depMap[chunk].length > 0) {
temp.push(...depMap[chunk]); // 将依赖项添加到栈中
}
}
};
// 遍历所有模块,调用 _fn 函数解析依赖
for (const moduleKey in depMap) {
_fn(moduleKey);
}
// 去重并返回结果
console.log("result", result);
return result.filter((item, index, self) => self.indexOf(item) === index);
}
实现3
- 使用拓扑排序
const dependencies = {
moduleA: ["moduleB", "moduleC"],
moduleB: ["moduleC"],
moduleC: [],
moduleD: ["moduleA", "moduleB"],
}
function topologicalSort (dependencies) {
const inDegree = {}; // 存储每个模块的入度
const graph = {}; // 存储图的邻接表
const result = []; // 存储最终的拓扑排序结果
// 初始化入度和图
for (const module in dependencies) {
inDegree[module] = 0;
graph[module] = dependencies[module];
}
// 计算每个模块的入度
for (const module in dependencies) {
for (const dep of dependencies[module]) {
inDegree[dep] = (inDegree[dep] || 0) + 1;
}
}
// 将所有入度为 0 的模块加入队列
const queue = [];
for (const module in inDegree) {
if (inDegree[module] === 0) {
queue.push(module);
}
}
console.log(graph, inDegree);
// 拓扑排序
while (queue.length > 0) {
const current = queue.shift();
result.unshift(current);
for (const dep of graph[current]) {
inDegree[dep] -= 1;
if (inDegree[dep] === 0) {
queue.push(dep);
}
}
}
// 检查是否存在环依赖
if (result.length !== Object.keys(dependencies).length) {
throw new Error("存在环依赖,无法进行拓扑排序");
}
return result;
}
console.log(topologicalSort(dependencies));
参考
百度 AI 数据结构与算法(十五)——图的拓扑排序和关键路径