​​​【收录 Hello 算法】9.3 图的遍历

news2024/10/13 21:37:03

目录

9.3   图的遍历

9.3.1   广度优先遍历

1.   算法实现

2.   复杂度分析

9.3.2   深度优先遍历

1.   算法实现

2.   复杂度分析


9.3   图的遍历

树代表的是“一对多”的关系,而图则具有更高的自由度,可以表示任意的“多对多”关系。因此,我们可以把树看作图的一种特例。显然,树的遍历操作也是图的遍历操作的一种特例

图和树都需要应用搜索算法来实现遍历操作。图的遍历方式也可分为两种:广度优先遍历深度优先遍历

9.3.1   广度优先遍历

广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外扩张。如图 9-9 所示,从左上角顶点出发,首先遍历该顶点的所有邻接顶点,然后遍历下一个顶点的所有邻接顶点,以此类推,直至所有顶点访问完毕。

图的广度优先遍历

图 9-9   图的广度优先遍历

1.   算法实现

BFS 通常借助队列来实现,代码如下所示。队列具有“先入先出”的性质,这与 BFS 的“由近及远”的思想异曲同工。

  1. 将遍历起始顶点 startVet 加入队列,并开启循环。
  2. 在循环的每轮迭代中,弹出队首顶点并记录访问,然后将该顶点的所有邻接顶点加入到队列尾部。
  3. 循环步骤 2. ,直到所有顶点被访问完毕后结束。

为了防止重复遍历顶点,我们需要借助一个哈希集合 visited 来记录哪些节点已被访问。

Tip

哈希集合可以看作一个只存储 key 而不存储 value 的哈希表,它可以在 𝑂(1) 时间复杂度下进行 key 的增删查改操作。根据 key 的唯一性,哈希集合通常用于数据去重等场景。

graph_bfs.c

/* 节点队列结构体 */
typedef struct {
    Vertex *vertices[MAX_SIZE];
    int front, rear, size;
} Queue;

/* 构造函数 */
Queue *newQueue() {
    Queue *q = (Queue *)malloc(sizeof(Queue));
    q->front = q->rear = q->size = 0;
    return q;
}

/* 判断队列是否为空 */
int isEmpty(Queue *q) {
    return q->size == 0;
}

/* 入队操作 */
void enqueue(Queue *q, Vertex *vet) {
    q->vertices[q->rear] = vet;
    q->rear = (q->rear + 1) % MAX_SIZE;
    q->size++;
}

