算法详解:拓扑排序 - 有向无环图的线性序列

引言

在计算机科学和日常生活中,我们经常遇到需要按照特定顺序执行任务的情况。比如:

  • 大学课程的先修关系(必须先学习数学基础才能学习高级算法)
  • 软件项目的编译依赖(模块A依赖模块B,则必须先编译B)
  • 任务调度系统中的任务依赖关系

拓扑排序(Topological Sort)就是解决这类问题的经典算法。它能够为有向无环图(DAG, Directed Acyclic Graph)中的所有顶点给出一个线性排序,使得对于图中的每一条有向边(u, v),顶点u在排序中都出现在顶点v之前。

核心概念

有向无环图(DAG)

拓扑排序只能应用于有向无环图。让我们先理解什么是DAG:

1
2
3
4
5
6
7
有向图示例:
A → B → D
↓   ↓   ↑
C → E → F

如果这个图是无环的,我们可以进行拓扑排序
一个可能的拓扑排序结果:A, C, B, E, F, D

拓扑排序的性质

  1. 唯一性:拓扑排序的结果不一定唯一
  2. 存在性:只有DAG才存在拓扑排序
  3. 线性时间:可以在O(V+E)时间内完成

现实生活中的应用场景

1. 课程安排系统

假设我们有以下课程依赖关系:

1
2
3
4
5
6
7
// 课程依赖关系示例
数据结构  算法设计
数学基础  数据结构
数学基础  算法设计
编程基础  数据结构
编程基础  软件工程
算法设计  高级算法

使用拓扑排序,我们可以得到一个合理的课程学习顺序: 数学基础 → 编程基础 → 数据结构 → 算法设计 → 软件工程 → 高级算法

2. 构建系统

在软件开发中,模块之间存在依赖关系:

1
2
3
项目构建依赖图:
utils.jar → core.jar → service.jar → web.jar
         → dao.jar  → service.jar

拓扑排序确保我们按正确顺序编译各个模块。

3. 任务调度

在项目管理中,任务之间的依赖关系:

1
2
3
项目任务依赖:
需求分析 → 系统设计 → 编码实现 → 单元测试 → 集成测试 → 部署上线
        → UI设计   → 编码实现

算法实现

方法一:基于DFS的拓扑排序

DFS方法的核心思想是:在DFS遍历过程中,当一个顶点的所有邻接顶点都被访问完毕时,将该顶点加入结果栈。最后栈中元素的逆序就是拓扑排序的结果。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
import java.util.*;

public class TopologicalSortDFS {
    private Map<Integer, List<Integer>> graph;
    private Set<Integer> visited;
    private Set<Integer> recursionStack;
    private Stack<Integer> topologicalOrder;

    public TopologicalSortDFS(int vertices) {
        this.graph = new HashMap<>();
        this.visited = new HashSet<>();
        this.recursionStack = new HashSet<>();
        this.topologicalOrder = new Stack<>();

        // 初始化图
        for (int i = 0; i < vertices; i++) {
            graph.put(i, new ArrayList<>());
        }
    }

    /**
     * 添加有向边
     * @param from 起始顶点
     * @param to 终止顶点
     */
    public void addEdge(int from, int to) {
        graph.get(from).add(to);
    }

    /**
     * 执行拓扑排序
     * @return 拓扑排序结果,如果存在环则返回null
     */
    public List<Integer> topologicalSort() {
        // 重置状态
        visited.clear();
        recursionStack.clear();
        topologicalOrder.clear();

        // 对每个未访问的顶点执行DFS
        for (int vertex : graph.keySet()) {
            if (!visited.contains(vertex)) {
                if (hasCycleDFS(vertex)) {
                    System.out.println("图中存在环,无法进行拓扑排序");
                    return null;
                }
            }
        }

        // 将栈中元素转换为列表(逆序)
        List<Integer> result = new ArrayList<>();
        while (!topologicalOrder.isEmpty()) {
            result.add(topologicalOrder.pop());
        }

        return result;
    }

