0

0

无向图循环检测:深度优先搜索与并查集应用详解

碧海醫心

碧海醫心

发布时间:2025-07-21 13:56:11

|

315人浏览过

|

来源于php中文网

原创

无向图循环检测:深度优先搜索与并查集应用详解

本文深入探讨了在无向图中检测循环的两种主要算法:深度优先搜索(DFS)和并查集(Union-Find)。DFS通过识别回边来发现循环,而并查集则通过检查连接的顶点是否已属于同一集合来高效地判断循环存在。文章将详细阐述这两种方法的原理、实现细节及注意事项,并提供示例代码,旨在帮助读者掌握无向图循环检测的关键技术。

1. 理解无向图中的循环

在图论中,一个循环(Cycle)是指从图中的某个顶点出发,沿着一系列边最终回到该顶点的路径,且路径上的所有顶点(除了起点和终点)都不重复。在无向图中,由于边是双向的,因此简单地沿着一条边再立即返回不算作循环。循环检测在许多图算法中至关重要,例如判断图是否为树、拓扑排序的先决条件(有向无环图DAG)、最小生成树算法等。

2. 方法一:基于深度优先搜索(DFS)的循环检测

深度优先搜索是一种遍历图的算法,它尽可能深地探索图的分支。在无向图中,DFS可以有效地检测循环,其核心思想是识别“回边”(Back-Edge)。

2.1 核心思想:回边检测

在DFS遍历过程中,如果从当前顶点 u 访问其邻接点 v 时,发现 v 已经被访问过,并且 v 不是 u 的直接父节点,那么就意味着存在一条从 u 到 v 的边,而 v 已经在当前DFS路径上,这条边 (u, v) 就构成了一个循环。

免费语音克隆
免费语音克隆

这是一个提供免费语音克隆服务的平台,用户只需上传或录制一段 5 秒以上的清晰语音样本,平台即可生成与用户声音高度一致的 AI 语音克隆。

下载

2.2 算法原理

  1. 初始化:
    • 使用一个集合或布尔数组 visited 来记录已被访问的顶点。
    • 使用一个映射 parent 来记录每个顶点在DFS遍历树中的父节点。
  2. 遍历:
    • 对图中的每个顶点执行DFS。这是为了确保处理非连通图中的所有组件。
    • 从一个未访问的顶点 u 开始DFS,并将其标记为已访问。
    • 对于 u 的每一个邻接点 v:
      • 如果 v 未被访问,则将 u 设为 v 的父节点,并递归地对 v 执行DFS。如果在递归调用中发现循环,则立即返回 true。
      • 如果 v 已经被访问,并且 v 不是 u 的父节点(即 v 不是我们从哪里来到 u 的那个顶点),则说明找到了一条回边,存在循环,返回 true。
  3. 返回: 如果整个DFS过程完成,没有发现任何回边,则说明图中不存在循环,返回 false。

2.3 示例代码(Java-like Pseudocode)

import java.util.*;

class GraphDFS {
    private Map<String, List<String>> adj; // 邻接列表表示图
    private Set<String> visited;         // 记录已访问的节点
    private Map<String, String> parent;  // 记录节点的父节点

    public GraphDFS(Map<String, List<String>> adjacencyList) {
        this.adj = adjacencyList;
    }

    /**
     * 检测无向图中是否存在循环。
     * @return 如果存在循环返回 true,否则返回 false。
     */
    public boolean hasCycle() {
        visited = new HashSet<>();
        parent = new HashMap<>();

        // 遍历所有节点,确保处理非连通图中的所有组件
        // 收集所有可能的节点,包括那些没有邻居的节点
        Set<String> allNodes = new HashSet<>(adj.keySet());
        for (List<String> neighbors : adj.values()) {
            allNodes.addAll(neighbors);
        }

        for (String node : allNodes) {
            if (!visited.contains(node)) {
                // 从未访问的节点开始DFS,父节点设为null
                if (dfs(node, null)) {
                    return true; // 发现循环
                }
            }
        }
        return false; // 没有发现循环
    }

