数据结构基础:P6.2-图(一)--->图的遍历

本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-线性结构—>应用实例:多项式加法运算
数据结构基础:P2.5-线性结构—>应用实例:多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现
数据结构基础:P4.1-树(二)—>二叉搜索树
数据结构基础:P4.2-树(二)—>二叉平衡树
数据结构基础:P4.3-树(二)—>小白专场:是否同一棵二叉搜索树-C实现
数据结构基础:P4.4-树(二)—>线性结构之习题选讲:逆转链表
数据结构基础:P5.1-树(三)—>堆
数据结构基础:P5.2-树(三)—>哈夫曼树与哈夫曼编码
数据结构基础:P5.3-树(三)—>集合及运算
数据结构基础:P5.4-树(三)—>入门专场:堆中的路径
数据结构基础:P5.5-树(三)—>入门专场:File Transfer
数据结构基础:P6.1-图(一)—>什么是图


前言

遍历这个词在图里面的意思跟在树里面的意思是一样的。图的遍历就是指把图里面每一个顶点访问一遍,而且不能有重复的访问。首先,一个问题是说我们干嘛要做这件事呢?你不要小看这件事,看上去非常简单,它可以用来解决很多有趣的问题,我们后面会用例子给大家说明。在这先给大家介绍两种非常典型的遍历的方法一种叫做 DFS,一种叫做 BFS。


一、深度优先搜索(Depth First Search, DFS)

所谓 DFS 是英文 Depth First Search 的缩写,它指的是深度优先搜索。要讲清楚深度优先搜索是怎么做的,我们可以先通过一个迷宫的例子来看一下。

假设我们有这样的一个迷宫,然后在迷宫的各个拐角处都有一堆灯泡,你要去一个一个的把它们点亮
在这里插入图片描述
假设给定了你迷宫的入口,那我当然先把入口的这个灯泡点亮了。
在这里插入图片描述
站在这个路口我要去点亮下一盏灯,可以看到有三盏灯在视力范围内。我挑一盏灯先把它点亮,比如说我挑右边这盏灯。
在这里插入图片描述
于是我人现在站在这个位置,可以看到两盏灯。其中一盏灯已经是点亮的,还有一盏灯没有亮,那我走下去把没亮的灯点亮。
在这里插入图片描述
同样的,我继续将没亮的那盏灯点亮。
在这里插入图片描述
现在视野范围内有3盏灯没亮,我们随便选一个去点亮,我们这里选1。
在这里插入图片描述
走到这里只有1没电了,只能去点亮1。
在这里插入图片描述
点亮1这盏灯的时候放眼一望,所有的灯都已经亮了。这时候要原路返回,退到2这个路口,再看看周围的灯是否全亮(注意,2这里能直接回到出口,但是不能去)。2周围的灯全都亮了,我们继续原路返回,碰到3,3周围有未点亮的灯。
在这里插入图片描述
3视野范围内还有两盏灯没亮,我就选了其中一条路走过去,把这盏灯点亮。
在这里插入图片描述
此时视野范围内还有1盏灯没亮,去把它点亮。
在这里插入图片描述
最后不能直接上去到出发点,必须按照原路返回,以防右边还有一些分岔口。
在这里插入图片描述


刚才演示的就是一个深度优先搜索的过程。这里头我们注意到有一个特点就是,当一个结点周围所有的灯都是亮的以后,它一定是原路返回。原路返回的这个行为在程序里面其实就对应是堆栈的出栈这个行为。下面就是我们深度优先搜索的一个算法的伪代码。这个程序看上去略眼熟,如果你去想一下树,这个实际上相当于是树的先序遍历。整个DFS其实是树的先序遍历的一个推广。

void DFS ( Vertex V ) 
{ 
	visited[ V ] = true; 
	for ( V 的每个邻接点 W )
		if ( !visited[ W ] )
			DFS( W );
}

接下来我们的问题就是,如果给定的这个图里面有n个顶点e条边,那么DFS的时间复杂度是多少呢?