    /**
     * DFS检测环并构建拓扑序列
     * @param vertex 当前顶点
     * @return 是否存在环
     */
    private boolean hasCycleDFS(int vertex) {
        // 将当前顶点标记为正在访问
        recursionStack.add(vertex);
        visited.add(vertex);

        // 访问所有邻接顶点
        for (int neighbor : graph.get(vertex)) {
            // 如果邻接顶点在递归栈中,说明存在环
            if (recursionStack.contains(neighbor)) {
                return true;
            }

            // 如果邻接顶点未被访问,递归访问
            if (!visited.contains(neighbor) && hasCycleDFS(neighbor)) {
                return true;
            }
        }

        // 当前顶点访问完毕,从递归栈中移除
        recursionStack.remove(vertex);

        // 将当前顶点加入拓扑排序结果
        topologicalOrder.push(vertex);

        return false;
    }

    /**
     * 打印图的邻接表表示
     */
    public void printGraph() {
        System.out.println("图的邻接表表示:");
        for (Map.Entry<Integer, List<Integer>> entry : graph.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}

方法二:Kahn算法(基于入度)

Kahn算法是另一种实现拓扑排序的经典方法。它的基本思想是:

  1. 计算所有顶点的入度
  2. 将入度为0的顶点加入队列
  3. 重复以下过程:
    • 从队列中取出一个顶点,加入结果
    • 将该顶点的所有邻接顶点的入度减1
    • 如果某个邻接顶点的入度变为0,将其加入队列
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import java.util.*;

public class TopologicalSortKahn {
    private Map<Integer, List<Integer>> graph;
    private Map<Integer, Integer> inDegree;

    public TopologicalSortKahn(int vertices) {
        this.graph = new HashMap<>();
        this.inDegree = new HashMap<>();

        // 初始化图和入度
        for (int i = 0; i < vertices; i++) {
            graph.put(i, new ArrayList<>());
            inDegree.put(i, 0);
        }
    }

    /**
     * 添加有向边
     * @param from 起始顶点
     * @param to 终止顶点
     */
    public void addEdge(int from, int to) {
        graph.get(from).add(to);
        inDegree.put(to, inDegree.get(to) + 1);
    }

    /**
     * 使用Kahn算法执行拓扑排序
     * @return 拓扑排序结果,如果存在环则返回null
     */
    public List<Integer> topologicalSort() {
        List<Integer> result = new ArrayList<>();
        Queue<Integer> queue = new LinkedList<>();

        // 创建入度的副本,避免修改原始数据
        Map<Integer, Integer> currentInDegree = new HashMap<>(inDegree);

        // 将所有入度为0的顶点加入队列
        for (Map.Entry<Integer, Integer> entry : currentInDegree.entrySet()) {
            if (entry.getValue() == 0) {
                queue.offer(entry.getKey());
            }
        }

        // Kahn算法主循环
        while (!queue.isEmpty()) {
            int current = queue.poll();
            result.add(current);

            // 遍历当前顶点的所有邻接顶点
            for (int neighbor : graph.get(current)) {
                // 减少邻接顶点的入度
                currentInDegree.put(neighbor, currentInDegree.get(neighbor) - 1);

                // 如果入度变为0,加入队列
                if (currentInDegree.get(neighbor) == 0) {
                    queue.offer(neighbor);
                }
            }
        }

        // 如果结果中的顶点数量等于图中顶点总数,说明无环
        if (result.size() == graph.size()) {
            return result;
        } else {
            System.out.println("图中存在环,无法进行拓扑排序");
            return null;
        }
    }

    /**
     * 获取指定顶点的入度
     * @param vertex 顶点
     * @return 入度
     */
    public int getInDegree(int vertex) {
        return inDegree.getOrDefault(vertex, 0);
    }

    /**
     * 打印所有顶点的入度信息
     */
    public void printInDegrees() {
        System.out.println("各顶点入度:");
        for (Map.Entry<Integer, Integer> entry : inDegree.entrySet()) {
            System.out.println("顶点 " + entry.getKey() + ": " + entry.getValue());
        }
    }
}

环检测与错误处理

在实际应用中,我们需要处理图中可能存在的环。以下是一个增强的环检测实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class CycleDetectionUtils {

    /**
     * 使用DFS检测有向图中的环
     * @param graph 图的邻接表表示
     * @return 如果存在环返回环中的顶点列表,否则返回空列表
     */
    public static List<Integer> detectCycle(Map<Integer, List<Integer>> graph) {
        Set<Integer> white = new HashSet<>(); // 未访问
        Set<Integer> gray = new HashSet<>();  // 正在访问
        Set<Integer> black = new HashSet<>(); // 已完成访问
        Map<Integer, Integer> parent = new HashMap<>();

        // 初始化所有顶点为白色
        for (int vertex : graph.keySet()) {
            white.add(vertex);
        }

        // 对每个白色顶点执行DFS
        for (int vertex : graph.keySet()) {
            if (white.contains(vertex)) {
                List<Integer> cycle = dfsVisit(vertex, graph, white, gray, black, parent);
                if (!cycle.isEmpty()) {
                    return cycle;
                }
            }
        }

        return new ArrayList<>(); // 无环
    }

    private static List<Integer> dfsVisit(int vertex, Map<Integer, List<Integer>> graph,
                                         Set<Integer> white, Set<Integer> gray, Set<Integer> black,
                                         Map<Integer, Integer> parent) {
        // 将顶点从白色移到灰色
        white.remove(vertex);
        gray.add(vertex);

        // 访问所有邻接顶点
        for (int neighbor : graph.get(vertex)) {
            parent.put(neighbor, vertex);

            if (gray.contains(neighbor)) {
                // 发现后向边,存在环
                return constructCycle(neighbor, vertex, parent);
            }

            if (white.contains(neighbor)) {
                List<Integer> cycle = dfsVisit(neighbor, graph, white, gray, black, parent);
                if (!cycle.isEmpty()) {
                    return cycle;
                }
            }
        }

        // 将顶点从灰色移到黑色
        gray.remove(vertex);
        black.add(vertex);

        return new ArrayList<>();
    }

    /**
     * 构造环路径
     */
    private static List<Integer> constructCycle(int start, int end, Map<Integer, Integer> parent) {
        List<Integer> cycle = new ArrayList<>();
        int current = end;

        while (current != start) {
            cycle.add(current);
            current = parent.get(current);
        }
        cycle.add(start);

        Collections.reverse(cycle);
        return cycle;
    }
}

高级应用

1. 关键路径分析(Critical Path Method, CPM)

在项目管理中,关键路径是完成项目所需的最长路径。我们可以结合拓扑排序来实现CPM:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
public class CriticalPathAnalysis {

    static class Task {
        int id;
        String name;
        int duration;
        int earliestStart;
        int earliestFinish;
        int latestStart;
        int latestFinish;

        public Task(int id, String name, int duration) {
            this.id = id;
            this.name = name;
            this.duration = duration;
        }

        public boolean isCritical() {
            return earliestStart == latestStart;
        }
    }

    private Map<Integer, Task> tasks;
    private Map<Integer, List<Integer>> dependencies;

    public CriticalPathAnalysis() {
        this.tasks = new HashMap<>();
        this.dependencies = new HashMap<>();
    }

    public void addTask(int id, String name, int duration) {
        tasks.put(id, new Task(id, name, duration));
        dependencies.put(id, new ArrayList<>());
    }

    public void addDependency(int from, int to) {
        dependencies.get(from).add(to);
    }

    /**
     * 计算关键路径
     * @return 关键路径上的任务列表
     */
    public List<Task> calculateCriticalPath() {
        // 1. 执行拓扑排序
        TopologicalSortKahn sorter = new TopologicalSortKahn(tasks.size());
        for (Map.Entry<Integer, List<Integer>> entry : dependencies.entrySet()) {
            for (int dep : entry.getValue()) {
                sorter.addEdge(entry.getKey(), dep);
            }
        }

        List<Integer> topologicalOrder = sorter.topologicalSort();
        if (topologicalOrder == null) {
            throw new IllegalStateException("项目存在循环依赖");
        }

        // 2. 计算最早开始时间和最早完成时间
        calculateEarliestTimes(topologicalOrder);

        // 3. 计算最晚开始时间和最晚完成时间
        calculateLatestTimes(topologicalOrder);

        // 4. 找出关键路径
        return findCriticalPath();
    }

    private void calculateEarliestTimes(List<Integer> order) {
        for (int taskId : order) {
            Task task = tasks.get(taskId);

            // 计算最早开始时间:所有前驱任务的最早完成时间的最大值
            int maxPredecessorFinish = 0;
            for (Map.Entry<Integer, List<Integer>> entry : dependencies.entrySet()) {
                if (entry.getValue().contains(taskId)) {
                    Task predecessor = tasks.get(entry.getKey());
                    maxPredecessorFinish = Math.max(maxPredecessorFinish, predecessor.earliestFinish);
                }
            }

            task.earliestStart = maxPredecessorFinish;
            task.earliestFinish = task.earliestStart + task.duration;
        }
    }

    private void calculateLatestTimes(List<Integer> order) {
        // 找到项目结束时间
        int projectEndTime = 0;
        for (Task task : tasks.values()) {
            projectEndTime = Math.max(projectEndTime, task.earliestFinish);
        }

        // 逆序计算最晚时间
        Collections.reverse(order);
        for (int taskId : order) {
            Task task = tasks.get(taskId);

            // 如果是最后的任务,最晚完成时间等于项目结束时间
            if (dependencies.get(taskId).isEmpty()) {
                task.latestFinish = projectEndTime;
            } else {
                // 最晚完成时间:所有后继任务最晚开始时间的最小值
                int minSuccessorStart = Integer.MAX_VALUE;
                for (int successor : dependencies.get(taskId)) {
                    Task successorTask = tasks.get(successor);
                    minSuccessorStart = Math.min(minSuccessorStart, successorTask.latestStart);
                }
                task.latestFinish = minSuccessorStart;
            }

            task.latestStart = task.latestFinish - task.duration;
        }
    }

    private List<Task> findCriticalPath() {
        List<Task> criticalTasks = new ArrayList<>();
        for (Task task : tasks.values()) {
            if (task.isCritical()) {
                criticalTasks.add(task);
            }
        }

        // 按拓扑顺序排列关键任务
        criticalTasks.sort((a, b) -> Integer.compare(a.earliestStart, b.earliestStart));
        return criticalTasks;
    }
}

2. 依赖解析系统

类似于包管理器的依赖解析:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
public class DependencyResolver {

    static class Package {
        String name;
        String version;
        List<Dependency> dependencies;

        public Package(String name, String version) {
            this.name = name;
            this.version = version;
            this.dependencies = new ArrayList<>();
        }

        @Override
        public String toString() {
            return name + "@" + version;
        }
    }

    static class Dependency {
        String packageName;
        String versionConstraint;

        public Dependency(String packageName, String versionConstraint) {
            this.packageName = packageName;
            this.versionConstraint = versionConstraint;
        }
    }

    private Map<String, Package> packages;
    private Map<String, List<String>> dependencyGraph;

    public DependencyResolver() {
        this.packages = new HashMap<>();
        this.dependencyGraph = new HashMap<>();
    }

    public void addPackage(Package pkg) {
        packages.put(pkg.name, pkg);
        dependencyGraph.put(pkg.name, new ArrayList<>());

        for (Dependency dep : pkg.dependencies) {
            dependencyGraph.get(pkg.name).add(dep.packageName);
        }
    }

    /**
     * 解析安装顺序
     * @param rootPackage 根包名
     * @return 安装顺序
     */
    public List<String> resolveInstallOrder(String rootPackage) {
        if (!packages.containsKey(rootPackage)) {
            throw new IllegalArgumentException("包不存在: " + rootPackage);
        }

        // 构建完整的依赖图
        Set<String> allPackages = collectAllDependencies(rootPackage);

        // 创建拓扑排序器
        Map<String, Integer> packageIndex = new HashMap<>();
        int index = 0;
        for (String pkg : allPackages) {
            packageIndex.put(pkg, index++);
        }

        TopologicalSortKahn sorter = new TopologicalSortKahn(allPackages.size());

        // 添加依赖边
        for (String pkg : allPackages) {
            if (dependencyGraph.containsKey(pkg)) {
                for (String dep : dependencyGraph.get(pkg)) {
                    if (allPackages.contains(dep)) {
                        sorter.addEdge(packageIndex.get(dep), packageIndex.get(pkg));
                    }
                }
            }
        }

        List<Integer> order = sorter.topologicalSort();
        if (order == null) {
            throw new IllegalStateException("检测到循环依赖");
        }

        // 转换回包名
        List<String> packageOrder = new ArrayList<>();
        String[] packageArray = allPackages.toArray(new String[0]);
        for (int idx : order) {
            packageOrder.add(packageArray[idx]);
        }

        return packageOrder;
    }

    private Set<String> collectAllDependencies(String rootPackage) {
        Set<String> visited = new HashSet<>();
        Queue<String> queue = new LinkedList<>();

        queue.offer(rootPackage);
        visited.add(rootPackage);

        while (!queue.isEmpty()) {
            String current = queue.poll();

            if (dependencyGraph.containsKey(current)) {
                for (String dep : dependencyGraph.get(current)) {
                    if (!visited.contains(dep) && packages.containsKey(dep)) {
                        visited.add(dep);
                        queue.offer(dep);
                    }
                }
            }
        }

        return visited;
    }
}

性能分析与优化

时间复杂度分析

  1. DFS方法:O(V + E)

    • 每个顶点被访问一次:O(V)
    • 每条边被检查一次:O(E)
  2. Kahn算法:O(V + E)

    • 计算入度:O(E)
    • 处理每个顶点:O(V)
    • 处理每条边:O(E)

空间复杂度分析

  1. DFS方法:O(V)

    • 递归栈深度最多为V
    • 访问标记和递归栈:O(V)
  2. Kahn算法:O(V)

    • 入度数组:O(V)
    • 队列最多包含V个元素:O(V)

优化技巧

1. 内存优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class OptimizedTopologicalSort {

    /**
     * 使用位集优化内存使用
     */
    public static List<Integer> topologicalSortOptimized(int[][] graph) {
        int n = graph.length;
        int[] inDegree = new int[n];

        // 计算入度
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < graph[i].length; j++) {
                if (graph[i][j] != -1) {
                    inDegree[graph[i][j]]++;
                }
            }
        }

        // 使用数组模拟队列,避免对象创建开销
        int[] queue = new int[n];
        int front = 0, rear = 0;

        // 添加入度为0的顶点
        for (int i = 0; i < n; i++) {
            if (inDegree[i] == 0) {
                queue[rear++] = i;
            }
        }

        List<Integer> result = new ArrayList<>(n);

        while (front < rear) {
            int current = queue[front++];
            result.add(current);

            // 更新邻接顶点的入度
            for (int j = 0; j < graph[current].length; j++) {
                if (graph[current][j] != -1) {
                    int neighbor = graph[current][j];
                    if (--inDegree[neighbor] == 0) {
                        queue[rear++] = neighbor;
                    }
                }
            }
        }

        return result.size() == n ? result : null;
    }
}