    /**
     * 深度优先搜索辅助函数。
     * @param u 当前访问的节点
     * @param p u的父节点
     * @return 如果在当前DFS路径中发现循环返回 true,否则返回 false。
     */
    private boolean dfs(String u, String p) {
        visited.add(u); // 标记当前节点为已访问
        parent.put(u, p); // 记录父节点

        // 遍历当前节点的所有邻接点
        for (String v : adj.getOrDefault(u, Collections.emptyList())) {
            if (!visited.contains(v)) {
                // 如果邻接点v未被访问,则递归地进行DFS
                if (dfs(v, u)) {
                    return true; // 递归调用发现循环
                }
            } else if (!v.equals(p)) {
                // 如果邻接点v已被访问,且v不是u的父节点,则发现回边,存在循环
                return true;
            }
        }
        return false; // 当前路径中未发现循环
    }
}

2.4 注意事项

  • 处理非连通图: 外层循环 for (String node : allNodes) 是必要的,以确保即使图包含多个不连通的组件,也能全面检测。
  • 父节点检查: !v.equals(p) 是避免将DFS路径中直接相邻的父子边误判为循环的关键。在无向图中,边 (u, v) 意味着 v 是 u 的邻居,同时 u 也是 v 的邻居。如果 v 是 u 的父节点,那么 (u, v) 只是DFS树中的一条正常边,不是循环。

3. 方法二:基于并查集(Union-Find)的循环检测

并查集(Disjoint Set Union, DSU)是一种用于处理不相交集合的数据结构,它能够高效地执行“查找”(Find)和“合并”(Union)操作。在无向图中,并查集特别适用于检测循环。

3.1 核心思想:集合合并与查找

并查集将图中的每个顶点视为一个独立的集合。当我们遍历图中的每条边 (u, v) 时:

  • 如果 u 和 v 已经在同一个集合中(即它们的代表元素相同),那么添加这条边 (u, v) 将会形成一个循环。
  • 如果 u 和 v 不在同一个集合中,则将它们所在的集合合并。

3.2 算法原理

  1. 初始化:
    • 为图中的每个顶点创建一个独立的集合,即每个顶点的父节点指向自身。
    • 可选地,维护一个 rank 或 size 数组/映射来优化合并操作(按秩合并或按大小合并),以及路径压缩来优化查找操作。
  2. 遍历边:
    • 遍历图中的所有边 (u, v)。对于无向图,每条边通常只处理一次(例如,如果边 (a, b) 已处理,则不再处理 (b, a))。
    • 对于每条边 (u, v):
      • 使用 find(u) 查找 u 的代表元素(根节点)。
      • 使用 find(v) 查找 v 的代表元素(根节点)。
      • 如果 find(u) 等于 find(v),说明 u 和 v 已经通过其他路径连通,现在再添加边 (u, v) 就会形成一个循环,返回 true。
      • 如果 find(u) 不等于 find(v),说明 u 和 v 属于不同的连通分量,此时执行 union(u, v) 操作,将这两个连通分量合并。
  3. 返回: 如果所有边都被处理完毕,没有发现任何循环,则返回 false。

3.3 示例代码(Java-like Pseudocode)

import java.util.*;

class UnionFind {
    private Map<String, String> parent; // 存储每个元素的父节点
    private Map<String, Integer> rank;   // 存储每个集合的秩(用于按秩合并优化)

    public UnionFind(Set<String> nodes) {
        parent = new HashMap<>();
        rank = new HashMap<>();
        for (String node : nodes) {
            parent.put(node, node); // 初始时每个节点的父节点是自己
            rank.put(node, 0);      // 初始秩为0
        }
    }

    /**
     * 查找元素i的代表元素(根节点),并进行路径压缩。
     * @param i 要查找的元素
     * @return 元素i的代表元素
     */
    public String find(String i) {
        if (!parent.containsKey(i)) {
            // 如果节点不在并查集中,可以根据需求处理,例如抛出异常或返回自身
            parent.put(i, i);
            rank.put(i, 0);
            return i;
        }
        if (parent.get(i).equals(i)) {
            return i;
        }
        String root = find(parent.get(i)); // 递归查找根
        parent.put(i, root); // 路径压缩
        return root;
    }

