10--图的深度优先遍历

693 阅读9分钟

image.png# 10--图和图的存储中已经讲解过了图的基本知识和图的存储:邻接矩阵和邻接表。总结图的特征如下:

  • 1.图是由有限顶点和顶点之间连接的组成的一种逻辑结构;
  • 2.图是无序的,即顶点和顶点之间的边不存在顺序关系;
  • 3.图即可以使用邻接矩阵实现顺序存储,也可以使用邻接表实现链式存储;

既然图的是无序的,那么应该如何遍历图,使得所有的顶点和它们的边都被访问到呢?

一、图的深度优先遍历

下面我们来讲解一下深度优先遍历的思路:

image.png

1.第一步

首先需要知悉,顺序存储的内存是连续,链式存储的链表也是有指向的,总有先后关系,但是图的特性确定图是无序的。我们从A开始遍历图,下面按步骤分析深度遍历的思路:

  • 1.当访问到A时,标记A已经被访问过了。此时与A有连接的顶点是B和C,判断B(C)是否被访问过,如果没有,则访问B(C)
  • 2.当访问到B时,标记B已经被访问过了。此时与B有连接的顶点是C、I、G。判断C(I、G)是否被访问过,如果没有,则访问C(I、G)
  • 3.当访问到C时,标记C已经被访问过了。此时与C有连接的顶点是D和I,判断D(I)是否被访问过,如果没有,则访问D(I)
  • 4.当访问到D时,标记D已经被访问过了。此时与D有连接的顶点是E、H、G、I,判断E(H、G、I)是否被访问过,如果没有,则访问E(H、G、I)
  • 5.当访问到E时,标记E已经被访问过了。此时与E有连接的顶点是F和H,判断F(H)是否被访问过,如果没有,则访问F(H)
  • 6.当访问到F时,标记F已经被访问过了。此时与F有连接的顶点是A和G,由于A已经被访问过了,所以访问G
  • 7.当访问到G时,标记G已经被访问过了。此时与G有连接的顶点是B、D、H,由于B和D已经被访问过了,所以访问H
  • 8.当访问到H时,标记H已经被访问过了。此时与H有连接的顶点是D和E,由于D和H都被访问过了,没有可以访问的顶点了,退出循环

通过上面的8个步骤,我们访问到了图中的顶点A B C D E F G H,但是还有一个顶点I没有被访问到,怎么办呢?

2.第二步

image.png 为了解决顶点I(或是图中的其它没有被访问到的顶点)没有被访问到的问题,我们可以通过上图中红色箭头的方式进行回溯,也就是从最后一个被访问过的顶点开始,依次向上回退,详细步骤如下:

  • 1.与H有连接的顶点D和E已经被访问过了,回退上一个访问过的顶点G
  • 2.遍历与G有连接的所有顶点B、D、H,发现都被访问过了,回退F
  • 3.遍历与F有连接的所有顶点F和G,发现都被访问过了,回退E
  • 4.遍历与E有连接的所有顶点F和H,发现都被访问过了,回退D
  • 5.遍历与D有连接的所有顶点E、H、G、I,发现I没有被访问过了,于是访问I,并将I标记已经被访问过了;
  • 6.遍历与I有连接的所有顶点B、C、D,发现都被访问过了,回退D
  • 7.遍历与D有连接的所有顶点E、H、G、I,发现都被访问过了,回退C
  • 8.遍历与C有连接的所有顶点D和I,发现都被访问过了,回退B
  • 9.遍历与B有连接的所有顶点C、G、I,发现都被访问过了,回退A
  • 10.遍历与A有连接的所有顶点B和F,发现都被访问过了,此时已经回退到了第一个被访问的顶点,结束回溯,遍历完成。

说明:

  • 1.在回溯过程如果遇到有多个未访问到的顶点,访问的逻辑是:依次访问并回溯,直到全部都被访问后继续向上回溯
  • 2.在回溯过程中,如果有未被访问的顶点,访问该顶点后,发现它下面还有未被访问到的顶点,访问逻辑1的逻辑一样;
  • 3.创建一个visited数组标记顶点是否被访问过
  • 4.每一次遍历到一个顶点或回溯到一个顶点的时候都需要遍历与之有连接的所有顶点,判断其中是否有未遍历过的顶点;
  • 5.回溯的逻辑我们可以使用递归的方式实现,递归调用栈的入栈出栈的逻辑刚好满足回溯的逻辑。

对于第一步第二步的逻辑,我们分别从邻接矩阵邻接表两种存储方式来进行实现

二、邻接矩阵的深度优先遍历

image.png 如上图,通过前面的分析,邻接矩阵的深度优先遍历结果是A B C D E F G H I

