算法之旅:LeetCode 拓扑排序由简入繁完全攻略

前言

欢迎来到我的算法探索博客,在这里,我将通过解析精选的LeetCode题目,与您分享深刻的解题思路、多元化的解决方案以及宝贵的实战经验,旨在帮助每一位读者提升编程技能,领略算法之美。
👉更多高频有趣LeetCode算法题

拓扑排序是一种适用于 有向无环图(DAG) 的重要算法,常用于解决依赖关系问题,如课程安排、任务调度等。在本文中,我们将通过以下四道题目,详细讲解拓扑排序的原理、实现方式及其多样化应用场景:

1557. 可以到达所有点的最少点数目 207. 课程表
210. 课程表 II 802. 找到最终的安全状态

拓扑排序基础知识

核心思想:

拓扑排序旨在为图中的节点安排一种线性顺序,使得对每条有向边 (u, v),节点 u 总是排在 v 之前。

本节我们利用 入度表 + 广度优先搜索(BFS) 实现拓扑排序:

  1. 入度的概念
  • 每个节点的 入度 是指有多少条边指向这个节点。
  • 如果某个节点的入度为 0,说明它没有依赖,可以作为起点开始。
  1. 拓扑排序的原理
  • 将所有入度为 0 的节点加入队列(表示这些节点可以直接开始,不需要经过任何依赖)。
  • 从队列中逐一取出节点,将其所有出边的目标节点的入度减 1 (被引用次数-1)
  • 如果某个节点的入度变为 0,将其加入队列。
  • 重复这一过程,直到队列为空。
  • 如果完成所有节点的拓扑排序,说明图中无环;否则,说明存在环。

适用条件:

图必须是 有向无环图(DAG)
若存在环,则无法构建拓扑排序。

常见实现方法:

  • Kahn算法: 基于入度统计。逐步移除入度为 0 的节点,动态更新图结构。
  • DFS(深度优先搜索): 通过后序遍历逆序输出结果。

实战:经典例题讲解

1557. 可以到达所有点的最少点数目

🪸题目描述

🪷核心思路

这是一个经典的入度问题,这题可以看作是 拓扑排序思想的局部应用,利用入度信息快速判断需要作为起点的节点。初具雏形。
若一个节点的入度为 0,则必须从它出发才能到达该节点。

因此,我们只需要找出所有入度为 0 的节点即可。

  1. 构建入度数组
java 复制代码
for(List<Integer> a : edges){
    inDegree[a.get(1)]++;
}
  • 遍历所有边,计算每个节点的入度 (即有多少条边指向该节点,被引用的次数)
  • 结果存储在 inDegree 数组中,其中 inDegree[i] 表示节点 i 的入度。
  1. 找出入度为 0 的节点
java 复制代码
for (int i = 0; i < n; i++) {
    if (inDegree[i] == 0) {
        res.add(i);
    }
}
  • 遍历所有节点,检查哪些节点的入度为 0
  • 入度为 0 的节点没有任何依赖,它们必须作为路径的起点,加入结果列表 res。
  1. 返回结果
    最终返回 res,即所有入度为 0 的节点构成的集合。

🌿代码实现

Java
java 复制代码
class Solution {
    public List<Integer> findSmallestSetOfVertices(int n, List<List<Integer>> edges) {
        List<Integer> res = new ArrayList<>();
        int[] inDegree = new int[n];

        // 构建入度数组
        // 其中每个元素表示对应被依赖的次数,也就是 入度
        for(List<Integer> a : edges){
            inDegree[a.get(1)]++;
        }

        // 将所有入度为 0 的课程加入队列
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            if (inDegree[i] == 0) {
                res.add(i);
            }
        }
        return res;
    }
}
Python
python 复制代码
class Solution(object):
    def findSmallestSetOfVertices(self, n, edges):
        """
        :type n: int
        :type edges: List[List[int]]
        :rtype: List[int]
        """
        # 初始化入度数组
        in_degree = [0] * n
        
        # 构建入度数组
        for edge in edges:
            in_degree[edge[1]] += 1
        
        # 找出所有入度为 0 的节点
        return [i for i in range(n) if in_degree[i] == 0]
C++
cpp 复制代码
class Solution {
public:
    vector<int> findSmallestSetOfVertices(int n, vector<vector<int>>& edges) {
        // 初始化入度数组
        vector<int> inDegree(n, 0);

        // 构建入度数组
        for (const auto& edge : edges) {
            inDegree[edge[1]]++;
        }

        // 找出所有入度为 0 的节点
        vector<int> result;
        for (int i = 0; i < n; ++i) {
            if (inDegree[i] == 0) {
                result.push_back(i);
            }
        }

        return result;
    }
};

