其他算法-DFS

DFS和BFS有所联系,BFS是广度优先搜索,DFS则是深度优先搜索;BFS可以借助队列实现,DFS则依靠堆栈实现


BFS回顾:其他算法-BFS


堆栈

堆栈的定义

堆栈(stack)简称栈,是一种基本的数据结构,具有先进后出的特点:FILO-First In Last Out,换句话说,只有栈顶元素可见;
举个例子:

  • 1.在生活中,洗完盘子叠起来,下次用盘子会拿出最后洗的那一个;
  • 2.浏览器中,可以支持网页回退;
  • 3.汇编语言调用子程序时,进行现场保护,会把当前的变量保存到堆栈,事实上任何函数调用都会这么做;

堆栈的操作

1.判断堆栈是不是空,时间复杂度为 O ( 1 ) O(1) O(1)
2.访问栈顶元素而不出栈,时间复杂度为 O ( 1 ) O(1) O(1)
3.新元素入栈,即push,时间复杂度为 O ( 1 ) O(1) O(1),注意,如果栈的空间已经不够,会发生上溢出"overflow";
4.旧元素出栈,即pop,时间复杂度为 O ( 1 ) O(1) O(1),注意,如果栈已经为空,会发生下溢出"underflow";
5.清空栈,如果是逐个元素出栈,时间复杂度为 O ( n ) O(n) O(n)
6.一般认为堆栈的空间复杂度为 O ( n ) O(n) O(n)

堆栈的实现

堆栈可以由数组或者链表实现,使用数组实现如下:
fig1

  • 初始化时,top=0,容量为n(注意top是开区间,top指向的位置不存储对象);
  • 判断堆栈为空,即判断top是否等于0;
  • 判断堆栈为满,即判断top是否等于n;
  • 入栈时:
stack[top]=elem
top+=1

如果用C++,可以简写为:stack[top++]=elem,在C++中,单目运算top++会先返回top给stack去索引,然后top才自加1;

  • 出栈时:
top-=1
elem=stack[top]
同样,使用C++可以简写,注意单目运算符这次在操作数之前:
elem=stack[--top]
  • 访问栈顶:stack[top-1]
  • 访问栈底:stack[0]

对于上图,top=4,n=7,可见stack[4]目前是没有对象的;

DFS算法

算法举例

DFS和BFS一样,也是会常用在隐式图的搜索,对于下面的树(树也是图):
fig2
如果用DFS进行遍历,流程为:

  • 假设先从根节点1开始,节点1入栈,2和3还没有访问过;
  • 节点2入栈,节点1已经访问过,4和5还没访问;
  • 节点4入栈,相邻的2已经访问过,节点4出栈,栈顶变成节点2;
  • 节点2相邻的还有5没有访问,节点5入栈,此时5的相邻节点2是访问过的,节点5出栈,栈顶变成节点2;
  • 现在节点2的相邻节点都是访问过的,节点2出栈,栈顶变成节点1;
  • 节点3还没访问,节点3入栈,同理,节点6入栈;
  • 节点6的相邻点已访问,所以6,3,1依次出栈,栈空;

算法流程

容易看出,因为使用了栈的特性,DFS确实是优先向深度方向发展,总结DFS如下:

选择一个起点入栈;
while(堆栈不为空)
{
	取出栈顶元素x;
	if x还有没访问的邻居y
	{
		访问该邻居y,同时y入栈;
	}
	else
	{
		x出栈;
	}
}

DFS应用-二叉树遍历

DFS可用于二叉树的后序遍历(Postorder),先序遍历(Preorder),中序遍历(Inorder);
先定义二叉树:

class BinaryTree:
     def __init__(self, x):
         self.val = x
         self.left = None
         self.right = None

注意遍历是相对根而言的:
1.后序:先遍历左子树,再遍历右子树,最后访问根;

def postorder(root):
	if root == None:
		return None
	postorder(root.left)
	postorder(root.right)
	print(root.val)

2.先序:先访问根,再遍历左子树,最后遍历右子树;

def preorder(root):
	if root == None:
		return None
	print(root.val)
	preorder(root.left)
	preorder(root.right)

