一文搞懂深度优先遍历

修改时间:2021.11.15

写在前面

图及其遍历算法的基本概念网上教程有很多,这里不在进行赘述。
本文侧重于算法的实现及其思想的讨论,所有代码均为伪代码,意在能简洁明了的阐述算法。

一、 “原生”dfs

1.分析

深度优先搜索算法需要分别实现以下内容:
1.访问顶点的实现
2.“依次从顶点V的未被访问的邻接点出发进行深度遍历”
其中 2 主要涉及:
a.顶点是否被访问标识;
b.顶点v的各邻接点的求解;
c.“从各邻接点出发深度遍历”的实现。

2.代码实现:

基于上述讨论可得dfs算法描述如下:

void dfs(graph G,int v)  //从顶尖V出发对图G进行深度优先搜索遍历
{ int w;
  visite(v);			 //访问顶点v
  visited[v]=TRUE;		//设置访问标志
  w=firstadj(G,v);		//求v的邻接点,作为深度遍历的新起点
  while(w!=0)			//当还有邻接点时
  {
	if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
	  dfs(G,w);
	w=nextadj(G,v,w);	//求下一个邻接点
   }
}
//完整遍历图的主算法
void dfs_teavel(graph G)
{ int i;
  for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
  	visited[i]=FALSE;
  for(i=1;i<=nodes(g);i++) //依次选择未被访问过的顶点作为起点来深度遍历
  	if(visited[i]=FALSE)
  		dfs(G,i);
}

注:

其中引用了两个求邻接点的函数:
firstadj(G,v)–返回图G中顶点v的第一个邻接点的序号,若不存在,则返回0;
nextadj(G,v,w)–返回图G中顶点v的邻接点中处于邻接点w之后的那个邻接点,若不存在,则返回0;
其中求邻接点个数的函数:
nodes(g)–返回图G中的顶点数;

另外算法中的访问顶点的操作visite(v)对每个顶点执行一次且仅执行一次。其具体执行内容在不同场合可以有不同含义。

3.时间复杂度分析:

整个算法要访问每个顶点,在访问每个顶点后,搜索其邻接点是花费时间最多的部分,并且所需要的时间取决于存储结构。
采用邻接矩阵存储图,对每个顶点来说,搜索其所有邻接点需要搜索矩阵中对应的整个一行,由此可知,算法时间复杂度为O(n²);
采用邻接表存储图,对每个顶点来说,搜索其所有邻接点需要搜索邻接表中对应链表的各结点,虽然各顶点邻接表的长度可能大小不一,但整个图的遍历所搜索的邻接点总数就是邻接点的结点数,也就是图的边数e(在无向图时,是2e),由此可知,算法的时间复杂度是O(n+e)

二、深度遍历算法的应用

1.设计算法求解无向图 G 的连通分量的个数

分析:

对无向图G来说,选择某一顶点v执行dfs(v),可访问到所在连通分量的所有顶点,故为了遍历整个图而 选择起点的次数 就是图G的连通分量数,而这可通过修改dfs_travel来实现:每调用一次dfs算法计数一次。

代码实现:

其中涉及dfs算法可直接照搬,为了清晰性,故此处省略。

int dfs_teavel(graph G)
{ int i;
  int k;					//增加变量k用于连通分量的计数
  for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
  	visited[i]=FALSE;
  for(i=1;i<=nodes(g);i++) //依次选择未被访问过的顶点作为起点来深度遍历
  	if(visited[i]=FALSE)
  	{
	  k++; 				//用k来累计连通分量个数
	  dfs(G,i);
	}
return k;
}

拓展:

以下算法与此算法设计思想相同:
a.判断无向图是否连通
(还可以通过dfs后,遍历visited数组是否都为true)
b. 设计算法判断有向图 G 中顶点 V0 到每一个顶点是否都有路径,若是,返回true,否则返回false。

2.设计算法求出无向图 G 的边数

分析:

在执行dfs(v)时,每搜索到一个邻接点即意味着搜索到一条以v为一个端点的边或弧,故应在dfs算法中计数;
由于遍历算法保证每个顶点都要被访问一次,也就意味着每个顶点都要对应的作为起点调用dfs算法一次,因此每个顶点相关联的边都会被计算在内,。由此导致无向图中每条边被计算两次,故最后边数结果应除以2才是实际边数