207. 课程表

🪸题目描述

🪷核心思路

这是一道经典的图是否有环的问题,可以通过 Kahn算法DFS 判断环的存在。

利用 入度表 + 广度优先搜索(BFS) 实现拓扑排序。

比上一题多了一步的就是加了一个邻接表 ,目的就是把两个点的有向连接(图二)表示出来进行BFS遍历求得结果。

🌿代码实现

Java
java 复制代码
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 入度数组,表示每个课程被依赖的次数
        int[] inDegree = new int[numCourses];
        // 图的邻接表表示
        List<List<Integer>> adjacency = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            adjacency.add(new ArrayList<>());
        }
        // 构建图和入度数组
        for (int[] pair : prerequisites) {
            inDegree[pair[0]]++;
            // 每个课程(节点)都有一个列表,列表中存储的是所有依赖于该课程的其他课程(即该课程是其他课程的先修课程)
            adjacency.get(pair[1]).add(pair[0]);
        }

        // 将所有 入度为 0 的课程加入队列,表示这些课程可以直接学习,无需先修课程。
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        // BFS 遍历
        int count = 0; // 记录已完成的课程数量
        while (!queue.isEmpty()) {
            int course = queue.poll();
            count++;
            for (int nextCourse : adjacency.get(course)) {
                inDegree[nextCourse]--;
                // 添加接下来 入度为0 的元素
                if (inDegree[nextCourse] == 0) {
                    queue.offer(nextCourse);
                }
            }
        }

        // 当 BFS 结束时,如果拓扑排序数组中的课程数小于总课程数,说明图中存在环,无法完成所有课程。这时,返回空数组 []
        return count == numCourses;
    }
}
Python
python 复制代码
class Solution(object):
    def canFinish(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: bool
        """
        # 入度数组,表示每个课程被依赖的次数
        in_degree = [0] * numCourses
        # 图的邻接表表示
        adjacency = [[] for _ in range(numCourses)]

        # 构建图和入度数组
        for pair in prerequisites:
            in_degree[pair[0]] += 1
            adjacency[pair[1]].append(pair[0])

        # 将所有 入度为 0 的课程加入队列,表示这些课程可以直接学习,无需先修课程。
        queue = []
        for i in range(numCourses):
            if in_degree[i] == 0:
                queue.append(i)

        # BFS 遍历
        count = 0  # 记录已完成的课程数量
        while queue:
            course = queue.pop(0)
            count += 1
            for next_course in adjacency[course]:
                in_degree[next_course] -= 1
                if in_degree[next_course] == 0:
                    queue.append(next_course)

        # 如果拓扑排序中的课程数小于总课程数,说明存在环,无法完成所有课程
        return count == numCourses
C++
cpp 复制代码
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        // 入度数组,表示每个课程被依赖的次数
        vector<int> inDegree(numCourses, 0);
        // 图的邻接表表示
        vector<vector<int>> adjacency(numCourses);

        // 构建图和入度数组
        for (const auto& pair : prerequisites) {
            inDegree[pair[0]]++;
            adjacency[pair[1]].push_back(pair[0]);
        }

        // 将所有 入度为 0 的课程加入队列,表示这些课程可以直接学习,无需先修课程。
        queue<int> q;
        for (int i = 0; i < numCourses; ++i) {
            if (inDegree[i] == 0) {
                q.push(i);
            }
        }

        // BFS 遍历
        int count = 0; // 记录已完成的课程数量
        while (!q.empty()) {
            int course = q.front();
            q.pop();
            count++;
            for (int nextCourse : adjacency[course]) {
                inDegree[nextCourse]--;
                if (inDegree[nextCourse] == 0) {
                    q.push(nextCourse);
                }
            }
        }

        // 如果拓扑排序中的课程数小于总课程数,说明存在环,无法完成所有课程
        return count == numCourses;
    }
};

210. 课程表 II

🪸题目描述

🪷核心思路

207. 课程表 类似,但需要输出一条合法的课程学习路径。我们可以直接基于拓扑排序构造学习路径

🌿代码实现

Java
java 复制代码
class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        // 入度数组,表示每个课程被依赖的次数
        int[] inDegree = new int[numCourses];
        // 图的邻接表表示
        List<List<Integer>> adjacency = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            adjacency.add(new ArrayList<>());
        }
        // 构建图和入度数组
        for (int[] pair : prerequisites) {
            inDegree[pair[0]]++;
            adjacency.get(pair[1]).add(pair[0]);
        }

        // 将所有入度为 0 的课程加入队列
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        // 保存课程学习顺序
        int[] order = new int[numCourses];
        int index = 0; // 指向 `order` 数组的位置

        // BFS 遍历
        while (!queue.isEmpty()) {
            int course = queue.poll();
            order[index++] = course; // 将课程加入学习顺序
            for (int nextCourse : adjacency.get(course)) {
                inDegree[nextCourse]--;
                if (inDegree[nextCourse] == 0) {
                    queue.offer(nextCourse);
                }
            }
        }

        return index < numCourses ? new int[0] : order; // 返回课程学习顺序
    }
}
Python
python 复制代码
class Solution(object):
    def findOrder(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: List[int]
        """
        # 入度数组,表示每个课程被依赖的次数
        in_degree = [0] * numCourses
        # 图的邻接表表示
        adjacency = [[] for _ in range(numCourses)]

        # 构建图和入度数组
        for pair in prerequisites:
            in_degree[pair[0]] += 1
            adjacency[pair[1]].append(pair[0])

        # 将所有入度为 0 的课程加入队列
        queue = []
        for i in range(numCourses):
            if in_degree[i] == 0:
                queue.append(i)

        # 保存课程学习顺序
        order = []
        while queue:
            course = queue.pop(0)
            order.append(course)
            for next_course in adjacency[course]:
                in_degree[next_course] -= 1
                if in_degree[next_course] == 0:
                    queue.append(next_course)

        # 如果拓扑排序未覆盖所有课程,返回空数组
        return order if len(order) == numCourses else []
C++
cpp 复制代码
class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        // 入度数组,表示每个课程被依赖的次数
        vector<int> inDegree(numCourses, 0);
        // 图的邻接表表示
        vector<vector<int>> adjacency(numCourses);

        // 构建图和入度数组
        for (const auto& pair : prerequisites) {
            inDegree[pair[0]]++;
            adjacency[pair[1]].push_back(pair[0]);
        }

        // 将所有入度为 0 的课程加入队列
        queue<int> q;
        for (int i = 0; i < numCourses; ++i) {
            if (inDegree[i] == 0) {
                q.push(i);
            }
        }

        // 保存课程学习顺序
        vector<int> order;
        while (!q.empty()) {
            int course = q.front();
            q.pop();
            order.push_back(course);
            for (int nextCourse : adjacency[course]) {
                inDegree[nextCourse]--;
                if (inDegree[nextCourse] == 0) {
                    q.push(nextCourse);
                }
            }
        }

        // 如果拓扑排序未覆盖所有课程,返回空数组
        return order.size() == numCourses ? order : vector<int>();
    }
};