3.中序:先遍历左子树,再访问根,最后遍历右子树;

def inorder(root):
	if root == None:
		return None
	inorder(root.left)
	print(root.val)
	inorder(root.right)

看到这里会想到,这和堆栈的关系是什么,并没有发现使用了堆栈;
事实上,递归的过程就是调用函数,即已经使用了系统堆栈,比如postorder,在调用postorder(root.left)时,根节点会被列入现场保护的对象而入栈,当访问了左子树后(左子树节点陆续入栈又出栈),同时恢复现场,根节点从栈顶弹出,再调用postorder(root.right),根节点又入栈,重复类似上述的操作,最后恢复现场根节点出栈,最后打印(访问)这个根节点;
可以看出,虽然没有显式使用堆栈,但递归的作用使得搜索依旧是深度优先的;


回顾上面的算法举例

  • 容易看出,如果在节点入栈的同时打印节点,其实就是Inorder;
  • 如果在节点出栈后打印节点,就是Postorder,这正好和上面分析的递归流程吻合;
  • 注意:如果递归过深,很容易导致系统堆栈空间不够;

DFS与BFS的比较

BFS在广度上优先搜索,搜索是一层一层递进,而DFS则是从一个起点开始,按深度搜索,一直到该支路没有元素才从其他分支继续搜索;
可见,DFS不一定能得到最短路径,因为它会错过最短路径所在的分支;

扩展:DFS计算强连通分量

强连通分量

在一幅无向图中,如果有一条路径连接点v和w,则它们就是连通的;在一幅有向图中,如果从顶点v有一条有向路径可到达顶点w,则顶点w是从顶点v可达的。
如果两个顶点互相可达,则它们是强连通的。如果一幅有向图中任意两个顶点都是强连通的,则这幅有向图也是强连通的。
有向图中的强连通性有着以下性质:

  • 自反性: 任意顶点和自己都是强连通的;
  • 对称性:如果v和w是强连通的,那么w和v也是强连通的;
  • 传递性:如果v和w是强连通的且如果w和x也是强连通的,那么v和x也是强连通的;

强连通性将所有顶点分成了平等的部分,每个部分都是由相互均为强连通的顶点的最大子集组成。这些子集称为强连通分量(强连通分支),比如下图分为5个强连通分量:
fig3

无向图连通分量的计算

连通分量是对于无向图而言的,即为连通子图数量:
fig4
以上无向图一共两个连通分量;
对于一个无向图的连通分量,从连通分量的任意一个节点开始,进行一次DFS,一定能遍历这个连通分量的所有节点。所以,整个图的连通分量数等于遍历整个图至少进行多少次DFS,一次DFS中遍历的所有节点必然属于同一个连通分量;

注意:将图中的节点依次作为DFS遍历的起点,每次DFS遍历后包含元素与之前得到集合完全相同的情况其实是同一个子图,不计入调用DFS的次数即,从而得到使用DFS遍历整个图的最少次数;

Kosaraju算法

Kosaraju(科萨拉朱)算法用于计算有向图的强连通分量,算法描述为:

  • step1.对原图进行DFS,可以得到递归树(递归过程的图像表达),对递归树进行后序遍历,元素依次入栈;
  • step2.原图取反向图(有向图的方向颠倒),取栈顶元素出栈,该元素作为反向图DFS的起点,标记可以遍历到的节点(之前访问过的不标记),这些节点就是一个连通分量;
  • step3.如果还有节点未标记,继续step2,否则就结束算法;

举例

现在有一个图:
fig5
任意取节点1,进行DFS,得到递归树:
fig6
该树的后序遍历为3 2 6 7 9 8 5 4 1,注意1在栈顶,3在栈底
原图取反:
fig7
在反向图中依次出栈进行DFS(对于之前访问过的节点需要停止访问),得到递归森林:
fig8
3棵递归树即3个强连通分量;


在对原图进行DFS时,可能会得到递归森林:
因为一次DFS不能遍历完整个图,就需要从未访问的节点再次DFS,搜索过程中,遇到访问过的点需停止该分支的搜索;
注意:如果得到的是递归森林,就依次把其中的递归树后序遍历并入栈


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值