这篇文章主要介绍的是基于图的BFS算法的思想与实现,文章主要分为四个部分:
第一部分,介绍BFS的思想与应用场景
第二部分,介绍BFS算法基于两种不同的数据结构的算法伪代码描述
第三部分,介绍基于上一篇文章中两种图的不同数据结构(邻接矩阵,邻接表),给出BFS算法实现的代码
第四部分,总结BFS和DFS算法的时间复杂度,和二者之间的共性与区别
参考书目《图论算法理论、实现与应用》
第一部分:
BFS 即广度优先搜索(Breadth First Search , BFS ) 是一个分层搜索的过程, 与 DFS 深度优先搜索不同的是,
是没有回退过程的,基于的是非递归的思想。BFS 的算法思想是,对一个无相连通图,在访问图中的某一个
起始顶点 v 之后,将顶点 v 标志为已访问,然后将顶点 v 压入数据结构队列的尾部,然后通过队列是否为空控制循环,
每次从队列头部取出一个元素 v , 再由顶点 v 点出发,访问与顶点 v 相邻接的且未被访问的所有的顶点,
每次访问一个顶点都对该顶点进行标示已访问,然后,将访问过的顶点全部存放到队列的数据结构中。
直至所有的顶点都被访问,即队列同时也会为空,因为所有的顶点都已经访问过,都不满足未被访问的入队列条件。
到此算法结束。
第二部分:
这里总共分为两个方向介绍,
1.基于邻接矩阵的算法伪代码描述
BFS_matrix( 起始顶点 start )
{
queue q
visited [start] = 1
q <- start
while ( ! q.empty() )
begin while
k <- q.front()
for i <- 0 to vexnum
begin for
if map[k][i] == 1 and !visited [ i ]
begin if
q <- i
visited[i] <- 1
end if
end for
end while
}
被访问过将 visited [i] 置为数值 1 ,未被方位将 visited [ i] 置为 0 。
map 是用来描述图中两点之间边的状况的邻接矩阵,详细的介绍请见前一个博客(link)。
2.基于邻接表的算法伪代码描述
BFS_list ( start )
{
queue q
visited[ start ] <- 1
q <- start
while ( !q.empty() )
begin while
k <- q.front( )
p <- graph.nodes[k]' head pointer // (1)
while ( p )
begin inner while
if !visited[p->adjvex]
begin if
visited[p->adjvex] <- 1
q<- p->adjvex
end if
p <- p->nextarc // (2)
end inner while
end while
}
在获得访问的顶点标号之后,通过这个顶点标号 k 来获得邻接链表中的该顶点对应的邻接链表的头指针, 将头指针赋值给 p
然后通过一个while 循环来访问邻接链表中的各个顶点,这些顶点根据邻接链表的特点,既然存放在与顶点相邻接的链表中,
及说明这些顶点一定是与顶点 k 是相邻接的, 所以接下来的语句只要判断这个与 k 顶点相邻接的顶点是否被访问过了,
如果没有被访问过,则满足入队列条件, 将其访问字段置为已访问过,然后加入到队列的尾部即可。
注释 (2) : 这个地方主要是想要解释一下 p->adjvex , 具体的结构体定义请看前一篇文章, 这里的 p 所指代的是指向当前从队列中
出队列的整数值所对应的邻接链表中的当前结点下标(在这里我们将其设定为 x ),通过这个顶点下标,
我们可以将指针指向邻接链表中的与该顶点相连接的边表。
而p->adjvex 表示的是与当前顶点 x 通过边相邻接的,边的另一端的顶点的标号。也就是与顶点 x 相邻接的顶点标号。
在这里的算法可以抽象的看成是由两层while 循环所构成的, 外层循环的循环条件是由队列是否为空来进行判断的,
而队列中的元素数值是由整个图中的顶点的总数所决定的。
内层循环是由指向邻接表中某一个链表中的结点个数所决定的, 并且在内层循环中,通过不断地对没有访问过的结点进行访问操作,
也在不断的更新队列中的元素个数,使得队列中的元素个数在图中的所有顶点还没有完全被访问的前提条件下,不至于为空而终止外层循环。
第三部分:
1. 首先给出基于邻接矩阵的代码实现:
//BFS_matrix.hpp
#include <cstdio>
#include <queue>
#define INF 1000000
#define MAXN 100
using namespace std ;
int map[MAXN][MAXN] , visited[MAXN] ;
int vexnum , arcnum ;
int directed ;
void createGraph_matrix()
{
int from , to ;
// from [0 , vexnum-1] ; to [0 , vexnum -1 ]
printf("vertex num , arc num \n") ;
scanf("%d%d",&vexnum , &arcnum ) ;
for( int i = 0 ; i < vexnum ; i++ )
for( int j = 0 ; j < vexnum ; j++ )
{
map[i][j] = map[j][i] = INF ;
}
for(int i = 0 ; i < vexnum ; i++ )
visited[i] = 0 ; //initial all vertexes unvisited
printf("directed ? (0: undirected , 1: directed)") ;
scanf("%d", &directed ) ;
for ( int i = 0 ; i < vexnum ; i++ )
{
printf("from to [0 -> %d] \n", vexnum-1 ) ;
scanf("%d%d", &from , &to ) ;
map[from][to] = 1 ;
if (!directed) // undirected graph , map[from][to] = map[to][map]
map[to][from] = 1 ;
}
}
void BFS_matrix( int start )
{
queue<int> q ;
visited[start] = 1 ;
printf("we begin visiting from vertex %d \n" , start ) ;
q.push( start ) ;
while ( !q.empty() )
{
int k = q.front() ;
q.pop() ;
for ( int i = 0 ; i < vexnum ; i++ )
{
if ( map[k][i] == 1 && !visited[i] )
{
visited [i] = 1 ;
printf("we are visiting vertex %d \n" , i ) ;
q.push( i ) ;
}
}
}
}
//listBFS.hpp
#include <cstdio>
#include <queue>
#define INF 100000
#define MAXN 100
using namespace std ;
struct ArcNode
{
int adjvex ;
ArcNode *nextarc ;
} ;
struct VNode
{
int data ;
ArcNode *head1 ;
ArcNode *head2 ;
} ;
struct LGraph
{
int vexnum , arcnum, directed ;
VNode nodes[MAXN] ;
} ;
//int visited [ MAXN ] ;
LGraph lGraph ;
void createGraph_list()
{
ArcNode *pi ;
int from , to ;
//gets input data
printf("vexnum , arcnum \n") ;
scanf("%d%d" , &lGraph.vexnum , &lGraph.arcnum ) ;
printf("directed ? (0: undirected ) (1:directed )\n") ;
scanf("%d", &lGraph.directed) ;
//initial vertex , arc ,direction
for ( int i = 0 ; i < lGraph.vexnum ; i++ )
{
visited[i] = 0 ;
//set all vertexes unvisited
}
for ( int i = 0 ; i < lGraph.vexnum ; i++ )
{
lGraph.nodes[i].head1= lGraph.nodes[i].head2 = NULL ;
}
//gets vertexs
for (int i = 0 ; i < lGraph.arcnum ; i++ )
{
printf("from to [0 -> %d ]\n", lGraph.vexnum -1 ) ;
scanf("%d%d" , &from , &to ) ;
pi = new ArcNode ;
pi->adjvex = to ;
pi->nextarc = lGraph.nodes[from].head1 ;
lGraph.nodes[from].head1 = pi ;
pi = new ArcNode ;
pi->adjvex = from ;
if ( lGraph.directed ) //this is a directed graph
{
pi->nextarc = lGraph.nodes[to].head2 ;
lGraph.nodes[to].head2 = pi ;
}
else //this is an undirected graph , only use head1 as the arcList
{
pi->nextarc = lGraph.nodes[to].head1 ;
lGraph.nodes[to].head1 = pi;
}
}
}
void BFS_list ( int start )
{
queue <int> q ;
visited[start] = 1 ;
q.push(start ) ;
printf("we begin visiting with vertex %d\n", start ) ;
while ( !q.empty() )
{
int k = q.front() ;
q.pop() ;
ArcNode *p = lGraph.nodes[k].head1 ;
while( p )
{
if ( !visited[p->adjvex])
{
printf("now we are visiting vertex %d \n", p->adjvex ) ;
visited[p->adjvex] = 1 ;
q.push( p->adjvex) ;
}
p = p->nextarc;
}
}
}
//main.cpp
#include "matrixBfs.hpp"
#include "listBfs.hpp"
using namespace std ;
int main ( void )
{
int start ;
createGraph_matrix() ;
printf("matrix table bfs start with ? [0-> %d]\n", (vexnum-1) ) ;
scanf("%d" , &start ) ;
BFS_matrix ( start ) ;
createGraph_list() ;
printf("linked list table bfs start with ? [0 -> %d ]\n", (lGraph.vexnum-1)) ;
scanf("%d", &start ) ;
BFS_list( start ) ;
system("pause") ;
return 0 ;
}
在这里需要注意一下的就是,LZ 将两个 *.hpp 文件同时的引入到 main.cpp 文件中进行运行 , 而在两个 *.hpp 文件中都有对 visited 变量进行定义,
为了避免冲突,将其中的一个 visted 变量进行注释掉了。
第四部分:
在时间复杂度分析之前,我们来做一下的约定, 假设图中有 n 个顶点, 有 m 条边。
1. DFS 算法时间复杂度分析
如果从顶点 i 进行深度优先搜索的话, 首先是需要取得该顶点i 的边链表的表头指针的, 我们假设它为 p ,
接下来我们通过表头指针 p 来访问与顶点 i 相邻接的第一个邻接顶点 x ,如果这个顶点 x 以前从没有访问过,
则从这个顶点 x 再次开始展开相应的递归搜索; 如果这个顶点 x 已经被访问过了,则指针 p 将继续向下移动至下一个链表中的顶点。
在这个过程中,每次对一个顶点都会递归的访问 1 次, 而对应的顶点数目为 n 个, 同理访问的过程中将会对(如果采用的存储方式是邻接链表)
每条边访问一次,而在邻接表中存放边的方式是(无向图) ,<顶点 x-> y , 则在顶点 x 的边表中存放一个 x->y 的边, 同时在顶点 y 对应的边表中存放
一个 y<- x 这样的信息>所以一条边将会被存放两次,所以一共需要访问 2m 次。所以对于 DFS 的时间复杂度是 O( n+2m)
2. BFS 算法时间复杂度分析
同样还是无向图中,有 n 个顶点 和 m 条边。
如果使用邻接链表来存放图结构的话, 对于从队列头部取出来的每一个顶点 k 来说, 首先要获得该顶点在邻接链表中所对应的边表的头部指针,
然后通过这个头部指针沿着该顶点的边链表中的每一个边结点,访问所有未被访问过的顶点, 并依次将这些顶点的未被访问的标识符置为已访问,
同时将这些未被访问过的结点入队列,作为下一次开始遍历的起始点。
在上述的过程中,图中的 n 个顶点每个顶点被访问一次 ,对应的时间为 O(n) , 而对应的每条边由于邻接链表中边表存放的特点(每一条边被存放两次),
所以每条边都被访问了两次,所以总共的时间复杂度为 O( n+2m).
3. DFS 与 BFS 之间的关系
深度优先搜索算法的思路是十分简单的,相比于BFS还是很好理解的,但是通过DFS在解空间中所求得的解并非是全局最优解,并且如果结点如果有无穷个
的话,会沿着图中的某一个分支无限的搜索下去。但是如果这个分支对应的并非是问题解所在的分支,则会造成十分严重的后果。
而对于广度优先搜索方法是这样的, 如果某一个问题存在解的话,则采用广度优先搜索必定能够找得到这个解,并且从全局上来看,使用
BFS 算法所找到解的过程中所经历的步骤也是最少的,同时解也是全局最优的。 但是对于BFS同样需要注意的一点就是,对于一些问题中的要求条件而言,
最优解并不是一定代表着所经历的步数是最少的,还需要考虑到一些其他的例如时间或是路径权值等等的额外限制条件。
好了,下午要看源代码,去吃饭~ ╰( ̄▽ ̄)╮