    /**
     * 合并元素i和j所在的集合。
     * @param i 元素i
     * @param j 元素j
     * @return 如果成功合并(未形成循环)返回 true,否则返回 false(已在同一集合,形成循环)。
     */
    public boolean union(String i, String j) {
        String rootI = find(i);
        String rootJ = find(j);

        if (!rootI.equals(rootJ)) { // 如果不在同一个集合
            // 按秩合并优化:将秩较小的树连接到秩较大的树的根上
            if (rank.get(rootI) < rank.get(rootJ)) {
                parent.put(rootI, rootJ);
            } else if (rank.get(rootI) > rank.get(rootJ)) {
                parent.put(rootJ, rootI);
            } else { // 秩相等时,任意连接,并将新根的秩加1
                parent.put(rootJ, rootI);
                rank.put(rootI, rank.get(rootI) + 1);
            }
            return true; // 合并成功,未形成循环
        }
        return false; // 已经在同一个集合,添加这条边会形成循环
    }
}

class GraphUnionFind {
    private Map<String, List<String>> adj; // 邻接列表表示图

    public GraphUnionFind(Map<String, List<String>> adjacencyList) {
        this.adj = adjacencyList;
    }

    /**
     * 检测无向图中是否存在循环。
     * @return 如果存在循环返回 true,否则返回 false。
     */
    public boolean hasCycle() {
        // 收集所有节点,包括那些没有邻居的节点
        Set<String> allNodes = new HashSet<>(adj.keySet());
        for (List<String> neighbors : adj.values()) {
            allNodes.addAll(neighbors);
        }

        UnionFind uf = new UnionFind(allNodes);

        // 遍历所有边。对于无向图,每条边 (u,v) 会在邻接列表中出现两次
        // 我们需要确保每条边只被处理一次,以避免重复判断或错误判断
        Set<String> processedEdges = new HashSet<>(); // 用于记录已处理的边对,例如 "a-b"
        for (String u : adj.keySet()) {
            for (String v : adj.getOrDefault(u, Collections.emptyList())) {
                // 确保只处理一次边 (u,v),例如通过规范化边表示或跳过已处理的逆向边
                String edge

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
edge是什么浏览器
edge是什么浏览器

Edge是一款由Microsoft开发的网页浏览器,是Windows 10操作系统中默认的浏览器,其目标是提供更快、更安全、更现代化的浏览器体验。本专题为大家提供edge浏览器相关的文章、下载、课程内容,供大家免费下载体验。

1726

2023.08.21

IE浏览器自动跳转EDGE如何恢复
IE浏览器自动跳转EDGE如何恢复

ie浏览器自动跳转edge的解决办法:1、更改默认浏览器设置;2、阻止edge浏览器的自动跳转;3、更改超链接的默认打开方式;4、禁用“快速网页查看器”;5、卸载edge浏览器;6、检查第三方插件或应用程序等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

397

2024.03.05

如何解决Edge打开但没有标题的问题
如何解决Edge打开但没有标题的问题

若 Microsoft Edge 浏览器打开后无标题(窗口空白或标题栏缺失),可尝试以下方法解决: 重启 Edge:关闭所有窗口,重新启动浏览器。 重置窗口布局:右击任务栏 Edge 图标 → 选择「最大化」或「还原」。 禁用扩展:进入 edge://extensions 临时关闭插件测试。 重置浏览器设置:前往 edge://settings/reset 恢复默认配置。 更新或重装 Edge:检查最新版本,或通过控制面板修复

1038

2025.04.24

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1010

2023.08.02

c语言union的用法
c语言union的用法

c语言union的用法是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型,union的使用可以帮助我们节省内存空间,并且可以方便地在不同的数据类型之间进行转换。使用union时需要注意对应的成员是有效的,并且只能同时访问一个成员。本专题为大家提供union相关的文章、下载、课程内容,供大家免费下载体验。

129

2023.09.27

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

548

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

30

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

44

2026.01.06

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
10分钟--Midjourney创作自己的漫画
10分钟--Midjourney创作自己的漫画

共1课时 | 0.1万人学习

Midjourney 关键词系列整合
Midjourney 关键词系列整合

共13课时 | 0.9万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号