学习数据结构--第五章:图(图的遍历操作)

学习数据结构--第五章:图(图的遍历操作)

Scroll Down

第五章:图(图的遍历操作)

1.图的遍历

图的遍历:从图中某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问依次且仅访问一次

其实树的层次遍历和图的广度优先搜索类似,可以把这个二叉树看成一个图

2.广度优先搜索(BFS)

广度优先搜索

  • 首先访问起始顶点v
  • 接着由v出发依次访问v的各个 未被访问过 的邻接顶点w1,w1....wi
  • 然后依次访问w1,w2....wi的所有 未被访问过 的邻接顶点
  • 在从这些访问过的顶点出发,访问它们所有 未被访问过 的邻接顶点
  • 以此类推

如上图,它的 广度优先搜索遍历 ,我们如果按照 树的层次遍历 这种方式进行遍历它的过程如下:

首先 1,然后访问它的所有邻接顶点即 2,3,然后分别访问 2 和 3 的邻接顶点,2 的 4 和5 ,3 的 6 ,接着我们从顶点 4 出发依旧访问它的所有邻接顶点即 7 和 5,此时我们就会发生错误,因为顶点 5 我们已经访问过了,再访问就不符合 广度优先搜索 的要求了, 所以我们遍历的时候不能完全按照树的层次遍历的方式进行遍历图 ,那么怎么实现图的广度优先搜索呢?

我们知道树的层次遍历我们借助了一种特殊的数据结构:队列

那么图我们不仅仅要依靠队列还要依靠一个辅助标记数组即: 队列+辅助标记数组

辅助标记数组

我们依旧使用上面的图,首先我们需要初始化,即将辅助标记数组中的值全部置为0,即0表示未被访问,1表示已被访问。重新进行遍历:从 1 出发,首先 1 入队,接着修改 v[0]=1 ,接着出队队首元素 1 并访问,接着我们需要将 1 的邻接顶点 2 和 3 一次入队,接着修改 v[1]=1,v[2]=1 ,然后出队队首元素 2 并进行访问,接着我们需要将顶点 2 的邻接顶点分别 1,4,5进行入队,这里我们其实只入队了 4 和 5,接着修改 v[3]=1,v[4]=1 ,这里有一个判断过程就是利用这个辅助标记数组,接着出队队首队首元素 3 并访问,然后将顶点 3 邻接的顶点并且未入队的顶点进行入队操作,即顶点 6 入队,同时修改a[5]=1,接着出队队首元素 4 并访问,然后入队 顶点 4 的邻接的顶点并且未入队的顶点 7 ,同时修改 v[6]=1,接着出队队首元素 5 并访问,接着出队队首元素 7 并访问,接着出队队首元素 7 并访问,遍历完成。此时数组中所有顶点的值都是1.

代码实现

bool visited[MAX_TREE_SIZE] 
void BFSTraverse(Graph G){  
    for(int i=0; i<G.vexnum;i++){
        visited[i]=FALSE;
    }
    InitQueue(Q);
    //for循环的作用,我们上面讲的是一个连通图,所有顶点都可以通过一个顶点依次进行访问
    //但是如果一个图不是连通的,我们需要遍历所有顶点
    for(int i=0;i<G.vexnum;i++){
        if(!visited(i)){
            BFS(G,i);
        }
    }
}
//广度优先搜索
void BFS(Graph G,int V){
    visit(v); //访问
    visited=TRUE; //TRUE 入队了,FALSE未入队
    EnQueue(Q,v); //将结点入队
    while(!isEmpty(Q)){ //判断队列是否是空
        DeQueue(Q,v); //出队队首元素,并赋值到v中
        //FirstNeighbor 求图G中顶点x的第一个邻接顶点,存在返回顶点号,不存在返-1
        //判断 w是否大于0,如果是-1则说明没有邻接顶点了
        //NextNeighbor 求图G中顶点v的的下一个邻接顶点并赋值给w
        for(int w=FirstNeighbor(G,v);w>0;w=NextNeighbor(G,v,w)){ 
            if(!visited[w]){//判断是否入队过
                visit[w];
                visited[w]=TRUE;
                EnQueue(Q,w);
            }
        }
    }
}

3.应用

3.1无权图单源最短路径问题

定义从顶点u到顶点v的最短路径d(u,v)为从u到v的任何路径中最少的边数,若从u到v没有通路,则d(u,v)=∞(表示不可达到)

//和广度优先搜索相似,增加了保存最短路径的一个数组
void BFS_MIN_Distance(Graph G,int u){//传入图 和 初始顶点
    for(int i=0;i<G.vexnum;++i){ // 
        d[i]=MAX; //保存最短路径的值,我们初始化为最大值
    }
    visited[u]=TRUE;//标识为该顶点已经入队
    d[u]=0;//初始顶点路径值改为0
    EnQueue(Q,u);//入队
    while(!isEmpty(Q)){//判断队列时候为空
    	DeQueue(Q,u);//出队队首元素
        //FirstNeighbor 求图G中顶点x的第一个邻接顶点,存在返回顶点号,不存在返-1
        //判断 w是否大于0,如果是-1则说明没有邻接顶点了
        //NextNeighbor 求图G中顶点v的的下一个邻接顶点并赋值给w
        for(int w=FirstNeighbor(G,u);w>0;w=NextNeighbor(G,u,w)){
            if(!visit[w]){//判断该顶点是否已经被访问过
                visited[w]=TRUE;//设置被访问过
                //d[u]表示到初始顶点的最短路径,w是它的邻接点,所以+1等目前w到初始顶点的最短路径
                d[w]=d[u]+1;
                EnQueue(Q,w);//入队
            }
        } 
    }
}