1.实现思路

  • 1.将图的顶点和边的信息按邻接矩阵的逻辑顺序存储到内存中;
  • 2.创建一个visited数组,用来标记顶点是否被访问过
  • 3.初始化visited数组,所有值为false;
  • 4.选择顶点开始遍历(注意非连通图的情况);
  • 5.进入递归:打印顶点信息,并标记已经被访问过了;
  • 6.循环遍历边表,判断顶点间的信息arc[i][j]是否为1(顶点之间是否有连接关系),并判断顶点是否被访问过,如果没有,则继续。

2.代码实现

(1)实现邻接矩阵的顺序存储

1.状态值定义和数据类型定义

#define OK 1

#define ERROR 0

#define TRUE 1

#define FALSE 0

typedef int Status;    //Status是函数的类型,其值是函数结果状态代码,如OK等 

typedef int Boolean; //Boolean是布尔类型,其值是TRUE或FALSE

typedef char VertexType; //顶点数据类型 

typedef int EdgeType; //边上的权值类型

#define MAXSIZE 9 //存储空间初始分配量

#define MAXEDGE 15

#define MAXVEX 9

#define INFINITYC 65535 //无穷大值,用来记录没有权重的情况

2.数据结构定义

typedef struct

{

    VertexType vexs[MAXVEX]; // 顶点表

    EdgeType arc[MAXVEX][MAXVEX];// 邻接矩阵,可看作边表

    int numVertexes, numEdges; //图中的顶点数和边数

}MGraph;

3.实现图的顺序存储

void CreateMGraph(MGraph *G)
{
    int i, j;

    //1.确定图的顶点数以及边数
    G->numVertexes=9;
    G->numEdges=15;

    //2.写入顶点信息,建立顶点表
    G->vexs[0]='A';
    G->vexs[1]='B';
    G->vexs[2]='C';
    G->vexs[3]='D';
    G->vexs[4]='E';
    G->vexs[5]='F';
    G->vexs[6]='G';
    G->vexs[7]='H';
    G->vexs[8]='I';

    //3.初始化图中的边表
    for (i = 0; i < G->numVertexes; i++)
    {
        for ( j = 0; j < G->numVertexes; j++)
        {
            G->arc[i][j]=0;
        }
    }

    //4.将图中的顶点之间的连接信息写入到边表中
    G->arc[0][1]=1;
    G->arc[0][5]=1;
    G->arc[1][2]=1;
    G->arc[1][8]=1;
    G->arc[1][6]=1;
    G->arc[2][3]=1;
    G->arc[2][8]=1;
    G->arc[3][4]=1;
    G->arc[3][7]=1;
    G->arc[3][6]=1;
    G->arc[3][8]=1;
    G->arc[4][5]=1;
    G->arc[4][7]=1;
    G->arc[5][6]=1;
    G->arc[6][7]=1;

    //5.无向图是对称矩阵,构成对称
    for(i = 0; i < G->numVertexes; i++)
    {
        for(j = i; j < G->numVertexes; j++)
        {
            G->arc[j][i] =G->arc[i][j];
        }
    }
}

(2)实现深度优先遍历

1.递归遍历顶点信息

Boolean visited[MAXVEX]; //标记数组

void DFS(MGraph G,int i){

    //1.标记顶点已经被访问过了
    visited[i] = TRUE;
    
    //2.输出顶点数据
    printf("%c",G.vexs[i]);

    //3.遍历顶点的边表信息
    for(int j = 0; j < G.numVertexes;j++){
        if(G.arc[i][j] == 1 && !visited[j]){
            //如果有与之连接的顶点没被访问过,递归(递归回调时即实现了回溯的逻辑)
            DFS(G, j);
        }  
    }
}

2.遍历图

void DFSTravese(MGraph G){
    //1.初始化标记数组
    for(int i=0;i<G.numVertexes;i++){
        visited[i] = FALSE;
    }

    //2.循环处理非连通图的情况
    for(int i = 0;i<G.numVertexes;i++){
        //从局部连通图的某一个顶点开始 
        if(!visited[i]){
            //如果顶点未被访问过,递归遍历顶点信息
            DFS(G, i);
        }
    }
}

(3)调试代码

    printf("邻接矩阵的深度优先遍历!\n");
    MGraph G;
    CreateMGraph(&G);
    DFSTravese(G);
    printf("\n");

三、邻接表的深度优先遍历

image.png

按照邻接表的链式实现的结构如上图所示,1~16步画出了整个邻接表的深度优先遍历的逻辑,最终的遍历结果是:A F G H E D I C B。可见在相同的遍历方式下,邻接矩阵和邻接表的遍历结果可能是不一样。原因很简单--图是无序的

1.实现思路

邻接矩阵的实现思路一样,不过代码上有些许差异,因为邻接表无法直接实现存储,需要借助邻接矩阵来实现,详情请看代码实现