802. 找到最终的安全状态

🪸题目描述

🪷核心思路

我们可以从反向图出发,寻找出度为 0 的节点(终点),并依次标记为安全。

为什么反向图能够帮助我们找到安全节点?

  1. 从安全节点开始反向查找:
  • 安全节点是指没有环的节点,因此从这些节点出发无法到达其他节点,因此它们的反向图中入度为 0
  • 一旦节点的入度为 0,意味着没有节点依赖它,它是"安全"的。
  1. 拓扑排序的过程:
  • 在反向图中,拓扑排序会找到所有的"无依赖"节点(即入度为 0 的节点),这些节点可以认为是安全的。
  • 通过拓扑排序,如果某个节点进入队列并被处理,说明它没有环,并且在反向图中是可以到达终点的。

🌿代码实现

Java
java 复制代码
class Solution {
    public List<Integer> eventualSafeNodes(int[][] graph) {
        // 反向图 + 拓扑排序
        int n = graph.length;
        List<List<Integer>> reverseGraph = new ArrayList<>();
        int[] inDegree = new int[n];
        for (int i = 0; i < n; i++) {
            reverseGraph.add(new ArrayList<>());
        }

        for (int i = 0; i < n; i++) {
            for (int next : graph[i]) {
                reverseGraph.get(next).add(i);
                inDegree[i]++;
            }
        }
        
        // 2. 将所有入度为 0 的节点加入队列(安全节点)
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        // 3. 拓扑排序
        List<Integer> safeNodes = new ArrayList<>();
        while (!queue.isEmpty()) {
            int node = queue.poll();
            safeNodes.add(node);
            for (int prev : reverseGraph.get(node)) {
                // 如果当前节点是某个节点的依赖节点,减少它的入度
                inDegree[prev]--;
                if (inDegree[prev] == 0) {
                    queue.offer(prev);  // 如果该节点的入度为 0,说明它是安全节点
                }
            }
        }
        
        // 4. 返回所有安全节点(按升序排序)
        Collections.sort(safeNodes);
        return safeNodes;
    }
}
Python
python 复制代码
class Solution(object):
    def eventualSafeNodes(self, graph):
        """
        :type graph: List[List[int]]
        :rtype: List[int]
        """
        n = len(graph)
        reverseGraph = [[] for _ in range(n)]
        inDegree = [0] * n
        # 构建反向图并计算入度
        for i in range(n):
            for next_node in graph[i]:
                reverseGraph[next_node].append(i)
                inDegree[i] += 1
        
        # 将所有入度为 0 的节点加入队列
        queue = deque()
        for i in range(n):
            if inDegree[i] == 0:
                queue.append(i)
        
        # 拓扑排序
        safeNodes = []
        while queue:
            node = queue.popleft()
            safeNodes.append(node)
            for prev in reverseGraph[node]:
                inDegree[prev] -= 1
                if inDegree[prev] == 0:
                    queue.append(prev)
        
        # 返回所有安全节点,按升序排序
        safeNodes.sort()
        return safeNodes
