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

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

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

相关推荐
未来之窗软件服务1 小时前
自己写算法(九)网页数字动画函数——东方仙盟化神期
前端·javascript·算法·仙盟创梦ide·东方仙盟·东方仙盟算法
豐儀麟阁贵1 小时前
基本数据类型
java·算法
乐迪信息3 小时前
乐迪信息:基于AI算法的煤矿作业人员安全规范智能监测与预警系统
大数据·人工智能·算法·安全·视觉检测·推荐算法
C+ 安口木3 小时前
vue中监听window某个属性被添加或值的变化
前端·javascript·vue.js
CoderYanger3 小时前
前端基础-HTML入门保姆级课堂笔记
前端·javascript·css·html
赛博切图仔3 小时前
qiankun、micro-app、wujie,2025年我们该选谁?
前端·javascript
hsjkdhs4 小时前
C++之多层继承、多源继承、菱形继承
开发语言·c++·算法
立志成为大牛的小牛4 小时前
数据结构——十七、线索二叉树找前驱与后继(王道408)
数据结构·笔记·学习·程序人生·考研·算法
星空下的曙光4 小时前
Node.js crypto模块所有 API 详解 + 常用 API + 使用场景
算法·node.js·哈希算法
Algo-hx4 小时前
数据结构入门 (七):从“链接”到“分支” —— 初探树与二叉树
数据结构