2.代码实现

(1)实现邻接表的链式存储

借助“实现邻接矩阵的顺序存储”的实现,我们来实现邻接表的链式存储

1.结点和数据结构定义

//1.边表信息结点
typedef struct EdgeNode
{

    int adjvex;    //邻接点域,存储该顶点对应的下标
    int weight;        //用于存储权值,对于非网图可以不需要
    struct EdgeNode *next; //指针域,指向下一个与顶点有连接的顶点的边信息
    
}EdgeNode;

//2.邻接表中的元素结点的数据结构
typedef struct VertexNode
{

    int in;    //顶点入度
    char data; //顶点域,存储顶点信息
    EdgeNode *firstedge;//边表头指针

}VertexNode, AdjList[MAXVEX];

//3.邻接表的数据结构
typedef struct
{
    AdjList adjList;//邻接表
    int numVertexes,numEdges; //图的顶点数和边数
    
}graphAdjList,*GraphAdjList;

2.利用邻接矩阵构建邻接表

void CreateALGraph(MGraph G,GraphAdjList *GL){

    //1.创建邻接表,并且设计邻接表的顶点数以及弧数
    *GL = (GraphAdjList)malloc(sizeof(graphAdjList));
    (*GL)->numVertexes = G.numVertexes;
    (*GL)->numEdges = G.numEdges;

    //2.从邻接矩阵中将顶点信息输入
    for (int i = 0; i < G.numVertexes; i++) {

        //顶点入度为0
        (*GL)->adjList[i].in = 0;

        //顶点信息
        (*GL)->adjList[i].data = G.vexs[i];

        //顶点边表置空
        (*GL)->adjList[i].firstedge = NULL;
    }

    //3.建立边表
    EdgeNode *e;

    for (int i = 0; i < G.numVertexes; i++) {
        for (int j = 0; j < G.numVertexes; j++) {
            if (G.arc[i][j] == 1) {

                //1.创建顶点i与j的边表信息结点
                e = (EdgeNode *)malloc(sizeof(EdgeNode));
               
                //记录顶点i与j的边信息,j是顶点的下标值
                e->adjvex = j;

                //将顶点i与j的边信息接入邻接表中
                e->next = (*GL)->adjList[i].firstedge;
                (*GL)->adjList[i].firstedge = e;

                //顶点j 上的入度++
                (*GL)->adjList[j].in++;

                //2.如果是无向图:创建顶点j与i的边表信息结点
                e = (EdgeNode *)malloc(sizeof(EdgeNode));

                //记录边表信息对应顶点的下标j
                e->adjvex = i;

                //将当前结点的指向adjList[i]的顶点边表上
                e->next = (*GL)->adjList[j].firstedge;
                (*GL)->adjList[j].firstedge = e;

                //顶点j 上的入度++;
                (*GL)->adjList[i].in++;
            }
        }
    }
}

3.递归遍历顶点信息

Boolean visited[MAXSIZE]; //访问标志的数组

void DFS(GraphAdjList GL, int i)
{
    //1.记录已经访问过了
    visited[i] = TRUE;

    //2.打印顶点数据
    printf("%c ",GL->adjList[i].data);

    
    //获取第一个边信息
    EdgeNode *p;
    p = GL->adjList[i].firstedge;

    //3.遍历边表信息,如果有示访问过的结点,则递归访问
    while (p) {
        if(!visited[p->adjvex]){
            DFS(GL,p->adjvex);
        }
        p = p->next;
    }
}

4.遍历图

void DFSTraverse(GraphAdjList GL)
{
    //1. 将访问记录数组默认置为FALSE
    for (int i = 0; i < GL->numVertexes; i++) {
        //初始化所有顶点状态都是未访问过的状态
        visited[i] = FALSE;
    }
    
    //2. 选择一个顶点开始DFS遍历. 例如A
    for(int i = 0; i < GL->numVertexes; i++)
        //对未访问过的顶点调用DFS, 若是连通图则只会执行一次
        if(!visited[i]){
            DFS(GL, i);
        }
}

5.调试代码

    printf("邻接表的深度优先遍历!\n");
    MGraph G;
    GraphAdjList GL;
    CreateMGraph(&G);
    CreateALGraph(G,&GL);
    DFSTraverse(GL);
    printf("\n");

四、总结

  • 1.图的深度遍历主要思路是边访问边标记的逻辑;
  • 2.经过一轮边访问边标记后,可能存在未访问到的顶点,所以需要进行回溯继续查找未访问到的顶点,直到回溯到最开始访问的顶点为止;
  • 3.递归的函数调用栈入栈出栈的逻辑刚好符合回溯的逻辑,所以我们在实现图的深度优先遍历时会使用递归。