用邻接表存储图,有 O ( N + E ) \rm{O(N+E)} O(N+E)
------如果我们是用邻接表来存储这个图的的话,那么 v 的每个邻接点我只要访问这个 v 对应的那条链表一个一个访问过去就好了。链表上面邻接点的总个数是等于两倍的边数。总的这个时间复杂度应该是对于个每一个点访问了一次,每条边访问了一次。
用邻接矩阵存储图,有 O ( N 2 ) \rm{O(N^2)} O(N2)
------如果我们用邻接矩阵存储图的时候,当我们在说 v 的每个邻接点的时候,实际上我们是需要把这个邻接矩阵对应 v 的一整行 n 个顶点全部都要访问一遍,我才知道哪个是他的邻接点。所以整个的复杂度是 n 里面套了一个 n。


二、广度优先搜索(Breadth First Search, BFS)

说完了DFS,我们再来看这个广度优先搜索。广度优先搜索在树里面就相当于是一个层序遍历,所以我们先来回顾一下树的层序遍历是怎么做的。

树的层序遍历:
在这里插入图片描述
我们会从根结点1出发,它是第一个被访问的,然后从上到下从左到右一层一层地访问这些结点。在程序实现的时候,我们借助了一个队列,首先把这个结点1入队。然后将1弹出访问,再把它左右两个孩子2和3继续入队。然后再把2弹出访问,再把2地两个孩子4和5入队…以此类推。
图的层序遍历:
①首先指定一个起点,将它压到队列中。
在这里插入图片描述
②然后进入那个队列的循环,把它弹出来的时候就顺序地把直接跟它有边相连的结点一一压到队列里。
在这里插入图片描述
③下一轮就从队列里面会弹出来这个2,然后把跟2直接相连的两个点8和9压到队列里。
在这里插入图片描述
④再下来弹出来的是3,然后把3相连的这个点10压到队列里.
在这里插入图片描述
⑤以此类推,我们就会把下一层也按照这样的顺序来访问,这个就是一个广度优先搜索。
在这里插入图片描述


对应的伪代码如下:

void BFS ( Vertex V ) 
{ 
	visited[V] = true; //图的入口先置为已访问
	Enqueue(V, Q);  //入队
	while(!IsEmpty(Q)){
		V = Dequeue(Q);  //出队
		for ( V 的每个邻接点 W )
		if ( !visited[W] ) {
			visited[W] = true;
			Enqueue(W, Q);
		}
	}
}

对于有N个顶点、E条边的图,BFS的复杂度是多少呢?

用邻接表存储图,有 O ( N + E ) \rm{O(N+E)} O(N+E)
------如果我们用邻接表去存这个图的话,那么会发现每一个顶点入列一次,所以一共有 n 个顶点。在for循环里头, v 的每一个邻接点被访问一次,也就意味着每条边被访问了一次。所以总体的时间复杂度是 n 加上 e 的关系。
用邻接矩阵存储图,有 O ( N 2 ) \rm{O(N^2)} O(N2)
------如果我们是用邻接矩阵来存这个图的话,遍历每个邻接结点这一步是比较耗时的,这要扫描所有的 n 的顶点,所以是 n 的平方。


三、为什么需要两种遍历

那前面我们介绍了两种不同的遍历方法,这里有一个很自然的问题就是:为什么我需要两种不同的遍历呢?所谓遍历,不过是把图里面每一个顶点访问一次,那我其实只要知道其中一种就可以了,为什么我需要知道两种呢?其实这两种遍历各有它们不一样的特点,它们的特点是什么样的呢?我们来通过一个迷宫的例子给大家演示一下:

