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

发布于:2025-04-16 ⋅ 阅读:(32) ⋅ 点赞:(0)

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

💗系列专栏: 【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);
}

运行结果