【高阶数据结构】第三弹---图的存储与遍历详解:邻接表构建与邻接矩阵的BFS/DFS实现

【高阶数据结构】第三弹---图的存储与遍历详解:邻接表构建与邻接矩阵的BFS/DFS实现

✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【高阶数据结构】

目录

1、图的存储结构

1.1、邻接表

1.1.1、边的结构

1.1.2、图的基本结构

1.1.3、图的创建

1.1.4、获取顶点下标

1.1.5、添加边

1.1.6、打印

1.1.7、测试一

1.1.8、测试二

2、图的遍历

2.1、图的广度优先遍历

2.2、图的深度优先遍历 


1、图的存储结构

1.1、邻接表

邻接表使用数组表示顶点的集合,使用链表表示边的关系

1. 无向图邻接表存储 

注意无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目即可。 

2. 有向图邻接表存储

注意:有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链表,看有多少边顶点的dst取值是i。 

1.1.1、边的结构

1、邻接表使用链表来存储边之间的关系,因此成员需要next指针,除此之外还需要起始点(此处可以省略,因为使用目标点即可实现图)目标点权值



2、此处还需要实现一个有参构造函数参数包括目标点下标和权值

3、权值需要使用类型模板参数
template<class W> struct Edge { // int _srci; // 起始点,入边使用 int _dsti; // 目标点下标,出边使用 W _w; // 权值 Edge<W>* _next; Edge(size_t dsti, const W& w) :_dsti(dsti) ,_w(w) ,_next(nullptr) {} };

1.1.2、图的基本结构

1、使用邻接表存储图的基本结构与使用邻接矩阵存储图的基本结构类似,邻接表同样的使用模板实现,但是无需表示无穷大的非类型模板参数,因为此处为链表结构,添加边的时候才需要new新的结点

2、成员变量包括顶点集合,顶点与下标的映射关系和领接表
template<class V, class W, bool Direction = false> class Graph { typedef Edge<W> Edge; public: // ... private: vector<V> _vertexs; // 顶点集合 map<V, int> _vIndexMap; // 顶点映射下标 vector<Edge*> _tables; // 邻接表 };

1.1.3、图的创建

图的创建与邻接矩阵一样,使用手动添加边的方式创建,即实现一个有参构造(参数包含定带你集合以及顶点个数)
Graph(const V* a, size_t n) { _vertexs.reserve(n); for (size_t i = 0; i < n; i++) { _vertexs.push_back(a[i]); _vIndexMap[a[i]] = i; } _tables.resize(n, nullptr); }

注意:此处的邻接表只需开辟一层空间,因为当添加边的时候,我们会new新的结点。

1.1.4、获取顶点下标

顶点与下标的映射存储在map中,获取顶点对应的下标只需要在map中进行查找即可,找到则返回下标,没找到则抛异常(为了编译通过,返回-1)。
// 根据顶点获取下标 size_t GetVertexIndex(const V& v) { auto it = _vIndexMap.find(v); if (it != _vIndexMap.end()) { return it->second; } else { throw invalid_argument("顶点不存在"); return -1; } }

1.1.5、添加边

1、参数为起始顶点,结束顶点和权值,还需注意有向与无向图

2、添加边的思想先获取顶点对应的下标,然后将新的结点头插到邻接表中,如果是无向图则需双向存储
void AddEdge(const V& src, const V& dst, const W& w) { size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); // 1 -> 2 Edge* eg = new Edge(dsti, w); // 将Edge头插到邻接表 eg->_next = _tables[srci]; _tables[srci] = eg; // 无向图 2 -> 1 if (Direction == false) { Edge* eg = new Edge(srci, w); // 将Edge头插到邻接表 eg->_next = _tables[dsti]; _tables[dsti] = eg; } }

1.1.6、打印

打印的方式可以根据自己需求打印,此处打印下标与顶点的映射关系和邻接表,为了让打印更美观,将邻接表打印成   顶点[下标]->[顶点,下标,权值]...  的形式
void Print() { // 顶点 下标 -> 顶点值 for (size_t i = 0; i < _vertexs.size(); i++) { cout << "[" << i << "]->" << _vertexs[i] << endl; } cout << endl; // 邻接表 for (size_t i = 0; i < _tables.size(); i++) { // 顶点[下标]->[顶点,下标,权值]... cout << _vertexs[i] << "[" << i << "]->"; Edge* cur = _tables[i]; while (cur) { cout << "[" << _vertexs[cur->_dsti] << "," << cur->_dsti << "," << cur->_w << "]->"; cur = cur->_next; } cout << "nullptr" << endl; } }

1.1.7、测试一

顶点值为char类型。
void TestGraph1() { Graph<char, int, true> g("0123", 4); g.AddEdge('0', '1', 1); g.AddEdge('0', '3', 4); g.AddEdge('1', '3', 2); g.AddEdge('1', '2', 9); g.AddEdge('2', '3', 8); g.AddEdge('2', '1', 5); g.AddEdge('2', '0', 3); g.AddEdge('3', '2', 6); g.Print(); }

构造函数 

添加边

打印结果

1.1.8、测试二

顶点值为string类型。

测试代码 

void TestGraph2() { string a[] = { "张三", "李四", "王五", "赵六" }; Graph<string, int, true> g1(a, 4); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 30); g1.Print(); }

打印结果 

2、图的遍历

注意:此处的遍历操作都是基于邻接矩阵进行遍历的。

给定一个图G和其中任意一个顶点v0,从v0出发,沿着图中各边访问图中的所有顶点,且每个顶点仅被遍历一次"遍历"即对结点进行某种操作的意思

2.1、图的广度优先遍历

如何防止节点被重复遍历?

我们可以创建一个标记容器成员类型为bool类型,访问过则将值设置为true,默认为false。 

广度优先遍历思想:

与二叉树的层序遍历类似,需要借助队列先将第一个值入队列,出队列的同时把邻接顶点入队,直到队列为空则遍历结束,具体步骤如下:1、获取顶点对应的下标2、创建队列和标记容器3、入队4、队列不为空则遍历4.1、打印队头4.2、将front的邻接顶点入队有效边且标志位为false则入队

遍历代码 

// 根据顶点进行广度优先遍历,使用队列+标记容器 void BFS(const V& src) { // 1.获取顶点对应的下标 size_t srci = GetVertexIndex(src); // 2.创建队列和标记容器 queue<int> q; vector<bool> visited(_vertexs.size(),false); // 默认false可以访问 // 3.入队 q.push(srci); visited[srci] = true; // 不能访问 size_t n = _vertexs.size(); // 4.队列不为空则遍历 while (!q.empty()) { // 打印队头 int front = q.front(); q.pop(); cout << front << ":" << _vertexs[front] << endl; // 将front的邻接顶点入队 for (size_t i = 0; i < n; i++) { // 有效边且标志位为false则入队 if (_matrix[front][i] != MAX_W && !visited[i]) { q.push(i); visited[i] = true; } } } cout << endl; }

测试代码

void TestBDFS() { string a[] = { "张三", "李四", "王五", "赵六", "周七" }; Graph<string, int> g1(a, sizeof(a) / sizeof(string)); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 30); g1.AddEdge("王五", "周七", 30); g1.Print(); g1.BFS("张三"); }

结构及打印结果

遍历结果

上面确实实现了广度优先遍历,但是能不能像二叉树的层序遍历一样,打印每一层呢?

可以的。

与二叉树打印每一层类型,我们可以创建一个记录队列元素个数的变量,一次打印队列元素个数个,打印完一层之后更新这个值

一层层打印 

// 广度优先遍历(一层层打印) void BFS(const V& src) { // 1.获取顶点对应的下标 size_t srci = GetVertexIndex(src); // 2.创建队列和标记容器 queue<int> q; vector<bool> visited(_vertexs.size(), false); // 默认false可以访问 // 3.入队 q.push(srci); visited[srci] = true; // 不能访问 int levelSize = 1; size_t n = _vertexs.size(); // 4.队列不为空则遍历 while (!q.empty()) { // 一层层打印,一次打印队列个数个 for (int i = 0; i < levelSize; i++) { // 打印队头 int front = q.front(); q.pop(); cout << front << ":" << _vertexs[front] << endl; // 将front的邻接顶点入队 for (size_t i = 0; i < n; i++) { // 有效边且标志位为false则入队 if (_matrix[front][i] != MAX_W && !visited[i]) { q.push(i); visited[i] = true; } } } cout << endl; levelSize = q.size(); } }

遍历结果 

2.2、图的深度优先遍历 

深度优先遍历思想: 

与二叉树的前序遍历类似,但是此处需要使用标记容器,因此需要实现一个子函数具体步骤如下:1、获取顶点对应的下标2、创建标记容器3、调用子函数

子函数思想:1、打印该位置的值并将该位置的标记位设置为true(不能再访问)2、找一个该位置相邻的且没有访问过的点深度遍历

代码实现 

void _DFS(size_t srci, vector<bool>& visited) { // 下标:顶点值 cout << srci << ":" << _vertexs[srci] << endl; visited[srci] = true; // 不能再访问 // 找一个scri相邻的且没有访问过的点,深度遍历 for (size_t i = 0; i < _vertexs.size(); i++) { if (_matrix[srci][i] != MAX_W && !visited[i]) { _DFS(i, visited); } } } void DFS(const V& src) { size_t srci = GetVertexIndex(src); vector<bool> visited(_vertexs.size(), false); _DFS(srci, visited); }

运行结果

Read more

【高级终端Termux】在安卓手机/平板上使用Termux 搭建 Debian 环境并运行 PC 级 Linux 应用教程(含安装WPS,VS Code)

【高级终端Termux】在安卓手机/平板上使用Termux 搭建 Debian 环境并运行 PC 级 Linux 应用教程(含安装WPS,VS Code)

Termux 搭建 Debian 环境并运行 PC 级 Linux 应用教程 一、前言 1. 背景 众所周知,最新搭载澎湃OS和鸿蒙OS的平板都内置了PC级WPS,办公效率直接拉满(板子终于从“泡面盖”升级为“生产力”了)。但问题来了:如果不是这两个系统,难道我们只能继续用平板盖泡面吗?当然不是!折腾了很长时间后,总算找到了一个好玩的东西:高级终端Termux 。现在,不仅能随时随地用WPS改文档,还能VSCode优雅地敲代码,再也不用背着电脑乱跑了。 由于每次搭建环境时都要去不同的平台找不同功能,有时还找不到,所以我决定自己写一篇博客,方便自己以后再搭建时直接“Ctrl C + Ctrl V”,顺便分享给有同样需求的小伙伴们。话不多说,直接开整! 2. 准备工作 * 一部安卓手机:性能越好,折腾起来越顺畅。 * Termux 应用: 不想去F-droid下载的看过来

By Ne0inhk
Flutter for OpenHarmony:darq 让 Dart 拥有 C# LINQ 般的超能力(集合查询与变换神器) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:darq 让 Dart 拥有 C# LINQ 般的超能力(集合查询与变换神器) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter for OpenHarmony:darq 让 Dart 拥有 C# LINQ 般的超能力(集合查询与变换神器) 深度解析与鸿蒙适配指南 前言 如果你是从 .NET/C# 转到 Dart/Flutter 开发的,你一定无比怀念 LINQ (Language Integrated Query)。 虽然 Dart 的 Iterable 提供了 map, where, reduce 等基础方法,但在处理复杂数据集合(如多重排序、连接 Join、分组 GroupBy、去重)时,原生 API 依然显得力不从心,

By Ne0inhk

Flutter 三方库 http_status_code 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、工业级的网络响应审计与 HTTP 状态码语义化控制引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 http_status_code 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、工业级的网络响应审计与 HTTP 状态码语义化控制引擎 在鸿蒙(OpenHarmony)系统的端云一体化网络库封装、政企级应用的网络错误诊断、或者是针对复杂的 REST API 全生命周期监听中,如何摆脱凌乱的 magic number(如 404, 500),转而使用具备自描述性、且完全符合 RFC 规范的语义化常量?http_status_code 为开发者提供了一套工业级的、基于标准定义的 HTTP 状态码枚举与描述查询方案。本文将深入实战其在鸿蒙网络安全架构中的应用。 前言 什么是 HTTP Status Code?它是 Web

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 hashlib 为鸿蒙应用提供军用级加密哈希算法支持(安全数据完整性卫士)

Flutter for OpenHarmony: Flutter 三方库 hashlib 为鸿蒙应用提供军用级加密哈希算法支持(安全数据完整性卫士)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在 OpenHarmony 应用开发中,涉及到本地存储加密、用户密码脱敏、大文件完整性校验或区块链应用时,一套算法完备、执行高效的哈希(Hash)库是必不可少的。虽然 Dart 原生也提供了一些简单的加密方法,但在算法多样性(如 Argon2、SHA-3, Blake2b 等高级算法)和性能表现方面,往往无法满足高安全等级项目的需求。 hashlib 正是专门为高性能数据加解密与完整性校验打造的库。它纯代码实现且经过了极致的循环优化,是鸿蒙平台上保护敏感数据的数字堡垒。 一、核心加密算法矩阵 hashlib 支持极其广泛且先进的加密标准。 原始明文数据 hashlib 算法引擎 传统算法 (MD5 / SHA-256) 现代算法 (SHA-3 / Keccak) 极致速度 (Blake2b / Blake2s) 密钥派生 (Argon2 / Scrypt)

By Ne0inhk