这种迷宫我们认为它是由一个一个方格子构成的,黑色的格子表示走不通,白色的格子走得通。同时,我们规定了一个入口和一个出口。我们还要规定一下什么叫图的结点,图中的结点就是图里面的每一个白格子。如果能从一个白格子直接走到下一个,那我认为这两个结点之间是有边的。
在这里插入图片描述
DFS的解决方法
我们在访问任一个顶点相邻的这些邻接点的时候,我们得规定一个顺序。比如说我们规定了一个不太聪明的顺序,每次都从它最上方的那个格子开始考虑,然后顺时针的去考虑它周边的8个格子。
在这里插入图片描述
可以看出它的部分路径如下所示:
在这里插入图片描述
最终需要经过这么多格子才能找到出口。
在这里插入图片描述
BFS的解决方法
BFS是要一层一层地去遍历各个结点
在这里插入图片描述
最终要遍历的格子如下:
在这里插入图片描述


所以很明显的我们会看到,如果我们在这个问题里头用广度优先搜索会比深度优先搜索效果要好很多。于是你要想了,既然如此的话那我一直都用广度优先搜索就好了,我还需要深度优先搜索干什么呢?其实这么说也是不公平的,在这个问题里面,广度优先搜索表现的好是因为我把迷宫的出口设在了这。如果把出口换到另外一个位置,可能DFS效果就会更好了。


四、图不连通怎么样

前面我们讲了两种图的遍历的方法,你要注意到不管是哪一种方法,它都是从一个结点出发,然后沿着某一条边往下走的。也就意味着他访问过的所有的结点互相之间都是有直接或者间接的边去连通的。如果这个问题里面它有另外一个完全跟谁都不挨着的结点怎么办呢?它怎么能做到遍历呢?你如果用一次深度优先或者广度优先遍历,你肯定会丢掉一些孤立的结点,那么就是我们的下一个问题,图不连通的时候怎么办呢?要想清楚这个问题,首先我们得知道什么是连通。


连通的相关概念

连通:如果从VW存在一条(无向)路径,则称VW是连通的
路径VW的路径是一系列顶点 { V , v 1 , v 2 , . . . , v n , W } {\rm{\{ V,}}{{\rm{v}}_{\rm{1}}}{\rm{,}}{{\rm{v}}_{\rm{2}}}{\rm{,}}...{\rm{,}}{{\rm{v}}_{\rm{n}}}{\rm{,W\} }} {V,v1,v2,...,vn,W}的集合,其中任一对相邻的顶点间都有图中的边。路径的长度是路径中的边数(如果带权重,则是所有边的权重和)。如果VW之间的所有顶点都不同,则称简单路径
回路:起点等于终点的路径
连通图:图中任意两顶点均连通


不连通的图

连通分量:无向图的极大连通子图
------极大顶点数:再加1个顶点就不连通了
------极大边数:包含子图中所有顶点相连的所有边
我们来看个例子,下面有一张图和它的4个子图。
在这里插入图片描述
①是连通分量,因为极大边数和极大顶点数都满足。
②不是连通分量,因为满足极大顶点数但是不满足极大边数。
③是连通分量,因为极大边数和极大顶点数都满足。
④不是连通分量,因为极大边数和极大顶点数都不满足。


我们这里说的连通分量指的是无向图,对于有向图是什么概念呢?

强连通:有向图中顶点V和W之间存在双向路径,则称V和W是强连通的。也就是说我既可以从 V 走到 W,也可以从 W 走到 V,这两条往返的路径不一定是同一条,但他们一定都存在。
强连通图:有向图中任意两顶点均强连通
弱连通图:如果他不是强连通的,但我把这个图里面所有的边的方向都抹掉以后,把它变成无向图以后,他就是连通的了,那么这样的图呢叫做弱连通的。
强连通分量:有向图的极大强连通子图
举个例子,我们有下面这张图G,它有以下两个强连通分量。
在这里插入图片描述


对于连通图,之前写的遍历程序如下。

void DFS ( Vertex V ) 
{ 
	visited[ V ] = true; 
	for ( V 的每个邻接点 W )
		if ( !visited[ W ] )
			DFS( W );
}