C++
cpp 复制代码
class Solution {
public:
    vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
        int n = graph.size();
        vector<vector<int>> reverseGraph(n);
        vector<int> inDegree(n, 0);
        
        // 构建反向图和计算每个节点的入度
        for (int i = 0; i < n; ++i) {
            for (int next : graph[i]) {
                reverseGraph[next].push_back(i);  // 反向图
                inDegree[i]++;  // 计算入度
            }
        }
        
        // 将所有入度为 0 的节点加入队列
        queue<int> q;
        for (int i = 0; i < n; ++i) {
            if (inDegree[i] == 0) {
                q.push(i);
            }
        }

        // 拓扑排序
        vector<int> safeNodes;
        while (!q.empty()) {
            int node = q.front();
            q.pop();
            safeNodes.push_back(node);
            for (int prev : reverseGraph[node]) {
                if (--inDegree[prev] == 0) {
                    q.push(prev);
                }
            }
        }
        
        // 返回所有安全节点,按升序排序
        sort(safeNodes.begin(), safeNodes.end());
        return safeNodes;
    }
};

结语

通过这四道题,我们可以看到拓扑排序的强大应用:

  • 解决 依赖问题 ,如课程安排(207, 210)
  • 处理 图中状态分类 的问题,如安全状态(802)
  • 分析 关键点或入度特性 ,如找到最小的起点集合(1557)

拓扑排序不仅是一种算法,更是一种理解图结构的思维方式。在面试中,遇到类似依赖关系的题目,尝试从有向图的角度切入往往是一个很好的突破点。


如果您渴望探索更多精心挑选的高频LeetCode面试题,以及它们背后的巧妙解法,欢迎您访问我的博客,那里有我精心准备的一系列文章,旨在帮助技术爱好者们提升算法能力与编程技巧。

👉更多高频有趣LeetCode算法题

在我的博客中,每一篇文章都是我对算法世界的一次深入挖掘,不仅包含详尽的题目解析,还有我个人的心得体会、优化思路及实战经验分享。无论是准备面试还是追求技术成长,我相信这些内容都能为您提供宝贵的参考与启发。期待您的光临,让我们共同在技术之路上不断前行!

相关推荐
Dream_Snowar4 分钟前
速通Python 第四节——函数
开发语言·python·算法
西猫雷婶5 分钟前
python学opencv|读取图像(十四)BGR图像和HSV图像通道拆分
开发语言·python·opencv
星河梦瑾6 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富9 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想11 分钟前
JMeter 使用详解
java·jmeter
言、雲13 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
Altair澳汰尔17 分钟前
数据分析和AI丨知识图谱,AI革命中数据集成和模型构建的关键推动者
人工智能·算法·机器学习·数据分析·知识图谱
TT哇20 分钟前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
东风吹柳33 分钟前
观察者模式(sigslot in C++)
c++·观察者模式·信号槽·sigslot
A懿轩A41 分钟前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列