1.知识预备
- 图的概念 引用自百度百科
主要有以下两种定义。
二元组的定义
图G是一个有序二元组(V,E),其中V称为顶集(Vertices Set),E称为边集(Edges set),E与V不相交。它们亦可写成V(G)和E(G)。
E的元素都是二元组,用(x,y)表示,其中x,y∈V。 [1]
三元组的定义
图G是指一个三元组(V,E,I),其中V称为顶集,E称为边集,E与V不相交;I称为关联函数,I将E中的每一个元素映射到 。如果e被映射到(u,v),那么称边e连接顶点u,v,而u,v则称作e的端点,u,v此时关于e相邻。同时,若两条边i,j有一个公共顶点u,则称i,j关于u相邻。
- 无向图和有向图
如果给图的每条边规定一个方向,那么得到的图称为有向图。在有向图中,与一个节点相关联的边有出边和入边之分。相反,边没有方向的图称为无向图。
单图
一个图如果任意两顶点之间只有一条边(在有向图中为两顶点之间每个方向只有一条边);边集中不含环,则称为单图。
- 图的存储表示
数组(邻接矩阵)存储表示(有向或无向)
邻接表存储表示
有向图的十字链表存储表示
无向图的邻接多重表存储表示
本文将介绍邻接矩阵图的表示方法
关于邻接表的广度优先遍历和深度优先遍历算法参考本人的另一篇博客 还没写?
2.广度优先遍历(BFS)
广度优先遍历类似于树的层序遍历(先访问完同一层的所有节点再访问下一层)
从图中顶点v出发进行广度优先遍历的基本思想是:
- 访问顶点v
- 依次访问顶点v的各个未被访问的邻接点v1,v2,…,vk(就是访问顶点v的下一层邻居)
- 分别从v1,v2,…,vk出发依次访问它们未被访问的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直到图中所有与顶点v有路径相通的顶点都被访问到
伪代码:
- 初始化队列queue;
- 访问顶点v,置顶点v已访问,visited[v]=1;,顶点v入队列queue
- while(队列queue非空)
3.1 v=队列queue的队首元素出队
3.2 w=顶点v的第一个邻接点
3.3 while(w存在)
…3.3.1 如果w未被访问,则访问顶点w,置顶点w已访问,visited[w]=1;顶点w入队列queue
…3.3.2 w=顶点v的下一个邻接点举个例子说明,看到这图不要慌:
首先,需要一个int visited [ ] 数组来存储顶点的访问信息,一开始全部初始化为0,即全部顶点都未被访问,每访问一个顶点,就将该顶点在visited数组中置为1。其次,还需要一个队列,来存储被访问的顶点顺序
- 假设给定起始点v0,各顶点在数组中下标为0,1,2,3,4,5,6,将v0入队,访问v0,此时队列状态:{v0}
- 队首元素v0出队,寻找v0的邻接顶点,此时队列状态:{}
2.1 寻找到v1且未被访问,则将v1入队,此时队列状态:{v1},访问v1
2.2 再寻找到v2且未被访问,则将v2入队,此时队列状态:{v1,v2},访问v2
2.3 再寻找到v3且未被访问,则将v3入队,此时队列状态:{v1,v2,v3},访问v3
2.4 找不到v0的其他邻接点了
(为什么顺序是v1→v2→v3呢?因为在for循环中,循环变量是从0开始,然后+1加上去的)- 队首元素v1出队,寻找v1的邻接顶点,此时队列状态:{v2,v3}
3.1 寻找到v0但已访问,不做任何操作
3.2 寻找到v4且未被访问,则将v4入队,此时队列状态:{v2,v3,v4},访问v4
3.3 找不到v1的其他邻接点了- 队首元素v2出队,寻找v2的邻接顶点,此时队列状态:{v3,v4}
4.1 寻找到v0但已访问,不做任何操作
4.2 寻找到v6且未被访问,则将v6入队,此时队列状态:{v3,v4,v6},访问v6
4.3 找不到v2的其他邻接点了- 队首元素v3出队,寻找v3的邻接顶点,此时队列状态:{v4,v6}
5.1 寻找到v0但已被访问,不做任何操作
5.2 寻找到v6但已被访问,不做任何操作
5.3 找不到v3的其他邻接点了- 队首元素v4出队,寻找v4的邻接顶点,此时队列状态:{v6}
6.1 寻找到v1但已被访问,不做任何操作
6.2 寻找到v5且未访问,则将v5入队,此时队列状态:{v6,v5},访问v5
6.3 找不到v4的其他邻接点了- 队首元素v6出队,寻找v6的邻接顶点,此时队列状态:{v5}
7.1 寻找到v2但已被访问,不做任何操作
7.2 寻找到v3但已被访问,不做任何操作
7.3 寻找到v5但已被访问,不做任何操作
7.4 找不到v4的其他邻接点了- 队首元素v5出队,寻找v5的邻接顶点,此时队列状态:{}
8.1 寻找到v4但已被访问,不做任何操作
8.2 寻找到v6但已被访问,不做任何操作
8.3 找不到v5的其他邻接点了- 至此,队列已空,说明所有顶点都访问完毕,该广度优先遍历顺序为v0→v1→v2→v3→v4→v6→v5
注意:广度优先遍历结果不唯一,因为访问的邻接点顺序不一样,结果就可能不同
以下使用类实现,若不想写成类的形式,只需要把代码提取出来写成结构体即可
/*邻接矩阵(MatrixGraph)表示无向图*/
#pragma once
#define MAXSIZE 10
#define INF 0x3f3f3f3f //无穷大
#include <iostream>
using namespace std;
/*无向图邻接矩阵*/
template<class T>
class MGraph
{
public:
MGraph(T a[],int n,int e); //构造函数,建立具有n个顶点e条边的图,a为自定义类型数组用于存储顶点名称
~MGraph() {}
void BFSTraverse(int v); //广度优先遍历图 v为起始顶点编号
private:
T vertex[MAXSIZE]; //存放图中顶点的数组
int arc[MAXSIZE][MAXSIZE]; //存放图中边的数组
int vertexNum, arcNum; //图的顶点数和边数
void _BFSTraverse(int v,int visited[]);//visited数组用于存储顶点的访问情况
};
template<class T>
MGraph<T>::MGraph(T a[], int n, int e)
{
int i, j, k;
vertexNum = n; arcNum = e;
for (i = 0; i < vertexNum; i++) {//保存顶点名称
vertex[i] = a[i];
}
for (i = 0; i < vertexNum; i++) {//初始化邻接矩阵
for (j = 0; j < vertexNum; j++) {
if (i == j)arc[i][j] = 0;//顶点到顶点自身设置为0
else arc[i][j] = INF; //顶点到其他顶点设置为无穷大
}
}
for (k = 0; k < arcNum; k++) { //依次输入每一条边
cout << "请分别输入边依附的两个顶点的编号:";
cin >> i >> j; //输入边依附的两个顶点的编号
arc[i][j] = 1; arc[j][i] = 1; //置有边标志(顶点i到顶点j有连线)
}
}
template<class T>
void MGraph<T>::BFSTraverse(int v)//广度优先遍历的启动函数(外部调用,在调用实际算法前 将数组初始化)
{
int visited[MAXSIZE] = { 0 };
_BFSTraverse(v, visited);
}
template<class T>
void MGraph<T>::_BFSTraverse(int v, int visited[])//广度优先遍历 v为给定的起始顶点编号
{
int front, rear, queue[MAXSIZE];//队列的头指针与尾指针,使用数组模拟顺序队列
front = rear = -1; //初始化队列,假设队列采用顺序存储并且不会发生溢出
cout << vertex[v]<<" "; //输出初始顶点名称
visited[v] = 1; //将初始顶点置为已访问
queue[++rear] = v; //记录访问顺序,将初始顶点入队
while (front != rear) { //当队列非空时循环
v = queue[++front]; //将队首元素出队并送到v中
for (int j = 0; j < vertexNum; j++) {//从顶点v出发寻找顶点v的邻接点
if (arc[v][j] == 1 && visited[j] == 0) {//顶点v和顶点j有连线 且 顶点j未被访问
cout << vertex[j]<<" "; //输出顶点j的名称
visited[j] = 1; //将顶点j置为已访问
queue[++rear] = j; //记录访问顺序,因此将顶点j入队
}
}
}
}
3.深度优先遍历
与广度优先遍历不同的是,深度优先遍历类似于树的前序遍历(在一条路走到底,走不通回到上一个分岔口走另一条路),从图中某顶点v出发进行深度优先遍历的基本思想是:
- 访问顶点v
- 从v的未被访问过的邻接点中选取一个顶点顶点w,从w出发进行深度优先遍历
- 重复上述两步,直至图中所有和v有路径相通的顶点都被访问到
伪代码:
- 访问顶点v;置顶点v已访问,visited[v]=1;
- w=顶点v的第一个邻接点
- while(w存在)
…3.1 if(w未被访问) 从顶点w出发递归执行该算法
…3.2 w=顶点v的下一个邻接点举个例子说明,看到这图不要慌:
首先,需要一个int visited [ ] 数组来存储各顶点的访问信息,一开始全部初始化为0,此处使用递归实现,就不需要用栈来存储顶点的访问路径了。但是本文将会使用栈来描述递归的状态扫描二维码关注公众号,回复: 8988321 查看本文章
- 假设给的起始点为v0,(v0,v1,v2…v6在visited数组中的下标为0,1,2…6)
- 起始点v0,将v0入栈,此时栈状态:{v0},访问v0,寻找v0的邻接点
- 寻找到v0的邻接点v1且未访问,将v1入栈,此时栈状态:{v0,v1},访问v1,寻找v1的邻接点
- X寻找到v1的邻接点v0,但已经访问,继续寻找v1的其他邻接点
- 寻找到v1的邻接点v4且未访问,将v4入栈,此时栈状态:{v0,v1,v4},访问v4,寻找v4的邻接点
- X寻找到v4的邻接点的邻接点v1,但已经访问,继续寻找v4的其他邻接点
- 寻找到v4的邻接点v5且未访问,将v5入栈,此时栈状态:{v0,v1,v4,v5},访问v5,寻找v5的邻接点
- X寻找到v5的邻接点v4,但已访问,继续寻找v5的其他邻接点
- 寻找到v5的邻接点v6且未访问,将v6入栈,此时栈状态:{v0,v1,v4,v5,v6},访问v6,寻找v6的邻接点
- 寻找到v6的邻接点v2且未访问,将v2入栈,此时栈状态:{v0,v1,v4,v5,v6,v2},访问v2,寻找v2的邻接点
- X寻找到v2的邻接点v0,但已访问,继续寻找v2的其他邻接点
- X寻找到v2的邻接点v6,但已访问,继续寻找v2的其他邻接点
- !v2的邻接点都已经被访问过了,栈顶元素v2出栈后,此时栈状态:{v0,v1,v4,v5,v6},返回上一层递归,寻找v6的其他邻接点
- 寻找到v6的邻接点v3且未访问,将v3入栈,此时栈状态:{v0,v1,v4,v5,v6,v3},访问v3,寻找v3的邻接点
- X寻找到v3的邻接点v0和v6,但都已访问
- !v3的邻接点都已经被访问过了,栈顶元素v3出栈后,此时栈状态:{v0,v1,v4,v5,v6},返回上一层递归,寻找v6的其他邻接点
- !v6的邻接点都已经被访问过了,栈顶元素v6出栈后,此时栈状态:{v0,v1,v4,v5},返回上一层递归,寻找v5的其他邻接点
- !v5的邻接点都已经被访问过了,栈顶元素v5出栈后,此时栈状态:{v0,v1,v4},返回上一层递归,寻找v4的其他邻接点
- !v4的邻接点都已经被访问过了,栈顶元素v4出栈后,此时栈状态:{v0,v1},返回上一层递归,寻找v1的其他邻接点
- !v1的邻接点都已经被访问过了,栈顶元素v1出栈后,此时栈状态:{v0},返回上一层递归,寻找v0的其他邻接点
- !v0的邻接点都已经被访问过了,栈顶元素v0出栈后,此时栈状态:{},递归结束,深度优先遍历完成
至此,所有顶点都访问完毕,该深度优先遍历顺序为v0→v1→v4→v5→v6→v2→v3
注意:深度优先遍历结果不唯一,因为访问的邻接点顺序不一样,结果就可能不同
template<class T>
void MGraph<T>::DFSTraverse(int v)//深度优先遍历的启动函数(外部调用,初始化visited数组后调用实际算法)
{
int visited[MAXSIZE] = { 0 };
_DFSTraverse(v, visited);
}
template<class T>
void MGraph<T>::_DFSTraverse(int v, int visited[])//深度优先遍历,递归实现
{
cout << vertex[v]<<" "; visited[v] = 1; //输出顶点的值 且 将该顶点置为已访问
for (int j = 0; j < vertexNum; j++) { //寻找顶点v的邻接点
if (arc[v][j] == 1 && visited[j] == 0)//若顶点v和顶点j有边 且 顶点j未被访问
_DFSTraverse(j, visited); //访问顶点j
}
}
文章中有误的部分,恳请指出,谢谢Thanks♪(・ω・)ノ。