代码实现:
void dfs(graph G,int v)  //从顶尖V出发对图G进行深度优先搜索遍历
{ int w;
  visited[v]=TRUE;		//设置访问标志
  w=firstadj(G,v);		//求v的邻接点,作为深度遍历的新起点
  while(w!=0)			//当还有邻接点时
  {
  	E++;				//此处意味着找到一天边,故累计到变量E中
	if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
	  dfs(G,w);
	w=nextadj(G,v,w);	//求下一个邻接点
   }
}
//完整遍历图的主算法
int Enum(graph G)
{ int i;
  int E = 0;				//增加全局变量E用于记录整个图中的边数
  for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
  	visited[i]=FALSE;
  for(i=1;i<=nodes(g);i++) //依次选择未被访问过的顶点作为起点来深度遍历
  	if(visited[i]=FALSE)
  		dfs(G,i);
  return E/2;
}

3.设计算法判别有向图 G 是否是一颗以 v0 为根的有向树

分析:

为了设计算法首先要明确相关概念,按照定义,v0的入度为0,其余各顶点的入度值为1.
即该算法需要对每个顶点判断其入度值是否符合定义的要求
为了实现这一判断,可以从v0出发深度遍历,如果每个顶点仅访问一次且仅访问一次则符合要求,判断成立

代码实现:
void dfs(graph G,int v)  //从顶尖V出发对图G进行深度优先搜索遍历
{ int w;
  visited[v]=TRUE;		//设置访问标志
  vnum++;				//对访问顶点操作计数
  w=firstadj(G,v);		//求v的邻接点,作为深度遍历的新起点
  while(w!=0)			//当还有邻接点时
  {
  	E++;				//此处意味着找到一天边,故累计到变量E中
	if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
	  dfs(G,w);
	else judge =FALSE;	//执行到此,标明顶点w被重复访问,故设置失败标志
	w=nextadj(G,v,w);	//求下一个邻接点
   }
}

BOOL IS_DirectedTree(graph G,int v0)
{ int i;
  bool judge=TRUE;				//judge用于记录判断的信息,初始情况下假设为true
  for(i=1;i<=nodes(g);i++) 		//初始化各顶点访问标志
  	visited[i]=FALSE;
  int vnum=0;					//记录访问过的顶点数
  dfs(G,vo);				   //仅从v0出发
  return vnum==n && judge;
}

注:

由于是判断型算法,故将返回值类型定义为BOOL型;
为了在两个函数之间传递判断结果,采用了定义bool行全局变量的方法

☆4.设计算法在图中找出一条包含所有顶点的简单图

分析:

搜索这一路径的过程就是深度优先搜索过程,过程中可能会遇到当前顶点(如顶点2)无邻接点,不能再往下搜索了,需要回溯到上一顶点(顶点1),注意此时需要取消顶点2的访问标志,为了使该顶点可被重新搜索,该操作可以放在dfs算法之后;
搜索成功的条件就是当前路径上顶点数正好等于n,因此在求解过程中需要记录当前路径上的顶点数。
还需将路径上的顶点依次保存到数组(严格说是栈)中

代码实现:
void dfs(graph G,int v)  //从顶尖V出发对图G进行深度优先搜索遍历
{ int w;
  visited[v]=TRUE;		//设置访问标志
  num++;				//记录路径上的顶点数
  A[num]=v0;			//保存路径上的顶点
  if(num==nodes(g)) 	//符合条件时输出路径
  	print(A);
  w=firstadj(G,v0);		//求v0的第一个邻接点
  while(w!=0)			//当还有邻接点时
  {
	if(visited[w]=FALSE) //从不在路径上的的邻接点出发深度遍历
	  dfs(G,w);
	w=nextadj(G,v0,w);	//求下一个邻接点
   }
}

void Hamilton(graph G)
{ int i;
  for(i=1;i<=nodes(g);i++) 		//初始化各顶点访问标志
  	visited[i]=FALSE;
  int num=0;					//记录路径上的的顶点数
  for(i=1;i<=nodes(g);i++) 
  	dfs(G,i);					//以每个顶点作为起点来搜索路径

}

注:

调用dfs及其调用前的准备工作,其中准备部分的操作与深度优先搜索遍历的准备部分操作相同,不同的是调用操作。

☆5.设计算法判断无向图G中顶点v是否是一个关节点。

(在无向图G中,若删除顶点v及其相关的边后,使得v所在的连通分量被分割为两个或两个以上,则称顶点v为关节点。)

分析:

在深度遍历算法中,如果某顶点被访问了,则不会再访问,并且也不会由此往下进行遍历,从而起到了“删除”的作用;
本算法可先访问顶点v,然后从其一个邻接点出发做一次深搜,再检查是否有v的林及诶单未被访问,若有,则说明是关节点。

