前言
逆拓扑排序(Reverse Topological Sort)是指对一个有向无环图(DAG)进行的一种拓扑排序,即将该图的所有顶点按照从后往前的顺序进行排序。在计算机科学中,逆拓扑排序通常用于编译器或解释器等软件的依赖关系处理,以便按照正确的顺序处理源代码文件。
逆拓扑排序的基本思想是:从图中所有出度为0的节点开始,依次将它们放入输出序列中,然后将这些节点从图中删除,并更新它们相邻节点的出度。重复这个过程,直到所有节点都被放入输出序列中。
如果在这个过程中发现某个节点无法放入输出序列中,说明存在循环依赖,逆拓扑排序失败。
逆拓扑排序的实现方法通常使用邻接矩阵表示图,并使用栈进行迭代。
因为要统计一个节点的入度,邻接表的复杂度会很高:O(V+E),近似 O(n^2);而邻接矩阵只需要 O(V), 近似 O(n)
简单来说,逆拓扑排序就是拓扑排序的逆序,所以过程很相似,规则也相似。下面看看代码的实现吧
上篇文章分享了如何计算有向图的逆拓扑排序,感兴趣快去看看吧:🥳每日一练-拓扑排序-JS简易版 - 掘金
准备数据
这个图来自王道考研的数据结构
视频课程中的截图。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相邻
};
/** 逆拓扑排序 */
// 生成邻接矩阵
const generateGraph = (graphNodes, graphEdges) => {
graphNodes = graphNodes.map((item, index) => Array(nodeCount).fill(-1)); // 初始化邻接矩阵
Object.entries(graphEdges).map(([node, edges]) => { // 遍历图的边
edges.map((edge) => { // 遍历当前节点的相邻节点
graphNodes[node][edge] = 1; // 将邻接矩阵的对应元素设为1
});
});
return graphNodes; // 返回邻接矩阵
};
const graph = generateGraph(graphNodes, graphEdges); // 生成邻接矩阵
逆拓扑排序
javascript
const getDegree = (graph) => {
// 初始化度数数组
const degree = Array(nodeCount).fill(0);
// 遍历邻接矩阵,计算每个节点的出度
for (let from = 0; from < nodeCount; from++) {
for (let to = 0; to < nodeCount; to++) {
if (graph[from][to] != -1) degree[from]++;
}
}
return degree;
};
逆拓扑排序,最重要的就是统计节点的出度了。有向图的存储结构是邻接矩阵,统计的过程就是遍历一个二维数组了,非常简单。
javascript
const topoLogicSort = (graph) => {
// 初始化栈和排序数组
const stack = [];
const sortArray = [];
// 获取每个节点的度
const degree = getDegree(graph);
// 找出度为0的节点,并将其入栈
for (let i = 0; i < degree.length; i++) {
if (degree[i] == 0) stack.push(i);
}
// 当栈不为空时,进行以下操作
while (stack.length) {
// 弹出栈首节点
const currentNode = stack.pop();
// 将当前节点添加到排序数组中
sortArray.push(currentNode);
// 遍历当前节点的相邻节点
for (let from = 0; from < nodeCount; from++) {
// 如果当前节点与相邻节点之间存在一条边,则更新相邻节点的度
if (graph[from][currentNode] != -1) {
if (--degree[from] == 0) stack.push(from);
}
}
}
// 如果排序数组的长度小于节点数,说明存在循环依赖,拓扑排序失败
if (sortArray.length < nodeCount) {
console.log("there is a loop");
return;
}
return sortArray;
};
先找到出度为 0 的节点入栈,然后进入栈循环。
出栈,删除指向出栈节点的节点的出度。当然不是真的删除 graph 的数据,而是修改 degree 的统计数据即可:--degree[from]
。修改了之后立马判断 from 节点
的出度是否为 0;如果为 0,就入栈。
出栈,重复上面的操作,直到栈空。退出栈循环后,还要判断是否有环路的存在,有环路的有向图是找不出拓扑排序的。所以就直接返回了。
DSF 实现逆拓扑排序
其实深度优先遍历也可以求逆拓扑排序。一上面这个图为例。想想深度优先遍历的过程,当遍历节点 4 时,节点 4 没有出度,遍历就需要回退了。这个时候就可以把节点 4 放入输出序列了。回退到 3 后,3 没有了其他的出度,也需要回退。这个时候就可以把节点 3 放入输出序列了.
依次类推,输出序列中是不是就是逆拓扑排序了😄
代码实现:
javascript
const sortArray2 = []; // 定义一个空数组,用于存储逆拓扑排序结果
const isVisited = Array(nodeCount).fill(false); // 定义一个标记节点是否被访问过的数组
const isVisitedNow = Array(nodeCount).fill(false); // 定义一个标记当前正在访问的节点数组
const topoLogicSort2 = (graph) => { // 定义一个逆拓扑排序函数
try { // 尝试执行以下代码
for (let i = 0; i < nodeCount; i++) { // 遍历所有节点
if (isVisited[i] == false) { // 如果节点没有被访问过
dsf(graph, i); // 执行深度优先搜索
}
}
} catch (error) { // 如果出现错误
console.log(error); // 打印错误信息
}
};
const dsf = (graph, currentNode) => { // 定义一个深度优先搜索函数
isVisited[currentNode] = true; // 将当前节点标记为已访问
isVisitedNow[currentNode] = true; // 将当前节点标记为正在访问
for (let node = graph[currentNode].next; node; node = node.next) { // 遍历当前节点的相邻节点
if (isVisitedNow[node.value] == true) { // 如果相邻节点已经被标记为正在访问
throw "there is a loop"; // 表示存在循环依赖,无法进行拓扑排序
}
if (isVisited[node.value] == false) { // 如果相邻节点没有被访问过
dsf(graph, node.value); // 对相邻节点进行深度优先搜索
}
}
sortArray2.push(currentNode); // 将当前节点添加到逆拓扑排序结果中
isVisitedNow[currentNode] = false; // 将当前节点标记为未访问
};
topoLogicSort2(graph, 0); // 对节点0进行逆拓扑排序,并输出结果
console.log(sortArray2); // 打印逆拓扑排序结果
// [ 4, 3, 1, 0, 2 ]
dsf 是一个深度优先遍历的代码。相信大家肯定很熟悉,不熟悉的话可以看这篇文章:
此处深度优先遍历的的 graph 是邻接表,和上文的邻接矩阵不相同,⚠️注意区分
邻接表的结构长这样:
其中sortArray2.push(currentNode);
的操作是放在了 for 循环的后面,for 循环结束意味着所有的出度边的节点都被访问了,到了回退当前节点的时候了。当然,这也是当前节点放到输出序列的时候。
你可以在脑中做个思维训练:假设现在 currentNode 是 4,然后 4 被放入了输出序列中,退出 dsf 方法,回到了上一个 dsf 方法中。
此时 currentNode 变成了 3,并且还在 for 循环中,因为节点 3 的邻接节点只有节点 4,所以 for 循环立马结束了,然后就轮到 3 放入输出序列了。然后又回到了上一个 dsf 中。
此时 currentNode 变成了 1...剩下的,就靠大家根据代码自己脑补了哈哈
这就是 dsf 求解逆拓扑排序的过程, 讲完了,是不是很简单😄
有两个需要注意的地方:
- dsf 方法求解不需要预先统计节点的出度情况
- dsf 需要特殊的方法来判断当前有向图是否存在环路。代码中使用的是
isVisitedNow
,和isVisited
不同的是,isVisited
存放着已经访问过的节点,而isVisitedNow
存在着正在访问的节点,如果遍历的过程中,访问到了正在访问的节点,说明就有环了。代码就会执行throw "there is a loop";
在退出当前节点的 dsf 方法时候,需要isVisitedNow[currentNode] = false;
表示当前节点访问完毕
完整代码
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],
};
/** 逆拓扑排序 */
// 邻接矩阵
const generateGraph = (graphNodes, graphEdges) => {
graphNodes = graphNodes.map((item, index) => Array(nodeCount).fill(-1));
Object.entries(graphEdges).map(([node, edges]) => {
edges.map((edge) => {
graphNodes[node][edge] = 1;
});
});
return graphNodes;
};
const graph = generateGraph(graphNodes, graphEdges);
const getDegree = (graph) => {
const degree = Array(nodeCount).fill(0);
for (let from = 0; from < nodeCount; from++) {
for (let to = 0; to < nodeCount; to++) {
if (graph[from][to] != -1) degree[from]++;
}
}
return degree;
};
const topoLogicSort = (graph) => {
const stack = [];
const sortArray = [];
// find the node outDegree is 0
const degree = getDegree(graph);
for (let i = 0; i < degree.length; i++) {
if (degree[i] == 0) stack.push(i);
}
while (stack.length) {
const currentNode = stack.pop();
sortArray.push(currentNode);
for (let from = 0; from < nodeCount; from++) {
if (graph[from][currentNode] != -1) {
if (--degree[from] == 0) stack.push(from);
}
}
}
return sortArray;
};
const sortArray = topoLogicSort(graph);
console.log(sortArray);
dsf 排序
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 sortArray2 = [];
const isVisited = Array(nodeCount).fill(false);
const isVisitedNow = Array(nodeCount).fill(false);
const topoLogicSort2 = (graph) => {
try {
for (let i = 2; i < nodeCount; i++) {
if (isVisited[i] == false) {
dsf(graph, i);
}
}
} catch (error) {
console.log(error);
}
};
const dsf = (graph, currentNode) => {
isVisited[currentNode] = true;
isVisitedNow[currentNode] = true;
for (let node = graph[currentNode].next; node; node = node.next) {
if (isVisitedNow[node.value] == true) {
throw "there is a loop";
}
if (isVisited[node.value] == false) {
dsf(graph, node.value);
}
}
sortArray2.push(currentNode);
isVisitedNow[currentNode] = false;
};
topoLogicSort2(graph);
console.log(sortArray2);
总结
这篇文章介绍了逆拓扑排序算法的 JS 代码实现。代码清晰,注释详细,是一篇不可多的的好文章啊
下篇文章将会分享如何求一个图的关键路径,不慌,这个也很简单。
喜欢就关注一下吧❤️,你的点赞和关注是我不断分享的动力