当前位置: 首页 > news >正文

【高阶数据结构】第三弹---图的存储与遍历详解:邻接表构建与邻接矩阵的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 -> 1if (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);
}

运行结果

相关文章:

  • PasteForm框架开发之Entity多级嵌套的表单的实现
  • 使用ZYNQ芯片和LVGL框架实现用户高刷新UI设计系列教程(第八讲)
  • jupyter中切换Anaconda虚拟环境
  • 自定义类型之结构体
  • Charles 安装与使用详解:实现 App 与小程序 HTTPS 抓包
  • Linux进程
  • 网络协议TCP/IP、UDP、HTTP/HTTPS 完全指南
  • 数据库学习通期末复习一
  • C# + Python混合开发实战:优势互补构建高效应用
  • Day09【基于Tripletloss实现的简单意图识别对话系统】
  • Android学习总结之git篇
  • 根据pdf文档生成问答并进行评估
  • 集成电路流片随笔10:UART模块tinyriscv 通信协议和RIB接口总线区别
  • 【day5】调用AI接口,生成自动化测试用例
  • JVM 垃圾回收
  • 一个简单的Python文件MCP服务器
  • OpenAI 最新发布的 GPT-4.1 系列在 API 中正式上线
  • Google-Tensorflow-NLP
  • NLP高频面试题(四十三)——什么是人类偏好对齐中的「对齐税」(Alignment Tax)?如何缓解?
  • 什么样的游戏需要用到游戏盾SDK呢?
  • 印巴战火LIVE|巴基斯坦多地遭印度导弹袭击,巴总理称“有权作出适当回应”
  • 纪念|“补白大王”郑逸梅,从藏扇看其眼光品味
  • 郑州一街道被指摊贩混乱经营,12345热线:已整治并加强巡查
  • 恒瑞医药通过港交所上市聆讯,最快或5月挂牌上市
  • 李学明谈笔墨返乡:既耕春圃,念兹乡土
  • 深入景区、文化街区及消费一线,多地省委书记调研文旅市场