拓扑排序经典的用处就是寻找一个图的关键路径。这篇就来看看怎么算出一个图的关键路径吧
什么是拓扑排序
拓扑排序(Topological Sorting)是一种常用的图论算法,用于找到一个线性序列,使得该序列中每个元素都排在它所有依赖的前面。在计算机科学中,拓扑排序通常用于分析程序的依赖关系,以便按照正确的顺序编译、链接和运行程序。
拓扑排序的基本思想是:首先,找到所有没有入度的节点,并将这些节点放入输出序列中。然后,将这些节点从图中删除,并更新它们相邻节点的入度。然后接着找入度为零的节点。重复这个过程,直到所有节点都被放入输出序列中。
如果在这个过程中发现某个节点无法放入输出序列中,说明存在环路,拓扑排序失败。
拓扑排序的实现方法通常使用邻接表表示图,并使用栈进行迭代
过程很简单,就是不断找入度为 0 的节点,下面看看如何实现吧
准备数据
这个图来自王道考研的数据结构
视频课程中的截图。B 站可搜王道考研数据结构
下面将会用代码生成表示上图的邻接矩阵:
javascript
const nodeCount = 5; // 定义节点数量
/**@type {number[]} */
let graphNodes = Array(nodeCount) // 创建一个节点数组
.fill(0) // 用0填充数组
.map((item, index) => index); // 将数组转换为表示节点索引的数组
/**@type {number[][]} */
const graphEdges = { // 定义一个表示图的边对象
0: [1], // 节点0与节点1相邻
1: [3], // 节点1与节点3相邻
2: [3, 4], // 节点2与节点3和节点4相邻
3: [4], // 节点3与节点4相邻
};
/**
* 生成一个有向图
*
* @param {number[]} graphNodes - 图的节点数组
* @param {number[][]} graphEdges - 图的边对象
* @returns {nodeType[]} - 生成的图节点数组
*/
const generateGraph = (graphNodes, graphEdges) => {
graphNodes = graphNodes.map((item, index) => { // 将节点数组转换为图节点对象数组
const tempNode = { value: index, next: null }; // 创建一个图节点对象
return tempNode; // 返回图节点对象
});
Object.entries(graphEdges).map(([value, edges]) => {
graphNodes[value].next = edges.reduce((res, toNode) => { // 将相邻节点的图节点对象连接起来
let tempRes = res;
while (tempRes.next) tempRes = tempRes.next; // 遍历链表,直到找到最后一个节点
tempRes.next = { value: toNode, next: null }; // 添加新的相邻节点
return res; // 返回链表头节点
}, {}).next; // 返回链表头节点的下一个节点
});
return graphNodes; // 返回生成的图节点数组
};
const graph = generateGraph(graphNodes, graphEdges); // 生成有向图
拓扑排序
javascript
const getDegree = (graph) => { // 定义一个计算节点度的函数
const degree = Array(nodeCount).fill(0); // 创建一个节点度数组
for (let i = 0; i < graph.length; i++) { // 遍历图节点数组
let node = graph[i].next; // 获取第一个相邻节点
while (node) { // 遍历相邻节点链表
degree[node.value]++; // 增加相邻节点的度
node = node.next; // 获取下一个相邻节点
}
}
return degree; // 返回节点度数组
};
在拓扑排序之前,首先是要找到入度为 0 的节点,getDegree
函数就是一个统计图中各个顶点入度数量的方法。
javascript
console.log(getDegree(graph));
//[ 0, 1, 0, 2, 2 ]
与上图对比,可知输出结果正确。图中有两个节点入度为 0,分别是节点 0
,节点 2
. 拓扑排序将会从这两个节点中一个开始
javascript
const topoLogicSort = (graph) => {
// 创建一个空数组,用于存储排序后的节点
const sortArray = [];
// 创建一个空栈,用于存储度为0的节点
const stack = [];
// 获取每个节点的度
const degree = getDegree(graph);
// 将度为0的节点入栈
for (let i = 0; i < degree.length; i++) {
if (degree[i] == 0) stack.push(i);
}
// 当栈不为空时,进行以下操作
while (stack.length !== 0) {
// 弹出栈顶节点
const currentNode = stack.pop();
// 将当前节点添加到排序数组中
sortArray.push(currentNode);
// 遍历当前节点的相邻节点
for (let node = graph[currentNode].next; node; node = node.next) {
// 相邻节点的度减1
if (--degree[node.value] == 0) stack.push(node.value);
}
}
// 如果排序数组的长度小于节点总数,说明存在循环依赖,拓扑排序失败
if (sortArray.length < nodeCount) {
console.log("there is a loop");
return;
}
// 返回排序后的节点数组
return sortArray;
};
首先找出图中入度为 0 的节点,然后从该节点开始,删除这个节点出度边的对应节点的入度:--degree[node.value]
,对应节点减一后,就立马看看这个节点的入度是否为 0,如果为 0,那就入栈吧。
之后再从栈里取出一个节点,重复上面的操作,直到栈为空
退出栈循环后,还要判断是否有环路的存在,有环路的有向图是找不出拓扑排序的。所以就直接返回了。
简单吧😄
完整代码:
javascript
const nodeCount = 5;
/**@type {number[]} */
let graphNodes = Array(nodeCount)
.fill(0)
.map((item, index) => index);
/**@type {number[][]} */
const graphEdges = {
0: [1],
1: [3],
2: [3, 4],
3: [4],
};
/**
*
* @param {number[]} graphNodes
* @param {number[][]} graphEdges
* @returns {nodeType[]}
*/
const generateGraph = (graphNodes, graphEdges) => {
graphNodes = graphNodes.map((item, index) => {
const tempNode = { value: index, next: null };
return tempNode;
});
Object.entries(graphEdges).map(([value, edges]) => {
graphNodes[value].next = edges.reduce((res, toNode) => {
let tempRes = res;
while (tempRes.next) tempRes = tempRes.next;
tempRes.next = { value: toNode - 0, next: null };
return res;
}, {}).next;
});
return graphNodes;
};
const graph = generateGraph(graphNodes, graphEdges);
const getDegree = (graph) => {
const degree = Array(nodeCount).fill(0);
for (let i = 0; i < graph.length; i++) {
let node = graph[i].next;
while (node) {
degree[node.value]++;
node = node.next;
}
}
return degree;
};
const topoLogicSort = (graph) => {
const sortArray = [];
const stack = [];
const degree = getDegree(graph);
for (let i = 0; i < degree.length; i++) {
if (degree[i] == 0) stack.push(i);
}
while (stack.length !== 0) {
const currentNode = stack.pop();
sortArray.push(currentNode);
for (let node = graph[currentNode].next; node; node = node.next) {
if (--degree[node.value] == 0) stack.push(node.value);
}
}
if (sortArray.length < nodeCount) {
console.log("there is a loop");
return;
}
return sortArray;
};
const sortArray = topoLogicSort(graph);
console.log(sortArray);
// [ 2, 0, 1, 3, 4 ]
总结
这篇文章介绍了拓扑排序算法的 JS 代码实现。代码清晰,注释详细,是一篇不可多的的好文章啊
下篇文章将会分析如何求一个有向图的逆拓扑排序。不慌,这个也很简单。
喜欢就关注一下吧❤️,你的点赞和关注是我不断分享的动力