最近在数据结构的学习中,学习到了图这一章节,在图中,有两个遍历的策略:一种是广度优先遍历,一种是深度优先遍历。
先上运行结果吧
图的结构
这里以D顶点为起始顶点
广度优先遍历
首先我们介绍广度优先遍历,在介绍图的广度优先遍历之前,先回顾一下树的广度优先遍历,就是层序遍历。下面是树的层序遍历代码
void LevelOrder(BitTree t) {
Linkqueue q;//声明一个链队列
initQueue(q);//初始化一个链队列
BitTree p;
enqueue(q, t);//把根节点入队
while (!isEmpty(q)) {//队列不空就循环
dequeue(q, p);//队头结点出队
visit(p);//访问出队结点
if (p->lchild != NULL)
enqueue(q, p->lchild);//左孩子入队
if (p->rchild != NULL)
enqueue(q, p->rchild);//右孩子入队
}
}
所以,我们还需要设置一个标记数组,记作visited,这个数组和图的顶点序列数组一致,数组的每一项都对应了一个顶点,设置bool值来反应,通过索引值可以比较方便判断顶点是否被遍历过。
bool visited[MAXSIZE];//全局
在开始之前,我们还需要两个函数,一个函数用来求x顶点的第一个和它直接连通的顶点。一个函数用来求除开y顶点,和x顶点直接连通的顶点
//求图G中顶点x的第一个邻接点,若有则返回顶点编号,若没有则返回 - 1。
int FirstNeighbor(Graph g, char vertex) {
if ((vertex - 65) < 0 || (vertex - 65) >= MAXSIZE) {
printf("输入字符不合法!\n");
return -1;
}
int ver_index = -1;
for (int i = 0; i < g.vertex_num; i++)
{
if (vertex == g.vertexs[i].ch) {
ver_index = i;//找到该顶点的索引.
}
}
if (ver_index == -1) {
return -1;//说明该顶点不存在
}
else {
for (int i = 0; i < g.vertex_num; i++)
{
if (g.edge[ver_index][i] != 0) {
return i;
}
}
}
return -1;
}
//求图G中顶点y是顶点x的一个邻接点,返回除了y之外的下一个和x邻接的顶点,如果y是唯一和x邻接的顶点,返回-1.
int NextNeighbor(Graph g, char ver_x, char ver_y) {
//超出A-Z的范围
if ((ver_x - 65) < 0 || (ver_x - 65) > 26) {
return -1;
}
int ver_index = -1;
int target_index = -1;
for (int i = 0; i < g.vertex_num; i++)
{
if (g.vertexs[i].ch == ver_x) {
ver_index = i;//获取x结点对应的数组下标
}
if (g.vertexs[i].ch == ver_y) {
target_index = i;//获取y结点对应的数组下标
}
}
if (ver_index == -1) {
return -1;//表示图中没有找到输入的x顶点
}
if (target_index == -1) {
return -1;
}
for (int i = target_index + 1; i < g.vertex_num; i++)
{
if (g.edge[ver_index][i] != 0) {
//表示找到目标y结点
return i;
}
}
return -1;
}
这两个函数当返回值>=0时,便表示找到了目标顶点,并返回目标顶点在顶点数组的下标
接下来就是广度优先遍历的主体函数
//图的广度优先遍历
bool BFSGraph(Graph g,char v) {
if (g.vertex_num == 0) {
return false;
}
char queue[30];//定义一个队列
int front = -1, rear = -1;//定义队列的指针
int v_index = -1;//表示v在vertexs数组的索引值
//获取该节点对应的index值
for (int i = 0; i < g.vertex_num; i++)
{
if (v == g.vertexs[i].ch) {
v_index = i;
}
}
if (v_index == -1) {
//表示没有找到对应的结点
return false;
}
VisitVertex(g.vertexs[v_index].ch);//访问第一个结点
visited[v_index] = true;//把对应的顶点的标记数组标记为真,表示访问过该顶点
rear++;
queue[rear] = v;//把该顶点入队
while (front != rear) {
front++;//出队
v = queue[front];//v等于出队顶点
for (int w_index = FirstNeighbor(g,v); w_index >= 0; w_index = NextNeighbor(g,v,g.vertexs[w_index].ch))
{
//FirstNeighbor是返回对应顶点的第一个邻接点在vertex中的索引。
if (!visited[w_index]) {
//若该顶点还没有被访问
VisitVertex(g.vertexs[w_index].ch);
visited[w_index] = true;
rear++;
queue[rear] = g.vertexs[w_index].ch;
}
}
}
return true;
}
到这里,还没有完全结束,因为图中,两个顶点不一定一定有通路,所以我们还需要把图中所有顶点全部遍历到,这时候,我们发现在visited数组中,还有一些数组元素的值仍然是false,这些顶点和你所输入的顶点并没有通路,所以函数没有遍历到,所以,我们再使用一个函数,遍历的去访问这些false的顶点。
//图的广度优先遍历的
void BFSGraphTraverse(Graph g,char v) {
for (int i = 0; i < g.vertex_num; i++)
visited[i] = false;//先把标记数组的值设置为false,表示没有遍历过的顶点
for (int i = 0; i < g.vertex_num; i++)
{
if (!visited[i]) {
BFSGraph(g, v);//这个顶点没有被访问就调用
}
}
}
这也是最终的入口函数
深度优先遍历
在树的深度优先遍历中,使用的先根遍历的方式,把这种思想迁移到图的深度遍历中,在这里,与广度遍历一样的是,也是准备一个visited数组,去避免回路的重复访问相同顶点,下面就是具体代码
//图的深度优先
void DFSGraphTraverse(Graph g,char v) {
VisitVertex(v);//访问v顶点
int v_index = -1;
//获取该节点对应的index值
for (int i = 0; i < g.vertex_num; i++)
{
if (v == g.vertexs[i].ch) {
v_index = i;
}
}
visited[v_index] = true;
for (int w_index = FirstNeighbor(g, v); w_index >= 0; w_index = NextNeighbor(g, v, g.vertexs[w_index].ch))
{
//FirstNeighbor是返回对应顶点的第一个邻接点在vertex中的索引。
if (!visited[w_index]) {
DFSGraphTraverse(g, g.vertexs[w_index].ch);
}
}
}
广度优先算法的性能分析,这里考虑主体的功能实现的性能,不考虑其他一些操作
-
在邻接矩阵存储的图中,访问|V|个顶点需要O(|V|)的时间,查找每一个顶点的邻接点都需要O(|V|)的时间,所以这个算法的时间复杂度是O(|V|²)
-
在邻接表存储的图中,访问|V|个顶点需要O(|V|)的时间,查找每一个顶点的邻接点一共需要O(|E|)的时间,所以这个算法的时间复杂度是O(|V| + |E|)
具体解释一下这段代码
for (int w_index = FirstNeighbor(g,v); w_index >= 0; w_index = NextNeighbor(g,v,g.vertexs[w_index].ch))
{
//FirstNeighbor是返回对应顶点的第一个邻接点在vertex中的索引。
if (!visited[w_index]) {
//若该顶点还没有被访问
VisitVertex(g.vertexs[w_index].ch);
visited[w_index] = true;
rear++;
queue[rear] = g.vertexs[w_index].ch;
}
}
FirstNeighbor函数是用用来返回v顶点的第一个直接连通的顶点的数组下标,g.vertexts[w_index]就是对应的顶点,NextNeighbor函数是返回除开g.vertexts[w_index]顶点的下一个v顶点直接连通的顶点数组下标,通过这个for循环,会一直循环返回出v结点的所有相连通的顶点的数组下标,一直到函数返回-1,表示没有顶点和v顶点连通为止。
其实不管是广度优先还是深度优先,图的遍历一定上和树的遍历都是一样的,因为图和树其实也是有很多相似的地方,主要在于理解树的先根遍历,层次遍历,理解队列的配合使用和函数的递归调用,这些才是本质。
我所定义的图的结构
//定义图的顶点
typedef struct Vertex {
int data;//顶点的数值
char ch;//顶点的字符代号
int info;//顶点的权
bool empty;//顶点是否存在
};
//定义邻接矩阵图
typedef struct Graph {
Vertex vertexs[MAXSIZE];//存储结点
int edge[MAXSIZE][MAXSIZE];//结点关系矩阵
int vertex_num, arc_num;//图的当前顶点数和边数
};
//初始化矩阵图
void InitGraph(Graph &g) {
for (int i = 0; i < MAXSIZE; i++)
{
g.vertexs[i].ch = 0;
}
//关系矩阵数据全部清零。
for (int i = 0; i < MAXSIZE; i++)
{
for (int j = 0; j < MAXSIZE; j++) {
g.edge[i][j] = 0;
}
}
//基本状态清0
g.vertex_num = 0;
g.arc_num = 0;
}
VisitVertex函数
//访问图的顶点
void VisitVertex(char vertex) {
cout << vertex ;
}