代码实现:
void dfs(graph G,int v0)  
{ visited[v0]=TRUE;		
 
  w=firstadj(G,v0);		//求v0的第一个邻接点
  while(w!=0)			//当还有邻接点时
  {
	if(visited[w]=FALSE) //从不在路径上的的邻接点出发深度遍历
	  dfs(G,w);
	w=nextadj(G,v0,w);	//求下一个邻接点
   }
}
BOOL judge(graph G,int v0)
{ int num=0;				//全局变量num用于存放不能被访问到的顶点数
  for(i=1;i<=nodes(g);i++)
  	visited[i]=FALSE;
  visited[v0]=TRUE;		//置v0的访问标志位已访问状态
  w=firstadj(G,v0);		//求v0的第一个邻接点
  if(w!=0)
  { dfs(G,w);
  	w=nextadj(G,v0,w);	//求下一个邻接点
  	while(w!=0)			//当还有邻接点时
  {
	if(visited[w]=FALSE) //从不在路径上的的邻接点出发深度遍历
	  num=num+1;		//统计v0的未被访问的顶点数
	w=nextadj(G,v0,w);	//求下一个邻接点
   }
  }
}
注:

如果给出具体的存储结构,则可以在其存储结构上进行删除。

6.设计算法判断连通的无向图G中顶点v是否满足在删除顶点v及其相关边之后,图G仍然是联通的。

分析:

如果删除v后图仍然连通,则从某顶点出发访问能遍历整个图,即一边访问,一边计数

代码实现:
void dfs(int v)  
{ visited[v]=TRUE;	
  n=n+1;				//设置访问标志并计数	
  w=firstadj(G,v);		//求v的第一个邻接点
  while(w!=0)			//当还有邻接点时
  {
	if(visited[w]=FALSE) //从不在路径上的的邻接点出发深度遍历
	  dfs(G,w);
	w=nextadj(G,v,w);	//求下一个邻接点
   }
}
BOOL IsACutPoint(graph G,int v)
{  for(i=1;i<=nodes(G);i++)
   	  visited[i]=FALSE;
   visited[v]=TRUE;	n=1;  //实现“删除”功能
   dfs(firstadj(G,v));    //从v的一个邻接点出发遍历
   return n!=nodes(G);    //依据访问的顶点数放回判断结果
}
注:

注意与5的对比,算法中“删除”功能的实现;

7.设计算法判断顶点vi到vj之间是否存在路径。

分析:

可以从vi开始遍历,只需在遍历中判断当前顶点是否是vj即可
注意flag标志的使用

代码实现:
void dfs(graph G,int u,int v,bool&flag)  
{ int w;
  visited[v]=TRUE;		//设置访问标志
  if(u==v)				//说明找到了重点
  	flag=true;
  w=firstadj(G,u);		//w指向u的第一个邻接点
  while(w!=0)			//当还有邻接点时
  {
	if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
	  dfs(G,w,v,flag);
	w=nextadj(G,u,w);	//求下一个邻接点
   }
}

void dfs_teavel(graph G,int i,int j)
{ int i; bool flag=false;   //flag用于记录判断的信息,初始情况下假设为false
  for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
  	visited[i]=FALSE;
  dfs(G,i,j,flag);
  return flag;
}

8.设计算法判断无向图G是否是一棵树。

分析:

一个无向图是一棵树的条件为:G必须是无回路的连通图或n-1条边的连通图
后者更好判断,所以采用深搜,从任一点出发,判断是否能遍历n-1条边(即判断是否有n-1条边),并且是否能遍历所有顶点(即判断是否连通)

代码实现:
void dfs(graph G,int v) 
{ int w;
  vn++;					//顶点自增
  visited[v]=TRUE;		//设置访问标志
  w=firstadj(G,v);		//求v的邻接点,作为深度遍历的新起点
  while(w!=0)			//当还有邻接点时
  {
  	en++;				//此处意味着找到一天边,故累计到变量E中
	if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
	  dfs(G,w);
	w=nextadj(G,v,w);	//求下一个邻接点
   }
}

BOOL Is_Tree(graph G)
{ int i,n=nodes(g);
  int en=0,vn=0;				//en vn分别为边数和顶点数
  for(i=1;i<=n;i++)            //初始化各顶点访问标志
  	visited[i]=FALSE;
  dfs(G,1);						//从顶点1开始遍历
  if(vn==n && en==2*(n-1))      
  	return true;
  return false;
}
注:

最后的判断条件中,要注意到无向图的每个边会被遍历两次;

写在后面

能力有限,写的算法可能不是最优的,不足之处恳请各位大佬及时指出,谢谢~

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

与 或

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

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

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

打赏作者

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

抵扣说明:

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

余额充值