拓扑排序(Topological Sorting)是一种线性排序方法,适用于有向无环图(DAG, Directed Acyclic Graph),它能够为图中的节点安排一个线性序列,使得对于图中的每一条有向边
(u, v)
,顶点u
在序列中出现在顶点v
之前。拓扑排序在许多实际应用中都有重要作用,如任务调度、课程安排、编译依赖等。本文将详细介绍拓扑排序的原理、实现及其应用。
一、算法原理
拓扑排序的基本思想是:
- 选择一个入度为0的节点,将其输出到排序结果,并从图中删除该节点及其关联的所有边。
- 重复步骤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' ]
注释说明:
- Kahn算法:
inDegree
:记录每个节点的入度。queue
:存储入度为0的节点。result
:存储拓扑排序结果。- 初始化入度表,并计算每个节点的入度。
- 将入度为0的节点加入队列,处理队列中的节点,更新相邻节点的入度。
- 最终检查是否存在环,返回拓扑排序结果。
- DFS方法:
visited
:记录已访问的节点。stack
:存储拓扑排序结果。- 递归遍历节点,将访问过的节点存入栈中,最终返回栈的逆序。
三、应用场景
- 任务调度:根据任务之间的依赖关系,确定任务的执行顺序。
- 课程安排:根据课程的先修关系,确定课程的学习顺序。
- 编译依赖:根据文件的依赖关系,确定编译的顺序。
- 数据处理:根据数据的依赖关系,确定处理的顺序。
四、总结
拓扑排序是一种用于有向无环图(DAG)的线性排序方法,通过Kahn算法和DFS方法可以实现拓扑排序,广泛应用于任务调度、课程安排、编译依赖和数据处理等场景。理解和掌握拓扑排序算法,对于解决实际问题具有重要意义。