3.2广度优先生成树

广度优先生成树:在广度遍历过程中,我们可以得到一颗遍历树,称为广度优先生成树(生成森林)

如果是一个连通图我们会得到是一颗生成树,而如果是非连通图我们得到的是生成森林

连通图:任意两个结点都是连通的

邻接矩阵法的广度优先生成树是唯一的,邻接表法的不唯一

因为一个图的邻接矩阵表示是唯一的,所以我们在进行遍历的过程也是唯一的,但是邻接表表示法中我们输入的次序不唯一生成的边表就不唯一,对应遍历的过程(遍历边的次序)就不唯一了

4.深度优先搜索(DFS)

广度优先搜索和树层次遍历比较类似,而深度优先搜索和树先序遍历比较类似,如果把这样的一个树看成一个图,它的先序遍历顺序就是图的深度优先搜索遍历顺序

我们可以发现广度优先搜索是按照图的宽度范围进行遍历,而深度优先搜索是按照一条路径的深度的走向进行搜索遍历的

深度优先搜索(DFS)

  • 首先访问起始顶点v
  • 接着由v出发访问v的任意一个邻接且未被访问的邻接顶点Wi
  • 然后再访问与Wi邻接且未被访问的任意顶点Yi
  • 若Wi没有邻接且未被访问的顶点时,退回到它的上一层顶点v
  • 重复上述过程,直到所有顶点被访问为止

我们通过上面的算法思想遍历一遍上图:首先访问 1 ,接着可以任意访问顶点 2 或 3,我们访问 2,接着我们可以 访问任意顶点 4 或 5,我们访问 4 ,然后我们可以访问任意的顶点 7 或者 5 (7和5也是4的邻接顶点),假设我们访问5,然后 5 没有邻接且未被访问的顶点我们退回到 4 ,接着访问顶点 7,接着退回到顶点 1 ,然后访问顶点 3 ,接着访问 6,遍历完成:1 2 4 5 7 3 6

我们从上面的遍历过程可以发现整个过程可以使用递归来实现,当然递归也可以转换成来实现,同时我们也需要一个辅助标记数组。即:递归(栈)+辅助标记数组

代码实现

bool visited[MAX_TREE_SIZE]//辅助标记数组
void DFSTraverse(Graph G){
    for(int i=0;i<G.vexnum;i++){
        visited[i]=FALSE;//初始化所有结点都未必访问
    }
    for(int i=0;i<G.vexnum;i++){
        if(!visited[i]){//如果结点未被访问
            DFS(G,i);//G:图,i:起始顶点的编号
        }
    }
}
void DFS(Graph G,int v){
    visit(v); //访问
    visited[v]=TRUE;//置为访问过
    for(int w=FirstNeighbor(G,v);w>0;w=NextNeighbor(G,v,w)){
            if(!visit[w]){//判断该顶点是否已经被访问过
                DFS(G,w);//递归
            }
    }
}

如上图,我们通过上面的代码来遍历一遍:从A出发,访问A并把A对应的辅助标记数组值设置为TREUE,接着我们找A的第一个邻接顶点比如是C,然后我们判断C没有被访问过,接着继续调用DFS函数,访问C,同理继续找C的第一个邻接顶点D,判断D没有被访问过,我们访问顶点D,然后D没有邻接顶点此函数结束,我们退回到访问C的顶点的DFS的for循环中,找到C的第二个邻接点E,判断E没有被方问过,我们访问E,接着E没有邻接顶点,继续退回到C,C没有未被访问过的邻接顶点,我们退回到A的DFS的for循环中,发现A的第二个邻接顶点E被访问过了,所以A也没有未被访问过的邻接顶点了,我们退回到了DFSTraverse函数的第二个for循环中,循环判断到B发现B为被访问,然后调用DFS函数,访问D,设为TRUE,然后发现B不存在未被访问的邻接顶点,所以退回到DFSTraverse函数的第二个for循环中,发现没有未被访问过的顶点了,所以遍历结束:ACDEB

从此过程可以看出第一个函数的作用就是如果我们的初始顶点无法完成遍历图中的所有顶点我们就可以通过循环遍历每一个顶点。

邻接矩阵法的DFS(BFS)序列唯一,邻接表法的不唯一

5.深度优先生成树

深度优先生成树:在深度遍历过程中我们可以得到一颗遍历树,称为深度优先生成树(生成森林)

邻接矩阵法的深度度优先生成树是唯一的,邻接表法的不唯一

6.遍历与连通性问题

如何通过遍历来判断该图的连通性?

上面是一个无向图:无论我们使用BFS还是DFS都能通过任何一个顶点访问到其他的顶点,所以他是一个连通图

所以我们有以下结论:在无向图中,在任意结点出发进行一次遍历(调用一次BFS或者DFS),若能访问全部结点,说明该图是连通的。

上面是一个非连通的无向图,我们在进行遍历(BFS或DFS)的时候为了遍历到每一个顶点,我们需要一个for循环对每一个顶点进行调用BFS或者DFS。

我们由此可以得到如下结论:在无向图中,调用遍历函数(BFS或DFS)的次数为连通分量的个数

如上面是一个有向图:我们从顶点B出发DFS可以遍历到任何顶点,但是能访问到所有顶点代表这个图是一个强连通图吗?答案当然是不是的,能遍历到所有顶点只能说明从某个顶点到另一个顶点有一条有向突击。

如果上图我们从顶点A开始遍历,我们则需要调用两次DFS,所以在有向图中,调用遍历函数(BFS或DFS)的次数为不是强连通分量的个数

无向图叫连图,有向图叫强连通

7.理木客

数据结构相关知识,公众号理木客同步更新中,欢迎关注