2. 并行优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ParallelTopologicalSort {

    /**
     * 并行拓扑排序(适用于大规模图)
     */
    public static List<Integer> parallelTopologicalSort(Map<Integer, List<Integer>> graph) {
        int n = graph.size();
        AtomicIntegerArray inDegree = new AtomicIntegerArray(n);

        // 并行计算入度
        graph.entrySet().parallelStream().forEach(entry -> {
            entry.getValue().forEach(neighbor -> {
                inDegree.incrementAndGet(neighbor);
            });
        });

        ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
        List<Integer> result = Collections.synchronizedList(new ArrayList<>());

        // 添加入度为0的顶点
        IntStream.range(0, n).parallel()
            .filter(i -> inDegree.get(i) == 0)
            .forEach(queue::offer);

        // 并行处理(需要同步控制)
        while (!queue.isEmpty()) {
            Integer current = queue.poll();
            if (current != null) {
                result.add(current);

                if (graph.containsKey(current)) {
                    graph.get(current).parallelStream().forEach(neighbor -> {
                        if (inDegree.decrementAndGet(neighbor) == 0) {
                            queue.offer(neighbor);
                        }
                    });
                }
            }
        }

        return result.size() == n ? new ArrayList<>(result) : null;
    }
}

实战案例

案例1:构建系统实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class BuildSystemExample {
    public static void main(String[] args) {
        System.out.println("=== 构建系统依赖解析示例 ===");

        // 创建模块依赖图
        TopologicalSortKahn buildSystem = new TopologicalSortKahn(6);

        // 定义模块
        String[] modules = {"utils", "core", "dao", "service", "web", "test"};

        // 添加依赖关系
        buildSystem.addEdge(0, 1); // utils -> core
        buildSystem.addEdge(0, 2); // utils -> dao
        buildSystem.addEdge(1, 3); // core -> service
        buildSystem.addEdge(2, 3); // dao -> service
        buildSystem.addEdge(3, 4); // service -> web
        buildSystem.addEdge(1, 5); // core -> test
        buildSystem.addEdge(3, 5); // service -> test

        List<Integer> buildOrder = buildSystem.topologicalSort();

        if (buildOrder != null) {
            System.out.println("推荐的构建顺序:");
            for (int i = 0; i < buildOrder.size(); i++) {
                System.out.println((i + 1) + ". " + modules[buildOrder.get(i)]);
            }
        } else {
            System.out.println("检测到循环依赖,无法构建!");
        }
    }
}

