【JavaScript 算法】拓扑排序:有向无环图的应用


🔥 个人主页:空白诗

文章目录

拓扑排序(Topological Sorting)是一种线性排序方法,适用于有向无环图(DAG, Directed Acyclic Graph),它能够为图中的节点安排一个线性序列,使得对于图中的每一条有向边(u, v),顶点u在序列中出现在顶点v之前。拓扑排序在许多实际应用中都有重要作用,如任务调度、课程安排、编译依赖等。本文将详细介绍拓扑排序的原理、实现及其应用。


一、算法原理

拓扑排序的基本思想是:

  1. 选择一个入度为0的节点,将其输出到排序结果,并从图中删除该节点及其关联的所有边。
  2. 重复步骤1,直到所有节点都被输出,或者图中仍存在入度不为0的节点(此时图中存在环,无法进行拓扑排序)。

常用的两种实现拓扑排序的方法是Kahn算法和深度优先搜索(DFS)。


二、算法实现

方法一:Kahn算法

Kahn算法利用队列实现拓扑排序,通过不断删除入度为0的节点来构建拓扑序列。

javascript 复制代码
/**
 * Kahn算法实现拓扑排序
 * @param {Object} graph - 图的邻接表表示
 * @return {string[]} - 拓扑排序结果
 */
function kahnTopologicalSort(graph) {
  const inDegree = {}; // 记录每个节点的入度
  const queue = []; // 存储入度为0的节点
  const result = []; // 存储拓扑排序结果

  // 初始化入度表
  for (const node in graph) {
    inDegree[node] = 0;
  }

  // 计算每个节点的入度
  for (const node in graph) {
    for (const neighbor of graph[node]) {
      inDegree[neighbor]++;
    }
  }

  // 将入度为0的节点加入队列
  for (const node in inDegree) {
    if (inDegree[node] === 0) {
      queue.push(node);
    }
  }

  // 处理队列中的节点
  while (queue.length > 0) {
    const node = queue.shift(); // 取出队首节点
    result.push(node); // 将节点加入拓扑排序结果

    // 减少相邻节点的入度
    for (const neighbor of graph[node]) {
      inDegree[neighbor]--;
      // 如果相邻节点的入度为0,加入队列
      if (inDegree[neighbor] === 0) {
        queue.push(neighbor);
      }
    }
  }

  // 检查是否存在环
  if (result.length !== Object.keys(graph).length) {
    throw new Error("图中存在环,无法进行拓扑排序");
  }

  return result;
}

// 示例
const graph = {
  A: ['C'],
  B: ['C', 'D'],
  C: ['E'],
  D: ['F'],
  E: ['H', 'F'],
  F: ['G'],
  G: [],
  H: []
};

console.log(kahnTopologicalSort(graph)); // 输出: [ 'A', 'B', 'D', 'C', 'E', 'F', 'H', 'G' ]

方法二:深度优先搜索(DFS)

DFS方法通过递归遍历图,将访问过的节点存入栈中,最终从栈顶依次取出节点构建拓扑序列。

javascript 复制代码
/**
 * 深度优先搜索实现拓扑排序
 * @param {Object} graph - 图的邻接表表示
 * @return {string[]} - 拓扑排序结果
 */
function dfsTopologicalSort(graph) {
  const visited = new Set(); // 记录已访问的节点
  const stack = []; // 存储拓扑排序结果

  /**
   * 递归函数:DFS遍历节点
   * @param {string} node - 当前节点
   */
  function dfs(node) {
    if (visited.has(node)) return;
    visited.add(node); // 标记节点为已访问

    for (const neighbor of graph[node]) {
      dfs(neighbor); // 递归访问相邻节点
    }

    stack.push(node); // 当前节点处理完毕,加入栈中
  }

  // 遍历所有节点,进行DFS
  for (const node in graph) {
    dfs(node);
  }

  return stack.reverse(); // 返回栈的逆序,即拓扑排序结果
}

// 示例
console.log(dfsTopologicalSort(graph)); // 输出: [ 'B', 'D', 'A', 'C', 'E', 'H', 'F', 'G' ]

注释说明:

  1. Kahn算法:

    • inDegree:记录每个节点的入度。
    • queue:存储入度为0的节点。
    • result:存储拓扑排序结果。
    • 初始化入度表,并计算每个节点的入度。
    • 将入度为0的节点加入队列,处理队列中的节点,更新相邻节点的入度。
    • 最终检查是否存在环,返回拓扑排序结果。
  2. DFS方法:

    • visited:记录已访问的节点。
    • stack:存储拓扑排序结果。
    • 递归遍历节点,将访问过的节点存入栈中,最终返回栈的逆序。

三、应用场景

  1. 任务调度:根据任务之间的依赖关系,确定任务的执行顺序。
  2. 课程安排:根据课程的先修关系,确定课程的学习顺序。
  3. 编译依赖:根据文件的依赖关系,确定编译的顺序。
  4. 数据处理:根据数据的依赖关系,确定处理的顺序。

四、总结

拓扑排序是一种用于有向无环图(DAG)的线性排序方法,通过Kahn算法和DFS方法可以实现拓扑排序,广泛应用于任务调度、课程安排、编译依赖和数据处理等场景。理解和掌握拓扑排序算法,对于解决实际问题具有重要意义。


相关推荐
夏幻灵1 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
程序猿编码1 小时前
榨干 RTX5090 算力!Qwen3 专用单卡推理引擎,手写C++/CUDA 算子实现 MTP 推测解码
开发语言·c++·大模型推理·qwen3·多模态推理
三天不学习2 小时前
Egg.js 4 突然爆火,原因是否归结于AI 原生落地需求爆发?
前端·javascript·全栈·egg.js
xcs194052 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js
bkspiderx3 小时前
Qt 插件机制:动态扩展应用功能的核心框架
开发语言·qt·元数据·qt 插件·qpluginloader
程序员老陆3 小时前
深入理解 C++ thread_local:线程私有存储的正确打开方式
开发语言·c++·程序设计
沐晓时光3 小时前
C语言入门,深入理解指针(3)
c语言·开发语言
信奥卷王3 小时前
[GESP202609 六级] 分树规划
算法
夏幻灵3 小时前
LeetCode 21:合并两个有序链表——双指针与哑节点详解
算法·leetcode·链表
码云数智-园园4 小时前
unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析
java·开发语言