/* 出队操作 */
Vertex *dequeue(Queue *q) {
    Vertex *vet = q->vertices[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    q->size--;
    return vet;
}

/* 检查顶点是否已被访问 */
int isVisited(Vertex **visited, int size, Vertex *vet) {
    // 遍历查找节点,使用 O(n) 时间
    for (int i = 0; i < size; i++) {
        if (visited[i] == vet)
            return 1;
    }
    return 0;
}

/* 广度优先遍历 */
// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
void graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {
    // 队列用于实现 BFS
    Queue *queue = newQueue();
    enqueue(queue, startVet);
    visited[(*visitedSize)++] = startVet;
    // 以顶点 vet 为起点,循环直至访问完所有顶点
    while (!isEmpty(queue)) {
        Vertex *vet = dequeue(queue); // 队首顶点出队
        res[(*resSize)++] = vet;      // 记录访问顶点
        // 遍历该顶点的所有邻接顶点
        AdjListNode *node = findNode(graph, vet);
        while (node != NULL) {
            // 跳过已被访问的顶点
            if (!isVisited(visited, *visitedSize, node->vertex)) {
                enqueue(queue, node->vertex);             // 只入队未访问的顶点
                visited[(*visitedSize)++] = node->vertex; // 标记该顶点已被访问
            }
            node = node->next;
        }
    }
    // 释放内存
    free(queue);
}

代码相对抽象,建议对照图 9-10 来加深理解。

<1><2><3><4><5><6><7><8><9><10><11>

graph_bfs_step4

图 9-10   图的广度优先遍历步骤

广度优先遍历的序列是否唯一?

不唯一。广度优先遍历只要求按“由近及远”的顺序遍历,而多个相同距离的顶点的遍历顺序允许被任意打乱。以图 9-10 为例,顶点 1、3 的访问顺序可以交换,顶点 2、4、6 的访问顺序也可以任意交换。

2.   复杂度分析

时间复杂度:所有顶点都会入队并出队一次,使用 𝑂(|𝑉|) 时间;在遍历邻接顶点的过程中,由于是无向图,因此所有边都会被访问 2 次,使用 𝑂(2|𝐸|) 时间;总体使用 𝑂(|𝑉|+|𝐸|) 时间。

空间复杂度:列表 res ,哈希集合 visited ,队列 que 中的顶点数量最多为 |𝑉| ,使用 𝑂(|𝑉|) 空间。

9.3.2   深度优先遍历

深度优先遍历是一种优先走到底、无路可走再回头的遍历方式。如图 9-11 所示,从左上角顶点出发,访问当前顶点的某个邻接顶点,直到走到尽头时返回,再继续走到尽头并返回,以此类推,直至所有顶点遍历完成。

图的深度优先遍历

图 9-11   图的深度优先遍历

1.   算法实现

这种“走到尽头再返回”的算法范式通常基于递归来实现。与广度优先遍历类似,在深度优先遍历中,我们也需要借助一个哈希集合 visited 来记录已被访问的顶点,以避免重复访问顶点。

graph_dfs.c

/* 检查顶点是否已被访问 */
int isVisited(Vertex **res, int size, Vertex *vet) {
    // 遍历查找节点,使用 O(n) 时间
    for (int i = 0; i < size; i++) {
        if (res[i] == vet) {
            return 1;
        }
    }
    return 0;
}

/* 深度优先遍历辅助函数 */
void dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {
    // 记录访问顶点
    res[(*resSize)++] = vet;
    // 遍历该顶点的所有邻接顶点
    AdjListNode *node = findNode(graph, vet);
    while (node != NULL) {
        // 跳过已被访问的顶点
        if (!isVisited(res, *resSize, node->vertex)) {
            // 递归访问邻接顶点
            dfs(graph, res, resSize, node->vertex);
        }
        node = node->next;
    }
}

/* 深度优先遍历 */
// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
void graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {
    dfs(graph, res, resSize, startVet);
}

深度优先遍历的算法流程如图 9-12 所示。

  • 直虚线代表向下递推,表示开启了一个新的递归方法来访问新顶点。
  • 曲虚线代表向上回溯,表示此递归方法已经返回,回溯到了开启此方法的位置。

为了加深理解,建议将图 9-12 与代码结合起来,在脑中模拟(或者用笔画下来)整个 DFS 过程,包括每个递归方法何时开启、何时返回。

<1><2><3><4><5><6><7><8><9><10><11>

graph_dfs_step4

图 9-12   图的深度优先遍历步骤

深度优先遍历的序列是否唯一?

与广度优先遍历类似,深度优先遍历序列的顺序也不是唯一的。给定某顶点,先往哪个方向探索都可以,即邻接顶点的顺序可以任意打乱,都是深度优先遍历。

以树的遍历为例,“根 → 左 → 右”“左 → 根 → 右”“左 → 右 → 根”分别对应前序、中序、后序遍历,它们展示了三种遍历优先级,然而这三者都属于深度优先遍历。

2.   复杂度分析

时间复杂度:所有顶点都会被访问 1 次,使用 𝑂(|𝑉|) 时间;所有边都会被访问 2 次,使用 𝑂(2|𝐸|) 时间;总体使用 𝑂(|𝑉|+|𝐸|) 时间。

空间复杂度:列表 res ,哈希集合 visited 顶点数量最多为 |𝑉| ,递归深度最大为 |𝑉| ,因此使用 𝑂(|𝑉|) 空间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1685175.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++三剑客之std::any(二) : 源码剖析

目录 1.引言 2.std::any的存储分析 3._Any_big_RTTI与_Any_small_RTTI 4.std::any的构造函数 4.1.从std::any构造 4.2.可变参数模板构造函数 4.3.赋值构造与emplace函数 5.reset函数 6._Cast函数 7.make_any模版函数 8.std::any_cast函数 9.总结 1.引言 C三剑客之s…

开源与闭源AI模型的对决:数据隐私、商业应用与社区参与

引言 在人工智能&#xff08;AI&#xff09;领域&#xff0c;模型的发展路径主要分为“开源”和“闭源”两条。这两种模型在数据隐私保护、商业应用以及社区参与与合作方面各有优劣&#xff0c;是创业公司、技术巨头和开发者们必须仔细权衡的重要选择。那么&#xff0c;面对这些…

[面试题]软件测试性能测试的常见指标在Linux系统中,一个文件的访问权限是 755,其含义是什么

1、选出属于黑盒测试方法的选项&#xff08;ABC&#xff09; A.决策表 B.边界值分析 C.正交法 D.分支覆盖 E.语句覆盖 F.条件覆盖 黑盒测试&#xff08;Black-box testing&#xff09;&#xff0c;又称为功能测试或数据驱动测试&#xff0c; 是一种不涉及软件内部结构和内部特…

优化问题基础知识

目录 线性规划(LP)整数线性规划(ILP)混合整数线性规划(MILP)非线性规划(NLP)整数非线性规划(INLP)混合整数非线性规划(MINLP)分组背包问题&#xff08;MCKP&#xff09;启发式算法在线算法长期优化李雅普诺夫算法随机优化块坐标下降法&#xff08;Block Coordinate Descent&…

当前API面临的安全风险,有什么安全措施

在当今信息化高速发展的时代&#xff0c;API&#xff08;应用程序编程接口&#xff09;技术已成为企业数字化转型的基石&#xff0c;它连接着各种服务、传输数据并控制系统&#xff0c;成为现代数字业务环境不可或缺的一部分。然而&#xff0c;随着API的广泛应用&#xff0c;其…

Vue3+ts(day07:pinia)

学习源码可以看我的个人前端学习笔记 (github.com):qdxzw/frontlearningNotes 觉得有帮助的同学&#xff0c;可以点心心支持一下哈&#xff08;笔记是根据b站上学习的尚硅谷的前端视频【张天禹老师】&#xff0c;记录一下学习笔记&#xff0c;用于自己复盘&#xff0c;有需要学…

一文读懂RDMA: Remote Direct Memory Access(远程直接内存访问)

目录 ​编辑 引言 一、RDMA的基本原理 二、RDMA的主要特点 三、RDMA的编程接口 四、RDMA的代码演示 服务器端代码&#xff1a; 客户端代码&#xff1a; 五、总结 引言 RDMA&#xff0c;全称Remote Direct Memory Access&#xff0c;即远程直接内存访问&#xff0c;是…

海山数据库(He3DB)数据仓库发展历史与架构演进:(一)传统数仓

从1990年代Bill Inmon提出数据仓库概念后经过四十多的发展&#xff0c;经历了早期的PC时代、互联网时代、移动互联网时代再到当前的云计算时代&#xff0c;但是数据仓库的构建目标基本没有变化&#xff0c;都是为了支持企业或者用户的决策分析&#xff0c;包括运营报表、企业营…

【Qt 学习笔记】Qt常用控件 | 布局管理器 | 表单布局Form Layout

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 布局管理器 | 表单布局Form Layout 文章编号&#xff1a…

黑马点评3——优惠券秒杀

&#x1f308;hello&#xff0c;你好鸭&#xff0c;我是Ethan&#xff0c;一名不断学习的码农&#xff0c;很高兴你能来阅读。 ✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。 &#x1f3c3;人生之义&#xff0c;在于追求&#xff0c;不在成败&#xff0c;勤通…

2024最新流媒体在线音乐系统网站源码| 音乐社区 | 多语言 | 开心版

简介&#xff1a; 2024最新流媒体在线音乐系统网站源码| 音乐社区 | 多语言 | 开心版 下载地址 https://www.kuaiyuanya.com/product/article/index/id/33.html 图片&#xff1a;

Pytorch DDP分布式细节分享

自动微分和autograde 自动微分 机器学习/深度学习关键部分之一&#xff1a;反向传播&#xff0c;通过计算微分更新参数值。 自动微分的精髓在于它发现了微分计算的本质&#xff1a;微分计算就是一系列有限的可微算子的组合。 自动微分以链式法则为基础&#xff0c;依据运算逻…

笔记-Apriori算法介绍(Python实现)

1.Apriori算法简介 Apriori算法是经典的挖掘频繁项集和关联规则的数据挖掘算法。A priori在拉丁语中指"来自以前"。当定义问题时&#xff0c;通常会使用先验知识或者假设&#xff0c;这被称作"一个先验"&#xff08;a priori&#xff09;。Apriori算法的名…

回溯算法05(leetcode491/46/47)

参考资料&#xff1a; https://programmercarl.com/0491.%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97.html 491. 非递减子序列 题目描述&#xff1a; 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列&#xff0c;递增子序列中 至少有两个元素…

基于python的k-means聚类分析算法,对文本、数据等进行聚类,有轮廓系数和手肘法检验

K-means算法是一种常见的聚类算法&#xff0c;用于将数据点分成不同的组&#xff08;簇&#xff09;&#xff0c;使同一组内的数据点彼此相似&#xff0c;不同组之间的数据点相对较远。以下是K-means算法的基本工作原理和步骤&#xff1a; 工作原理&#xff1a; 初始化&#x…

Java面试八股之start()和run()的区别

start()和run()的区别 在Java中&#xff0c;run()方法和start()方法是与线程操作紧密相关的&#xff0c;两者之间存在本质的区别&#xff1a; start()是Thread类的一个实例方法&#xff0c;它的主要作用是启动一个新的线程。当调用线程对象的start()方法时&#xff0c;Java虚…

教师专属的成绩发布小程序

还在为成绩发布而烦恼&#xff1f;还在担心家长无法及时获得孩子的学习反馈&#xff1f;是否想要一个既安全又高效的工具来简化你的教学工作&#xff1f;那么&#xff0c;易查分小程序可能是你一直在寻找的答案。 现在的老师们有了超多的工具来帮助我们减轻负担&#xff0c;提高…

数据结构之二叉树的超详细讲解(2)--(堆的概念和结构的实现,堆排序和堆排序的应用)

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 数据结构之二叉树的超详细讲解(2)--(堆的概念和结构的实现,堆排序和堆排序的应用) 收录于专栏【数据结构初阶】 本专栏旨在分享学习数据结构学习的一点学习笔记…

python从0开始学习(十二)

目录 前言 1、字符串的常用操作 2、字符串的格式化 2.1 格式化字符串的详细格式&#xff08;针对format形式&#xff09; ​编辑 总结 前言 上一篇文章我们讲解了两道关于组合数据类型的题目&#xff0c;本篇文章我们将学习新的章节&#xff0c;学习字符串及正则表达式。 …

Gradle和Maven项目解决Spring Boot Configuration Annotation Processor not configured警告

问题描述 写了一个配置类,加了注解@ConfigurationProperties(prefix = “xxx”) 后一直报警告:Spring Boot Configuration Annotation Processor not configured 意思是 Spring boot 未配置注解处理器 解决过程 出现这个问题后,百度查了解决方式 1.maven项目 maven项目是…