数据结构和算法之图的遍历

简介: 6.2 图的遍历 6.2.1 图的遍历——DFS 遍历:把图里面每个顶点都访问一遍而且不能有重复的访问 深度优先搜索(DFS) 当访问完了一个节点所有的灯后,一定原路返回对应着堆栈的出栈入栈的一个行为 深度优先搜索的算法描述void DFS(Vertex V)//从迷宫的节点出来{visited[V] = true;//给每个节点一个变量,true相当于灯亮了,false则是熄灭状态for(V的每个邻接点W)//视野看得到的灯if(!visited[W])//检测是否还有没点亮的DFS(W);//递归调用}//类似树的先序遍历若有N个顶点、E条边,时间复杂度是用邻

6.2 图的遍历

6.2.1 图的遍历——DFS

遍历:把图里面每个顶点都访问一遍而且不能有重复的访问

深度优先搜索(DFS)

当访问完了一个节点所有的灯后,一定原路返回对应着堆栈的出栈入栈的一个行为

深度优先搜索的算法描述

void DFS(Vertex V)//从迷宫的节点出来

{

visited[V] = true;//给每个节点一个变量,true相当于灯亮了,false则是熄灭状态

for(V的每个邻接点W)//视野看得到的灯

if(!visited[W])//检测是否还有没点亮的

DFS(W);//递归调用

}

//类似树的先序遍历

若有N个顶点、E条边,时间复杂度是

用邻接表存储图,有O(N+E)//对每个点访问了一次,每条边也访问了一次

用邻接矩阵存储图,有O(N²)//V对应的每个邻接点W都要访问一遍

6.2.2 图的遍历——BFS

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

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条边,时间复杂度是

用邻接表存储图,有O(N+E)

用邻接矩阵存储图,有O(N²)

实例说明

第1步:访问A。

2步:访问(A的邻接点)C。在第1步访问A之后,接下来应该访问的是A的邻接点,即"C,D,F"中的一个。

但在本文的实现中,顶点ABCDEFG是按照顺序存储,C"DF"的前面,因此,先访问C

3步:访问(C的邻接点)B。在第2步访问C之后,接下来应该访问C的邻接点,即"BD"中一个(A已经被

访问过,就不算在内)。而由于BD之前,先访问B

4步:访问(C的邻接点)D。在第3步访问了C的邻接点B之后,B没有未被访问的邻接点;因此,返回到

访问C的另一个邻接点D

5步:访问(A的邻接点)F。 前面已经访问了A,并且访问完了"A的邻接点B的所有邻接点(包括递归的邻

接点在内)";因此,此时返回到访问A的另一个邻接点F

6步:访问(F的邻接点)G

7步:访问(G的邻接点)E

因此访问顺序是:A -> C -> B -> D -> F -> G -> E

当然,上图是基于无向图,具体的代码在文章后面实现。

广度优先搜索

广度优先搜索算法(Breadth First Search),又称为"宽度优先搜索""横向优先搜索",简称BFS

它的思想是:从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这

些邻接点出发依次访问它们的邻接点,并使得先被访问的顶点的邻接点先于后被访问的顶点的邻接点被

访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另

选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。

换句话说,广度优先搜索遍历图的过程是以v为起点,由近至远,依次访问和v有路径相通且路径长度为

1,2…的顶点。

实例说明

第1步:访问A。

2步:依次访问C,D,F。在访问了A之后,接下来访问A的邻接点。前面已经说过,在本文实现中,顶点

ABCDEFG按照顺序存储的,C"DF"的前面,因此,先访问C。再访问完C之后,再依次访问D,F

3步:依次访问B,G。在第2步访问完C,D,F之后,再依次访问它们的邻接点。首先访问C的邻接点B,再

访问F的邻接点G

4步:访问E。在第3步访问完B,G之后,再依次访问它们的邻接点。只有G有邻接点E,因此访问G的邻

接点E

因此访问顺序是:A -> C -> D -> F -> B -> G -> E。

6.2.3 图的遍历——为什么需要两种遍历

在不同的情况下效率不同

广度跟深度的区别

  • 1. 深度是直接一条路走到黑,碰壁没路走了在返回
  • 2. 广度是一圈一圈的扫描过去,虽然前面还有路也不会强行深入

6.2.4 图的遍历——图不连通怎么办

连通:如果从V到W存在一条(无向)路径,则称V与W是连通的

路径VW路径是一系列顶点{V,v1,v2,....,vn,W}的集合,其中任一对相邻的顶点间都有图中的边。

路径的长度是路径中的边数(如果带权(带权图),则是所有边的权重和)。如果V到W之间的所有顶点都不同, 则称为简单路径(有回路就不是简单路径)

回路:起点等于终点的路径

连通图:图中任意两顶点均连通

连通分量:无向图的极大连通子图

  • 1. 极大顶点数:再加1个顶点就不连通了
  • 2. 极大边数:包含子图中所有顶点相连的所有边

对有向图

