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

前言

在聊图的结构之前,我们可以先从熟悉的地方开始,这有一条结论:树是一种特殊的图,图不一定是树。我们知道树形结构多用于搜索查找,典型的结构:搜索二叉树,红黑树和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);            // 将顶点添加到顶点集合
	}

	// 查找顶点在数组中的下标,如果找不到返回-1
	size_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 << "* ";
				else
					cout << _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);            // 将顶点添加到顶点集合
	}

	// 查找顶点在数组中的下标,如果找不到返回-1
	size_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);            // 将顶点添加到顶点集合
		}

		// 查找顶点在数组中的下标,如果找不到返回-1
		size_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 << "* ";
					else
						cout << _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);            // 将顶点添加到顶点集合
		}

		// 查找顶点在数组中的下标,如果找不到返回-1
		size_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;              // 邻接表
	};
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值