文章目录

  • 前言
  • 常见概念总结
  • 图的模拟实现
  • 邻接矩阵和邻接表的优劣
    • 图的模拟实现(邻接表)
  • 广度优先遍历(BFS)
  • 深度优先遍历(DFS)
  • hpp代码展示

前言

在聊图的结构之前,我们可以先从熟悉的地方开始,这有一条结论:树是一种特殊的图,图不一定是树。我们知道树形结构多用于搜索查找,典型的结构:搜索二叉树,红黑树和AVL树。在树的结构中,我们更侧重其存储的数据,你看,查找不就是判断给定的数据是否存储在结构中吗?因此,怎么快速的查找指定的数据就是树形结构的主要侧重问题。而图结构呢?它不侧重你存储了什么数据(因此它较少出现在需要快速查找某一元素的场景中,况且还有一个O(1)的哈希桶结构呢),它更侧重数据之间的关系,数据之间是否有联系?再看树形结构,双亲节点与子节点之间也有联系,这种联系通过一个指针体现,指针就是连接两个节点的“通道”。在图结构中,这样的用来连接的“通道”被强化成了,节点在图结构中被称为顶点,顶点与顶点之间是否有边连接,是否有联系,这是图结构关心的问题。这条边甚至带有一个权值,用来表示顶点之间的某种关系的强弱。所以说,在树形结构中,“边”只是用来完成查找的工具,它是数据的附属,也是必要的,但不是主要的。但在图结构中,边的地位提升,成为结构中的主要部分,一张图需要保存一个顶点的集合(其存储了描述顶点的数据),还需要保存一个边的集合,可能还要表示边的权值。在模拟实现之前,先来认识几个图的概念

常见概念总结

图是由顶点及顶点间的关系构成的一种数据结构,G = (V, E)

顶点和边:节点在图中叫做顶点,连接顶点的是一条边

有向图和无向图:对于有向图,顶点对<x, y>是有序的,是指x->y,与<y, x>不同。对于无向图,顶点对(x, y)是无序的,是指x->y,y->x,与(y, x)相同

完全图:在n个顶点的无向图中,如果有n*(n - 1) / 2条边(即任意两顶点有且只有一条边的情况),则称此图为无向完全图。在n个顶点的有向图中,如果有n*(n - 1)条边(即任意两顶点有且仅有方向相反的两条边),则称此图为有向完全图

邻接顶点:对于无向图,如果边(u, v)是真实存在的一条边,则称u和v互为邻接顶点,边(u, v)依附于顶点u和v。对于有向图,如果<u, v>是真实存在的一条边,则称u 邻接到 v,v邻接自u,边<u, v>与顶点u和顶点v相关联

顶点的度:与树一样,顶点的度是指与顶点相关联的边的条数,对于有向图,顶点的度等于入度(以该顶点为终点的边的条数)+ 出度(以该顶点为起点的边的条数 ),对于无向图,顶点的度就是与其相关联的边的条数

路径:从顶点u出发,有一组边可以达到顶点v,则称这组边是u到v的路径

路径长度:对于无权的图来说,路径长度就是边的条数,对于有权的图来说,路径长度就是各边的权值相加

简单路径和回路:若路径上各顶点不重复,则称该路径是简单路径。如果第一个顶点与最后一个顶点重复,则称该路径是回路

子图:顶点或者边是原图的子集,则称该图是原图的子图

连通图:如果两顶点有路径相连(注意不是被边直接相连),则称两顶点是连通的,如果一张无向图中任意两顶点都是连通的,则称该图是连通图

强连通图:如果一张有向图的每一对顶点u和v,都存在一条从u到v的路径与一条从v到u的路径,则称该有向图是强连通图

生成树:对于无向图,一个连通图的最小连通子图称为该图的生成树。n个顶点的连通图的生成树有n个顶点和n-1条边

下面是一些图的逻辑结构

图的模拟实现