案例2:课程安排系统

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class CourseSchedulingExample {
    public static void main(String[] args) {
        System.out.println("=== 课程安排系统示例 ===");

        // 创建课程依赖图
        Map<String, Integer> courseMap = new HashMap<>();
        courseMap.put("数学基础", 0);
        courseMap.put("编程基础", 1);
        courseMap.put("数据结构", 2);
        courseMap.put("算法设计", 3);
        courseMap.put("软件工程", 4);
        courseMap.put("高级算法", 5);

        TopologicalSortDFS scheduler = new TopologicalSortDFS(6);

        // 添加先修关系
        scheduler.addEdge(0, 2); // 数学基础 -> 数据结构
        scheduler.addEdge(0, 3); // 数学基础 -> 算法设计
        scheduler.addEdge(1, 2); // 编程基础 -> 数据结构
        scheduler.addEdge(1, 4); // 编程基础 -> 软件工程
        scheduler.addEdge(2, 3); // 数据结构 -> 算法设计
        scheduler.addEdge(3, 5); // 算法设计 -> 高级算法

        List<Integer> schedule = scheduler.topologicalSort();

        if (schedule != null) {
            System.out.println("推荐的学习顺序:");
            String[] courses = {"数学基础", "编程基础", "数据结构", "算法设计", "软件工程", "高级算法"};
            for (int i = 0; i < schedule.size(); i++) {
                System.out.println("第" + (i + 1) + "学期: " + courses[schedule.get(i)]);
            }
        }
    }
}