//强连通:有向图中顶点VW之间存在双向路径,则称VW是强连通的(路径可以不同同一条,但是一定是连

通的)

//强连通图:有向图中任意两顶点均强连通

//强连通分量:有向图的极大强连通子图

//弱连通图:将强连通图的所有边的方向抹掉变成无向图就是连通的了

每调用一次DFS(V),就把V所在的连通分量遍历了一遍,BFS也一样

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)

}

}

6.3 应用实例:拯救007

void Save007(Graph G)

{

for(each V in G){

if(!visited[V] && FirstJump(V)){//这个FirstJump(V)是007第一跳有没有可能从孤岛

跳到V上有没有可能,有且没踩过就跳上去

answer = DFS(V);//or BFS(V)

if(answer == YES) break;0

}

}

if(answer == YES) output("Yes");

else output("No");

}

DFS算法

void DFS(Vertex V)

{

visited[V] = true;//表示鳄鱼头踩过了

for(V的每个邻接点W)

if(!visited[W])

DFS(W);//递归

}

改良版本

void DFS(Vertex V)

{

visited[V] = true;//表示鳄鱼头踩过了

if(IOsSafe(V)) answer = YES;

else{

for(each W in G )

if(!visited[W] && Jump(V,W)){//可以从V jump跳到这个w上面,作用是算V到W之间的距

离是不是小于007可以跳跃最大距离

answer = DFS(W);//递归

if(answer == YES) break;

}

}

return answer;

}

6.4 应用实例:六度空间(Six Degrees of Separation)

理论:

  1. 你和任何一个陌生人之间所间隔的人不会超过6个
  2. 给定社交网络图,请对每个节点计算符合"六度空间"理论的结点占结点总数的百分比

算法思路

1.对每个节点进行广度优先搜索

2.搜索过程中累计访问的节点数

3.需要记录"层",仅计算6层以内的节点数

void SDS()

{

for(each V in G){

count += BFS(V);

Output = (count/N);

}

}

//结合最初的BFS

void BFS(Vertex V)

{

visited[V] = true;count = 1;

Enqueue(V,Q);//压到队列里

while(!IsEmpty(Q)){

V = Dequeue(Q);//每次循环弹出一个节点

for(V的每个邻接点W)

if(!visited[W]){//没有访问过的去访问将其压入队列中

visited[W] = true;

Enqueue(W,Q);count++;

}

}return count;

}

另外的解决方案

int BFS(Vertex V)

{

vistex[V] = true;count = 1;

level = 0;last = V;

Enqueue(V,Q);

while(!IsEmpty(Q)){

V = Dequeue(Q);

for( V的每个邻接点W)

if(!visited[W]){

visited[W] = true;

Enqueue(W,Q);count++;

tail = W;

}

if(V == last ){

level++;last = tail;

}

}

return count++;

}

相关文章
|
5月前
|
存储 监控 安全
企业上网监控系统中红黑树数据结构的 Python 算法实现与应用研究
企业上网监控系统需高效处理海量数据,传统数据结构存在性能瓶颈。红黑树通过自平衡机制,确保查找、插入、删除操作的时间复杂度稳定在 O(log n),适用于网络记录存储、设备信息维护及安全事件排序等场景。本文分析红黑树的理论基础、应用场景及 Python 实现,并探讨其在企业监控系统中的实践价值,提升系统性能与稳定性。
158 1
|
5月前
|
存储 监控 算法
基于跳表数据结构的企业局域网监控异常连接实时检测 C++ 算法研究
跳表(Skip List)是一种基于概率的数据结构,适用于企业局域网监控中海量连接记录的高效处理。其通过多层索引机制实现快速查找、插入和删除操作,时间复杂度为 $O(\log n)$,优于链表和平衡树。跳表在异常连接识别、黑名单管理和历史记录溯源等场景中表现出色,具备实现简单、支持范围查询等优势,是企业网络监控中动态数据管理的理想选择。
150 0
|
9月前
|
存储 算法 Java
算法系列之数据结构-二叉树
树是一种重要的非线性数据结构,广泛应用于各种算法和应用中。本文介绍了树的基本概念、常见类型(如二叉树、满二叉树、完全二叉树、平衡二叉树、B树等)及其在Java中的实现。通过递归方法实现了二叉树的前序、中序、后序和层次遍历,并展示了具体的代码示例和运行结果。掌握树结构有助于提高编程能力,优化算法设计。
286 10
 算法系列之数据结构-二叉树
|
9月前
|
算法 Java
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
242 3
 算法系列之数据结构-Huffman树
|
9月前
|
算法 Java
算法系列之数据结构-二叉搜索树
二叉查找树(Binary Search Tree,简称BST)是一种常用的数据结构,它能够高效地进行查找、插入和删除操作。二叉查找树的特点是,对于树中的每个节点,其左子树中的所有节点都小于该节点,而右子树中的所有节点都大于该节点。
351 22
|
10月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
362 30
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
291 59
|
6月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
123 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
11月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
515 77