🥳每日一练-逆拓扑排序-JS简易版

前言

逆拓扑排序(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 求解逆拓扑排序的过程, 讲完了,是不是很简单😄

有两个需要注意的地方:

  1. dsf 方法求解不需要预先统计节点的出度情况
  2. 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);

以上代码,可以直接 copy 到本地运行

总结

这篇文章介绍了逆拓扑排序算法的 JS 代码实现。代码清晰,注释详细,是一篇不可多的的好文章啊

下篇文章将会分享如何求一个图的关键路径,不慌,这个也很简单。

喜欢就关注一下吧❤️,你的点赞和关注是我不断分享的动力

相关推荐
无问8171 小时前
数据结构-排序(冒泡,选择,插入,希尔,快排,归并,堆排)
java·数据结构·排序算法
LJ小番茄1 小时前
Vue 常见的几种通信方式(总结)
前端·javascript·vue.js·html
Lenyiin2 小时前
《 C++ 修炼全景指南:十 》自平衡的艺术:深入了解 AVL 树的核心原理与实现
数据结构·c++·stl
程序猿进阶2 小时前
如何在 Visual Studio Code 中反编译具有正确行号的 Java 类?
java·ide·vscode·算法·面试·职场和发展·架构
Eloudy2 小时前
一个编写最快,运行很慢的 cuda gemm kernel, 占位 kernel
算法
slandarer2 小时前
MATLAB | R2024b更新了哪些好玩的东西?
java·数据结构·matlab
king_machine design2 小时前
matlab中如何进行强制类型转换
数据结构·算法·matlab
西北大程序猿3 小时前
C++ (进阶) ─── 多态
算法
无名之逆3 小时前
云原生(Cloud Native)
开发语言·c++·算法·云原生·面试·职场和发展·大学期末
头发尚存的猿小二3 小时前
树——数据结构
数据结构·算法