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

拓扑排序经典的用处就是寻找一个图的关键路径。这篇就来看看怎么算出一个图的关键路径吧

什么是拓扑排序

拓扑排序(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 ]

这是完整代码,可以直接 copy 到本地运行

总结

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

下篇文章将会分析如何求一个有向图的逆拓扑排序。不慌,这个也很简单。

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

相关推荐
此生只爱蛋10 分钟前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
blammmp18 分钟前
Java:数据结构-枚举
java·开发语言·数据结构
昂子的博客39 分钟前
基础数据结构——队列(链表实现)
数据结构
咕咕吖42 分钟前
对称二叉树(力扣101)
算法·leetcode·职场和发展
王哲晓43 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v1 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云1 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
九圣残炎1 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
lulu_gh_yu1 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
丫头,冲鸭!!!2 小时前
B树(B-Tree)和B+树(B+ Tree)
笔记·算法