🥳每日一练-拓扑排序-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 代码实现。代码清晰,注释详细,是一篇不可多的的好文章啊

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

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

相关推荐
鸽鸽程序猿3 分钟前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd3 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo6177 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v12 分钟前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A1 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
古希腊掌管学习的神1 小时前
[搜广推]王树森推荐系统——矩阵补充&最近邻查找
python·算法·机器学习·矩阵
云边有个稻草人1 小时前
【优选算法】—复写零(双指针算法)
笔记·算法·双指针算法
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
忘梓.2 小时前
解锁动态规划的奥秘:从零到精通的创新思维解析(3)
算法·动态规划
️南城丶北离2 小时前
[数据结构]图——C++描述
数据结构··最小生成树·最短路径·aov网络·aoe网络