由于图结构侧重顶点之间的关系,所以顶点集合是结构的一个主体,我们用vector数组存储每个顶点的值,并使用模板参数V接收顶点的类型,但是顶点之间的关系要怎么表示呢?这有两种表示方法,一个是邻接矩阵,一个是邻接表。先说邻接矩阵,这是一个二维数组,数组的行和列分别代表一个顶点,由于行和列都是整数,所以它们表示的是顶点抽象后的整数(这一点与并查集很像)。因此我们需要保存每个顶点抽象后的整数,这里用unorder_ map存储<V, size_t>这样的键值对,first成员就是顶点的值,second成员是顶点抽象后的数组下标。顶点被抽象成数组的下标,这步操作使用者是不知道的,这是结构的内部细节,使用者只会传入顶点的值,如果此时需要操作邻接矩阵,我们就要注意顶点与下标之间的转换,需要先通过map表获取顶点的数组下标
接着说邻接矩阵,该二维数组的行和列分别对应了两个顶点,通过行和列就能锁定一个元素,该元素存储的值将表明两顶点之间是否相连,一般这个值是边的权重,如果图没有权重,我们就用某些特定值表示顶点是否相连,比如用1表示两顶点相连,用-1表示两顶点不相连。如果图有权重,我们也需要指定一个特定值,用它表示顶点间没有相连,比如整数的最大值。如果数组中u行v列存储的值不等于所指定的特定值,说明u顶点和v顶点相连。

有了大概的结构,我们来聊一下图的模板参数,首先顶点的值是一个泛型,我们用参数V表示,其次邻接矩阵保存的值是边的权重,权重也是一个泛型,并且还需要接收一个特定值以表示两顶点之间的不相连,最后需要一个bool变量表示该图为无向图还是有向图


然后是操作接口的实现,首先是构造函数,设计两个构造函数,一个是强制编译器生成的默认构造函数,它会调用自定义成员的默认构造,由于自定义成员都实现了默认构造,所以不会有成员为初始化的问题。还有一个构造是,使用者传入一个顶点的集合,我们调用add_tex依次添加顶点,最后再初始化邻接矩阵。

接着是add_tex接口,该接口将接收的顶点值添加到顶点集合vector和映射表unordered_map中,并且被构造函数复用。需要注意的是不要添加相同的顶点值

然后是要进入邻接矩阵前,对顶点进行的抽象整数获取的接口get_index,我们需要用unordered_map中查找该顶点值并返回键值对中其对应的整数值。同样要注意顶点是否存在的判断,不存在返回-1
最后是边的添加,该接口接收两个顶点值,以及连接两顶点的边的权值。首先也是要判断两顶点是否存在:通过get_index达到顶点的下标,判断两下标中是否存在-1,如果存在-1说明有顶点不存在,需要抛异常。接着是邻接矩阵的进入,通过获取的两个下标,锁定邻接矩阵的一个元素(在此之前判断邻接矩阵是否有足够的空间,因为默认构造函数不会为邻接矩阵开辟足够的空间),将它的值修改为接收到的权值。此时还要注意图是否是无向图,如果是,我们还需要修改对称的元素,比如(u, v)和(v, u)两个元素都要修改