对于不连通的图,如果要遍历所有结点,解决办法很简单。就是我要把所有的连通分量都列出来,然后对每个分量进行遍历

void ListComponents ( Graph G ) 
{ 
for ( each V in G ) 
	if ( !visited[V] ) {
		DFS( V ); /*or BFS( V )*/
}

C语言代码:DFS-邻接表存储

/* 邻接表存储的图 - DFS */

void Visit( Vertex V )
{
    printf("正在访问顶点%d\n", V);
}

/* Visited[]为全局变量,已经初始化为false */
void DFS( LGraph Graph, Vertex V, void (*Visit)(Vertex) )
{   /* 以V为出发点对邻接表存储的图Graph进行DFS搜索 */
    PtrToAdjVNode W;
    
    Visit( V ); /* 访问第V个顶点 */
    Visited[V] = true; /* 标记V已访问 */

    for( W=Graph->G[V].FirstEdge; W; W=W->Next ) /* 对V的每个邻接点W->AdjV */
        if ( !Visited[W->AdjV] )    /* 若W->AdjV未被访问 */
            DFS( Graph, W->AdjV, Visit );    /* 则递归访问之 */
}

C语言代码:BFS-邻接矩阵存储

/* 邻接矩阵存储的图 - BFS */

/* IsEdge(Graph, V, W)检查<V, W>是否图Graph中的一条边,即W是否V的邻接点。  */
/* 此函数根据图的不同类型要做不同的实现,关键取决于对不存在的边的表示方法。*/
/* 例如对有权图, 如果不存在的边被初始化为INFINITY, 则函数实现如下:         */
bool IsEdge( MGraph Graph, Vertex V, Vertex W )
{
    return Graph->G[V][W]<INFINITY ? true : false;
}

/* Visited[]为全局变量,已经初始化为false */
void BFS ( MGraph Graph, Vertex S, void (*Visit)(Vertex) )
{   /* 以S为出发点对邻接矩阵存储的图Graph进行BFS搜索 */
    Queue Q;     
    Vertex V, W;

    Q = CreateQueue( MaxSize ); /* 创建空队列, MaxSize为外部定义的常数 */
    /* 访问顶点S:此处可根据具体访问需要改写 */
    Visit( S );
    Visited[S] = true; /* 标记S已访问 */
    AddQ(Q, S); /* S入队列 */
    
    while ( !IsEmpty(Q) ) {
        V = DeleteQ(Q);  /* 弹出V */
        for( W=0; W<Graph->Nv; W++ ) /* 对图中的每个顶点W */
            /* 若W是V的邻接点并且未访问过 */
            if ( !Visited[W] && IsEdge(Graph, V, W) ) {
                /* 访问顶点W */
                Visit( W );
                Visited[W] = true; /* 标记W已访问 */
                AddQ(Q, W); /* W入队列 */
            }
    } /* while结束*/
}

小测验

1、已知一个图如下图所示,从顶点a出发按深度优先搜索法进行遍历,则可能得到的一种顶点序列为
在这里插入图片描述

A. a,e,b,c,f,d
B. a,b,e,c,d,f
C. a,c,f,e,b,d
D. a,e,d,f,c,b

答案:D

2、已知一个图如下图所示,从顶点a出发按广度优先搜索法进行遍历,则可能得到的一种顶点序列为
在这里插入图片描述

A. a,b,c,e,d,f
B. a,b,c,e,f,d
C. a,e,b,c,f,d
D. a,c,f,d,e,b

答案:B

3、具有个N(>0)顶点的无向图至多有多少个连通分量

A. 0
B. 1
C. N
D. N-1

答案:D

4、如果从无向图的任一顶点出发进行一次深度优先搜索可访问所有顶点,则该图一定是

A. 有回路的图
B. 完全图
C. 连通图
D. 一棵树

答案:C

5、具有N(>0)个顶点的无向图至少有多少个连通分量

A. 0
B. 1
C. N
D. N-1

答案:B

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值