实际应用场景

1. 编译器设计

在编译器中,拓扑排序用于:

  • 符号表构建:确保类型定义的正确顺序
  • 代码生成:确定函数和类的编译顺序
  • 优化阶段:确保优化passes的正确执行顺序

2. 数据库查询优化

在数据库系统中:

  • 表连接顺序:优化多表连接的执行计划
  • 索引构建:确定复合索引的构建顺序
  • 视图依赖:解析视图间的依赖关系

3. 任务调度系统

在分布式系统中:

  • MapReduce作业调度:确保数据处理流水线的正确执行
  • 微服务部署:确定服务的启动和停止顺序
  • 资源分配:按依赖关系分配计算资源

总结

拓扑排序是图论中的一个重要算法,在计算机科学和软件工程中有着广泛的应用。通过本文,我们学习了:

  1. 核心概念:拓扑排序的定义、性质和适用条件
  2. 算法实现:DFS和Kahn两种经典实现方法
  3. 环检测:如何检测和处理图中的环
  4. 高级应用:关键路径分析、依赖解析等实际应用
  5. 性能优化:内存优化和并行化技巧
  6. 实战案例:构建系统、课程安排等具体应用

拓扑排序的时间复杂度为O(V+E),空间复杂度为O(V),是一个高效的算法。在实际应用中,我们需要根据具体场景选择合适的实现方法:

  • DFS方法:适合需要检测环的场景,代码相对简洁
  • Kahn算法:更直观易懂,适合教学和理解
  • 优化版本:适合大规模图和性能敏感的应用

掌握拓扑排序不仅能够解决依赖关系问题,更重要的是培养了系统性思维,这在软件设计和架构中都是非常宝贵的能力。