#pragma once#include <iostream>
#include <vector>
#include <unordered_map>using namespace std;template <class V, class W, W MAX_W, bool Direction = false> // 默认无向图
class graph
{public:graph() = default;graph(const V* arr, size_t n){_vertex.reserve(n);for (size_t i = 0; i < n; ++i){add_tex(arr[i]);              // 将数组中的顶点依次添加到顶点集合和映射map中}// 对邻接矩阵的初始化,默认顶点间不相连_matrix.resize(_vertex.size());for (size_t i = 0; i < _vertex.size(); ++i){_matrix[i].resize(_vertex.size(), MAX_W); }}// 添加顶点的接口void add_tex(const V& v){auto ret = _index_map.find(v);if (ret != _index_map.end())      // 如果顶点不存在则添加,否则抛异常{throw invalid_argument("顶点重复");}_index_map[v] = _vertex.size();  // 建立顶点与数组下标之间的映射_vertex.push_back(v);            // 将顶点添加到顶点集合}// 查找顶点在数组中的下标,如果找不到返回-1size_t get_index(const V& v)         {auto ret = _index_map.find(v);if (ret == _index_map.end()){return -1;}return ret->second;}// 边的添加void add_edge(const V& src, const V& det, const W& w){// 需要进入邻接表,将顶点转换成下标size_t src_index = get_index(src);size_t det_index = get_index(det);// 顶点存在的判断if (src_index == -1 || det_index == -1){throw invalid_argument("顶点不存在");}// 检查邻接矩阵是否初始化,因为默认构造函数并不会初始化矩阵if (_matrix.size() != _vertex.size())   {_matrix.resize(_vertex.size());   for (size_t i = 0; i < _matrix.size(); ++i){// 用最大值初始化矩阵_matrix[i].resize(_vertex.size(), MAX_W);  }}_matrix[src_index][det_index] = w;// 如果是无向图,镜像也要添加边if (Direction == false)                         {_matrix[det_index][src_index] = w;}}// for test,邻接矩阵的打印void print(){for (size_t i = 0; i < _matrix.size(); ++i){for (size_t j = 0; j < _matrix.size(); ++j){if (_matrix[i][j] == INT_MAX)cout << "* ";elsecout << _matrix[i][j] << ' ';}cout << endl;}}
private:vector<V> _vertex;                         //保存顶点的集合unordered_map<V, size_t> _index_map;     // 保存顶点与数组下标之间的转化vector<vector<W>> _matrix;               // 邻接矩阵
};

除此之外,我还设置了print接口打印邻接矩阵的值,用来测试模拟实现的图结构,以下面的有向图为例,用我们实现的图结构常见一个和它一样的图,然后打印邻接矩阵判断图结构是否正确


经过一些测试,以上结构没有出现严重的bug。至此图的基本结构就完成了

邻接矩阵和邻接表的优劣

刚才我实现的图是用邻接矩阵表示边之间的关系的,现在回头看邻接矩阵,我发现它需要接收一个特定值以表示顶点间的不相连,并且邻接矩阵是一个二维数组,如果一张图没有相连的顶点居多,那么这个二维数组存储的有效数据会很少,存储的数据都是表示顶点间不相连的特定值,一定程度上会造成空间的浪费。除了这个缺点呢,想要查找与一个顶点相连的所有顶点也优点费时,需要遍历数组的一行或者一列,复杂度达到O(n)。对于这些痛点,邻接表却可以很好的解决。

什么是邻接表呢?有点像哈希桶,它是一个指针数组,每个成员都是一个单链表的头指针,或者说这个单链表是与一个顶点相连的所有顶点的指针。哈希桶中,数据经过哈希函数的映射被抽象成了指针数组的下标,只要数据被抽象后的下标相同,它们就被存储在同一单链表中,链表的头指针被存储在了指针数组中,通过抽象后的下标就能在数组中找到链表的头指针。邻接表也是如此,只不过数据不经过哈希函数抽象成整数,而是被抽象成一个唯一的整数,这样的抽象关系被保存在一个map表中。但最后的结果都是数据被抽象成一个整数,每个顶点在邻接表中有了一个唯一的位置,用来存储与之相连的顶点指针

所以,使用邻接表保存顶点间的关系,可以不用接收一个特定值以表示顶点间的不相连,邻接表中的桶结构可以做到空间的按需分配,不浪费空间资源。而查找与一个顶点相连的所有顶点,只需要遍历一张单链表即可,复杂度为O(1),与图中的节点数无关。但邻接表也是有缺点的,比如快速判断了两个顶点是否相连,邻接矩阵可以用O(1)的复杂度得到答案,而邻接表却需要遍历单链表。至此,总结一下两者的优缺点,最后再用邻接表实现图结构

邻接矩阵,优点:
1.适合稠密图的顶点关系存储,不浪费空间
2.适合快速查找两顶点是否相连以及边的权值
缺点:不适合查找一个顶点的所有边
邻接表,优点:
1.适合稀疏图的顶点关系存储,节约空间
2.适合快速查找一个顶点的所有边
缺点:相对不适合查找两顶点是否相连以及边的权值

图的模拟实现(邻接表)

连接顶点的边,需要保存权值,需要保存边的终点(至于起点为什么不需要保存,因为起点已经被抽象成数组的下标,与该顶点连接的边都被存储在该下标下,可以保存但没有必要),最后还需要有一个指针域,指向下一个节点地址

struct edge_node
{size_t _det;        // 终点的下标 W _w;               // 边的权值edge_node* _next;   // 下一个节点的地址edge_node(size_t det, W w):_det(det),_w(w),_next(nullptr){}
};
vector<edge_node*> _tables;      // 邻接表,保存edge_node的数组

接着是所有接口的实现,与邻接矩阵不同的是:构造函数和add_tex接口中对邻接矩阵的初始化需要修改为对邻接表的初始化,以及add_edge接口中,需要修改的不再是邻接矩阵而是邻接表,对矩阵中某个元素的修改变为对单链表的头插,具体的细节就看下面的实现吧

template <class V, class W, W MAX_W, bool Direction = false> // 默认无向图
class graph
{private:// 只保存边的终点struct edge_node{size_t _det;        // 终点的下标 W _w;               // 边的权值edge_node* _next;   // 下一个节点的地址edge_node(size_t det, W w):_det(det),_w(w),_next(nullptr){}};
public:graph() = default;graph(const V* arr, size_t n){_vertex.reserve(n);for (size_t i = 0; i < n; ++i){add_tex(arr[i]); // 将数组中的顶点依次添加到顶点集合和映射map中}// 对邻接表的初始化_tables.resize(_vertex.size(), nullptr);}// 添加顶点的接口void add_tex(const V& v){auto ret = _index_map.find(v);if (ret != _index_map.end())     // 如果顶点不存在则添加,否则抛异常{throw invalid_argument("顶点重复");}_index_map[v] = _vertex.size();  // 建立顶点与数组下标之间的映射_vertex.push_back(v);            // 将顶点添加到顶点集合}// 查找顶点在数组中的下标,如果找不到返回-1size_t get_index(const V& v){auto ret = _index_map.find(v);if (ret == _index_map.end()){return -1;}return ret->second;}// 边的添加void add_edge(const V& src, const V& det, const W& w){// 需要进入邻接表,将顶点转换成下标size_t src_index = get_index(src);size_t det_index = get_index(det);// 顶点存在的判断if (src_index == -1 || det_index == -1){throw invalid_argument("顶点不存在");}// 检查邻接表是否初始化,因为默认构造函数并不会初始化邻接表if (_tables.size() != _vertex.size()){_tables.resize(_vertex.size(), nullptr);}// edge_node节点的构造edge_node* new_edge = new edge_node(det_index, w);// 将节点头插new_edge->_next = _tables[src_index];_tables[src_index] = new_edge;// 如果是无向图,对方也要添加边if (Direction == false){edge_node* new_edge = new edge_node(src_index, w);new_edge->_next = _tables[det_index];_tables[det_index] = new_edge;}}// for test,邻接表的打印void print(){for (size_t i = 0; i < _tables.size(); ++i){cout << '[' << i << "]->";edge_node* cur = _tables[i];while (cur){cout << cur->_det << ' ';cur = cur->_next;}cout << endl;}}
private:vector<V> _vertex;                         //保存顶点的集合unordered_map<V, size_t> _index_map;     // 保存顶点与数组下标之间的转化vector<edge_node*> _tables;              // 邻接表
};

实现完成后,与邻接矩阵一样,选择一个例子进行测试,还是同样的例子,经过比较该模拟实现符合预期的逻辑,没有严重的问题

广度优先遍历(BFS)

图的广度优先遍历与二叉树的层序遍历很相似,都是先遍历与起始节点最近的节点,二叉树是先遍历节点的子节点,而图是优先遍历与顶点有直接的边的连接的顶点
比如以A为起始顶点,先遍历的顶点是与A直接相连的顶点,B,C,D,接着再遍历与A次相连的顶点,也就是与B,C,D直接相连的顶点E,F…知道所有顶点遍历完。在这个过程中有一个问题,就是第二次遍历时,与B,C,D直接相连的顶点不止E,F,还有A顶点,但是这里不需要遍历A顶点了,所以我们需要给A顶点做一个标记,只有没有被标记顶点才会被遍历

我们可以创建一个bool数组,对应顶点的下标在数组中的值为true,说明顶点被遍历过,不需要遍历,只有数组中的值为false时,顶点才需要遍历。那么怎么控制程序遍历与A直接相连的顶点呢?可以遍历邻接表或者邻接矩阵,得到这些顶点。在遍历开始之前,将A存储到一个队列中,并在标记数组中将A标记为true,表示已经遍历过该顶点,然后取出队列中第一个数据,顶点A,对A进行遍历操作,遍历完成后将与A直接相连的顶点也存储到队列中,并在标记数组中将它们标记为true。接着取出队列中第一个数据,遍历,将与其直接相连的顶点入队,再出队,遍历顶点,入队…直到图被遍历完

void BFS(const V& v) // 根据节点的值进行广度优先遍历
{// 先查找该顶点在矩阵中的下标size_t src_index = get_index(v);if (src_index == -1) // 顶点不存在直接返回{return;}// 标记数组与控制广度优先的队列的创建vector<bool> visited(_vertex.size(), false);queue<size_t> con_queue;// 将起始顶点入队con_queue.push(src_index);// 标记数组的修改visited[src_index] = true;// 每一层的顶点数量,一开始当然是1了size_t level_size = 1;// 队列不为空,说明图中还有顶点没有遍历while (!con_queue.empty()){// 一层一层的遍历while (level_size--){size_t cur_index = con_queue.front();con_queue.pop();// 这里是遍历操作,比如打印该顶点的值cout << _vertex[cur_index] << ' ';// 将与队头顶点直接相连的顶点入队for (size_t i = 0; i < _matrix.size(); ++i){// 遍历该顶点所在的行或者列,如果矩阵中的值不等于MAX_W就说明有顶点与之相连// 当然还需要注意顶点是否被访问过if (_matrix[cur_index][i] != MAX_W && visited[i] == false){con_queue.push(i);// 入队时记得修改标记数组visited[i] = true;}}}cout << endl; // 打印格式的控制// 每层顶点个数的控制level_size = con_queue.size();}
}

深度优先遍历(DFS)

这又有点像二叉树的前序遍历,它们的思想都是相同的:二叉树不断遍历子节点直到遇到根据才回溯,图的深度遍历就是不断遍历与当前顶点相连的节点,直到走到底无路可走。但是要注意的是,二叉树不会出现环路,但是图可以出现环路,所以深度优先也需要保存一个标记数组,记录访问过的顶点
与广度优先不同,深度优先需要不断的深入遍历节点,也就是将一条路径走到头,这样的特征使用栈结构。将起始顶点入栈,栈不为空,将栈顶元素出栈,将与其相连的其他顶点入栈,与队列不同,栈结构会先访问后入栈的元素,所以出栈时,得到的顶点是与当前遍历节点相连的一个节点,比如A出栈,B,C,D都会入栈,但是我们不会依次访问B,C,D而是只访问B,B将E入栈,程序再访问E…直到不能继续访问时,程序才会回溯,访问其他的顶点。当然了,这个过程也是要注意标记数组的修改,并且我们可以用天然的栈结构——函数栈帧,使用递归完成深度优先遍历

void _DFS(size_t cur_index, vector<bool>& visited)
{// 对顶点的访问操作,这里直接打印cout << _vertex[cur_index] << ' ';// 查找与该顶点相连的顶点,并递归访问这些节点for (size_t i = 0; i < _vertex.size(); ++i){if (_matrix[cur_index][i] != MAX_W && visited[i] == false){visited[i] = true;// 顶点的递归访问,如果程序走到头了,将退回到上层递归函数_DFS(i, visited);}}
}void DFS(const V& v)
{// 顶点的存在检查size_t src_index = get_index(v);if (src_index == -1){return;}// 标记数组的创建,与起始点的修改vector<bool> visited(_vertex.size(), false);visited[src_index] = true;// 子函数的调用_DFS(src_index, visited);
}

程序运行结果,与上面图片的访问路径不同,因为深度优先遍历的路径不是唯一的


至此关于图的结构实现以及其基础算法的讲解结束,下面给出整份hpp文件

hpp代码展示

#pragma once#include <iostream>
#include <vector>
#include <unordered_map>
#include <queue>
#include "UnionFindSet.hpp"using namespace std;
namespace matrix
{template <class V, class W, W MAX_W, bool Direction = false> // 默认无向图class graph{public:graph() = default;graph(const V* arr, size_t n){_vertex.reserve(n);for (size_t i = 0; i < n; ++i){add_tex(arr[i]);              // 将数组中的顶点依次添加到顶点集合和映射map中}// 对邻接矩阵的初始化,默认顶点间不相连_matrix.resize(_vertex.size());for (size_t i = 0; i < _vertex.size(); ++i){_matrix[i].resize(_vertex.size(), MAX_W);}}// 添加顶点的接口void add_tex(const V& v){auto ret = _index_map.find(v);if (ret != _index_map.end())      // 如果顶点不存在则添加,否则抛异常{throw invalid_argument("顶点重复");}_index_map[v] = _vertex.size();  // 建立顶点与数组下标之间的映射_vertex.push_back(v);            // 将顶点添加到顶点集合}// 查找顶点在数组中的下标,如果找不到返回-1size_t get_index(const V& v){auto ret = _index_map.find(v);if (ret == _index_map.end()){return -1;}return ret->second;}void _add_edge(size_t srci, size_t deti, const W& w){// 检查邻接矩阵是否初始化,因为默认构造函数并不会初始化矩阵if (_matrix.size() != _vertex.size()){_matrix.resize(_vertex.size());for (size_t i = 0; i < _matrix.size(); ++i){// 用最大值初始化矩阵_matrix[i].resize(_vertex.size(), MAX_W);}}_matrix[srci][deti] = w;// 如果是无向图,镜像也要添加边if (Direction == false){_matrix[deti][srci] = w;}}// 边的添加void add_edge(const V& src, const V& det, const W& w){// 需要进入邻接表,将顶点转换成下标size_t src_index = get_index(src);size_t det_index = get_index(det);// 顶点存在的判断if (src_index == -1 || det_index == -1){throw invalid_argument("顶点不存在");}_add_edge(src_index, det_index, w);}// for test,邻接矩阵的打印void print(){for (size_t i = 0; i < _matrix.size(); ++i){for (size_t j = 0; j < _matrix.size(); ++j){if (_matrix[i][j] == INT_MAX)cout << "* ";elsecout << _matrix[i][j] << ' ';}cout << endl;}}void print(){for (size_t i = 0; i < _matrix.size(); ++i){for (size_t j = 0; j < i; ++j){if (_matrix[i][j] != INT_MAX)cout << _vertex[j] << "->" << _vertex[i] << ':' << _matrix[i][j] << endl;}}}void BFS(const V& v) // 根据节点的值进行广度优先遍历{// 先查找该顶点在矩阵中的下标size_t src_index = get_index(v);if (src_index == -1) // 顶点不存在直接返回{return;}// 标记数组与控制广度优先的队列的创建vector<bool> visited(_vertex.size(), false);queue<size_t> con_queue;// 将起始顶点入队con_queue.push(src_index);// 标记数组的修改visited[src_index] = true;// 每一层的顶点数量,一开始当然是1了size_t level_size = 1;// 队列不为空,说明图中还有顶点没有遍历while (!con_queue.empty()){// 一层一层的遍历while (level_size--){size_t cur_index = con_queue.front();con_queue.pop();// 这里是遍历操作,比如打印该顶点的值cout << _vertex[cur_index] << ' ';// 将与队头顶点直接相连的顶点入队for (size_t i = 0; i < _matrix.size(); ++i){// 遍历该顶点所在的行或者列,如果矩阵中的值不等于MAX_W就说明有顶点与之相连// 当然还需要注意顶点是否被访问过if (_matrix[cur_index][i] != MAX_W && visited[i] == false){con_queue.push(i);// 入队时记得修改标记数组visited[i] = true;}}}cout << endl; // 打印格式的控制// 每层顶点个数的控制level_size = con_queue.size();}}void _DFS(size_t cur_index, vector<bool>& visited){// 对顶点的访问操作,这里直接打印cout << _vertex[cur_index] << ' ';// 查找与该顶点相连的顶点,并递归访问这些节点for (size_t i = 0; i < _vertex.size(); ++i){if (_matrix[cur_index][i] != MAX_W && visited[i] == false){visited[i] = true;// 顶点的递归访问,如果程序走到头了,将退回到上层递归函数_DFS(i, visited);}}}void DFS(const V& v){// 顶点的存在检查size_t src_index = get_index(v);if (src_index == -1){return;}// 标记数组的创建,与起始点的修改vector<bool> visited(_vertex.size(), false);visited[src_index] = true;// 子函数的调用_DFS(src_index, visited);}private:vector<V> _vertex;                         //保存顶点的集合unordered_map<V, size_t> _index_map;     // 保存顶点与数组下标之间的转化vector<vector<W>> _matrix;               // 邻接矩阵};
}
namespace table
{template <class V, class W, W MAX_W, bool Direction = false> // 默认无向图class graph{private:// 只保存边的终点struct edge_node{size_t _det;        // 终点的下标 W _w;               // 边的权值edge_node* _next;   // 下一个节点的地址edge_node(size_t det, W w):_det(det),_w(w),_next(nullptr){}};public:graph() = default;graph(const V* arr, size_t n){_vertex.reserve(n);for (size_t i = 0; i < n; ++i){add_tex(arr[i]);              // 将数组中的顶点依次添加到顶点集合和映射map中}// 对邻接表的初始化_tables.resize(_vertex.size(), nullptr);}// 添加顶点的接口void add_tex(const V& v){auto ret = _index_map.find(v);if (ret != _index_map.end())      // 如果顶点不存在则添加,否则抛异常{throw invalid_argument("顶点重复");}_index_map[v] = _vertex.size();  // 建立顶点与数组下标之间的映射_vertex.push_back(v);            // 将顶点添加到顶点集合}// 查找顶点在数组中的下标,如果找不到返回-1size_t get_index(const V& v){auto ret = _index_map.find(v);if (ret == _index_map.end()){return -1;}return ret->second;}// 边的添加void add_edge(const V& src, const V& det, const W& w){// 需要进入邻接表,将顶点转换成下标size_t src_index = get_index(src);size_t det_index = get_index(det);// 顶点存在的判断if (src_index == -1 || det_index == -1){throw invalid_argument("顶点不存在");}// 检查邻接表是否初始化,因为默认构造函数并不会初始化邻接表if (_tables.size() != _vertex.size()){_tables.resize(_vertex.size(), nullptr);}edge_node* new_edge = new edge_node(det_index, w);// 将节点头插new_edge->_next = _tables[src_index];_tables[src_index] = new_edge;// 如果是无向图,对方也要添加边if (Direction == false){edge_node* new_edge = new edge_node(src_index, w);new_edge->_next = _tables[det_index];_tables[det_index] = new_edge;}}// for test,邻接矩阵的打印void print(){for (size_t i = 0; i < _tables.size(); ++i){cout << '[' << i << "]->";edge_node* cur = _tables[i];while (cur){cout << cur->_det << ' ';cur = cur->_next;}cout << endl;}}private:vector<V> _vertex;                        //保存顶点的集合unordered_map<V, size_t> _index_map;     // 保存顶点与数组下标之间的转化vector<edge_node*> _tables;              // 邻接表};
}

C++ | 数据结构 | 图结构的讲解与模拟实现 | DFS与BFS的实现相关推荐

  1. 数据结构——图结构:图

    数据结构与算法分析--目录 第一部分:数据结构 数据结构--图结构:图 图基础 \quad 图是一种比线性表和数更为复杂的数据结构.在图结构中,结点之间的关系可以是任意的,也就是说,图中的任意两个数据 ...

  2. 三十张图片让你彻底弄明白图的两种遍历方式:DFS和BFS

    1 引言   遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次.图的遍历.遍历过程中得到的顶点序列称为图遍历序列. 2 深度优先搜索 2.1 算法思想 ...

  3. lisp遍历表中所有顶点_三十张图片让你彻底弄明白图的两种遍历方式:DFS和BFS...

    1 引言   遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次.   在二叉树基础中,介绍了对于树的遍历.树的遍历是指从根节点出发,按照一定的访问规则 ...

  4. 【数据结构与算法】详解什么是图结构,并用代码手动实现一个图结构

    本系列文章[数据结构与算法]所有完整代码已上传 github,想要完整代码的小伙伴可以直接去那获取,可以的话欢迎点个Star哦~下面放上跳转链接 https://github.com/Lpyexplo ...

  5. Carson带你学数据结构:手把手带你了解 ”图“ 所有知识!(含DFS、BFS)

    前言 本文主要讲解 数据结构中的图 结构,包括 深度优先搜索(DFS).广度优先搜索(BFS).最小生成树算法等,希望你们会喜欢. 目录 1. 简介 数据结构的中属于 圆形结构 的逻辑结构 具体介绍如 ...

  6. 【数据结构笔记21】图的遍历,DFS与BFS,连通图

    本次笔记内容: 6.2.1 图的遍历 - DFS 6.2.2 图的遍历 - BFS 6.2.3 图的遍历 - 为什么需要两种遍历 6.2.4 图的遍历 - 图连不通怎么办? 文章目录 深度优先搜索(D ...

  7. Java图结构-模拟校园地图-迪杰斯特拉(Dijkstra)算法求最短路径 #谭子

    目录目录 一.前言 二.模拟校园地图描述 三.分析题目及相关绘图 四.代码部分 1.GraphNode类 2.Menu类(管理文字) 3.Attraction类 4.AttractionGraph类( ...

  8. C语言 数据结构 图的邻接矩阵存储 基本操作(附输入样例和讲解)

    代码参照了严蔚敏.吴伟民编写的数据结构(C语言版). 部分内容参考了这位大佬: https://blog.csdn.net/jeffleo/article/details/53326648 所有代码采 ...

  9. python函数结构图_Python数据结构与算法之图结构(Graph)实例分析

    本文实例讲述了Python数据结构与算法之图结构(Graph).分享给大家供大家参考,具体如下: 图结构(Graph)--算法学中最强大的框架之一.树结构只是图的一种特殊情况. 如果我们可将自己的工作 ...

最新文章

  1. linux rpm包解压到当前目录
  2. 【收藏】在QGIS中添加Google Maps地图和卫星影像
  3. 选择结构_标准if-else语句
  4. ConnectivityManager ConnectivityService in Android
  5. LINUX 下编译 ffmpeg
  6. 微软高管解读财报:努力创新云基础架构
  7. C#实现树的双亲表示法
  8. 《scikit-learn》通过GridSearchCV来进行超参数优化
  9. SourceTree的基本使用 - 天字天蝎 - 博客园
  10. 云计算实战系列-磁盘阵列
  11. Image2icon for Mac(icon图标设计软件)
  12. BZOJ1468: Tree BZOJ3365: [Usaco2004 Feb]Distance Statistics 路程统计
  13. 字符数组的定义与使用具体解析
  14. mysqldump 快还是navicat快_剪辑软件评测:选喵影工厂、爱剪辑还是快剪辑?
  15. 淘宝关键词搜索采集商品数据接口
  16. 之前招的当老板了,阿里拍卖急需前端!!!
  17. 海康威视 + 搭配内网穿透,搭建远程视频监控教程
  18. 云服务器远程登录方法
  19. 国家及校级奖项、称号(中英对照)
  20. 【解决方案】PicGo图片上传失败问题【少走弯路】

热门文章

  1. 从这 5 个场景 , 看 MPC 多方安全计算的行业应用
  2. python 数据分析day4 Pandas 之 DataFrame
  3. LTE学习-OFDM
  4. 磕磕碰碰中用Visual Studio编译出了64位静态x264和ffmpeg
  5. STM32 CAN通信的学习笔记总结(从小白开始)
  6. 外包:.epub格式漫画解压后图片顺序重排
  7. 图形推理1000题pdf_行测80分秒杀技——图形推理满分√
  8. Chrome播放视频时只有声音没有画面
  9. 三跨考生准备考研复试(机试)之路(日记版)
  10. 项目管理软件售后培训方案