10--图的广度优先遍历

386 阅读7分钟

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

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

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

一、图的广度优先遍历

image.png 如上图,将右边的树进行适当的变形,使之变为如右图那样有一定层次关系,以方便我们查看。

图的广度优先遍历需要借助队列来实现,下面分析一下实现的逻辑: image.png

  • 1.标记A已经访问过了,同时将A入队
  • 2.将A出队,并且没有标记过的顶点B和F入队标记B和F已经访问过了;
  • 3.将B出队,同时将与B有连接的顶点C、I、G入队,将顶点C、I、G入队标记已经访问过了;
  • 4.将F出队,同时将与F有连接的,并且还没有入队的顶点E入队标记E已经访问过了;A和G标记访问过了,不用重复入队;
  • 5.将C出队,此时与C连接的B和I已经标记,所以它们不用重复入队,只需要将D入队,并且标记D访问过了;
  • 6.将I出队,此时与I连接的B、C和D都已经标记过了,所以与I有连接了B、C、D都不用重复入队
  • 7.将G出队,此时与G有连接的B、F和D已经标记过了,所以只需要将H入队,标记H已经访问过了;
  • 8.将E出队,此时与E有连接的F和D已经标记过了,所以F和D都不用重复入列
  • 9.将D出队,此时与D有连接的C、I、G、E和H已经标记过了,所以C、I、G、E、H都不用重复入队;
  • 10.将H出队,此时与H连接的D、G、E已经标记过了,不用重复入队。并且此时,队列为空,遍历结束

总结起来广度优先遍历的特点如下:

  • 1.开始遍历时,将任意一个顶点入队
  • 2.每次从队头出队一个顶点,并且标记访问过了。同时将与这个顶点连接的没有标记过的顶点入队;
  • 3.当队列为空时,结束遍历

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

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];
        }
    }
}

4.构建队列

//1.循环队列的顺序存储结构
typedef struct
{
    int data[MAXSIZE];

    int front;        //头指针

    int rear;        //尾指针,若队列不空,指向队列尾元素的下一个位置
}Queue;

//2.初始化一个空队列Q 
Status InitQueue(Queue *Q)
{

    Q->front=0;

    Q->rear=0;

    return  OK;
}

//3.队列判空
Status QueueEmpty(Queue Q)
{
    // 队列空的标志
    if(Q.front==Q.rear)
    {
        return TRUE;
    }
    else{
        return FALSE;
    }
}

//4.入队
Status EnQueue(Queue *Q,**int** e)

{
    //队列满的判断
    if ((Q->rear+1)%MAXSIZE == Q->front){
        return ERROR;
    }

    //将元素e入队
    Q->data[Q->rear]=e;
    
    //移动队尾指针
    Q->rear=(Q->rear+1)%MAXSIZE;
    
    return  OK;
}

//出队
Status DeQueue(Queue *Q,int *e)
{
    //队列空的判断
    if (Q->front == Q->rear){
       return ERROR;
    } 
    
    //获取队头元素
    *e = Q->data[Q->front];
    
    //出队:移动队头指针
    Q->front = (Q->front+1)%MAXSIZE;    

    return  OK;
}

5.邻接矩阵的广度优先遍历实现

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

void BFSTraverse(MGraph G){

    int temp = 0;

    //1.创建队列
    Queue Q;
    InitQueue(&Q);

    //2.将访问标志数组全部置为"未访问状态FALSE"
    for (int i = 0 ; i < G.numVertexes; i++) {
        visited[i] = FALSE;
    }

    //3.对遍历邻接表中的每一个顶点(对于连通图只会执行1次,这个循环是针对非连通图)
    for(int i = 0 ; i < G.numVertexes; i++) {
        //判断顶点是否访问过
        if(!visited[i]){
            //访问并标记
            visited[i] = TRUE;
            printf("%c  ",G.vexs[i]);
            
            //4.将顶点入队
            EnQueue(&Q, i);
            
            while (!QueueEmpty(Q)) {
                //5.将顶点出队
                DeQueue(&Q, &i);
                
                //6.遍历与之相连的顶点
                for (int j = 0; j < G.numVertexes; j++) {
                    //7.有连接,并且没有访问标记
                    if(G.arc[i][j] == 1 && !visited[j])
                    {   
                        //8.访问并标记,同时入列
                        visited[j] = TRUE;
                        printf("%c   ",G.vexs[j]);
                        EnQueue(&Q, j);
                    }
                }
            }
        }
    }
}

6.调试代码

    printf("邻接矩阵广度优先遍历!\n");

    MGraph G;

    CreateMGraph(&G);

    BFSTraverse(G);

    printf("\n");

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

1.构建邻接矩阵顺序存储下的图的存储

二、邻接矩阵的广优先遍历已经实现了

2.结点和数据结构定义

//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;

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

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++;
            }
        }
    }
}

4.构建队列

二、邻接矩阵的广优先遍历已经实现了

5.邻接表的广度优先遍历实现

Boolean visited[MAXSIZE]; //标志数组

void BFSTraverse(GraphAdjList GL){

    //1.定义结点
    EdgeNode *p;

    //2.创建队列
    Queue Q;
    InitQueue(&Q);

    //3.将访问标志数组全部置为"未访问状态FALSE"
    for(int i = 0; i < GL->numVertexes; i++)
        visited[i] = FALSE;
        
    //4.对遍历邻接表中的每一个顶点(对于连通图只会执行1次,这个循环是针对非连通图)
    for(int i = 0 ;i < GL->numVertexes;i++){

        //4.判断当前结点是否被访问过.
        if(!visited[i]){
            //5.标记访问过了
            visited[i] = TRUE;
            //打印顶点
            printf("%c ",GL->adjList[i].data);
            
            //6.将顶点入队
            EnQueue(&Q, i);
            
            //7.判断队列是否为空
            while (!QueueEmpty(Q)) {
                //8.出队
                DeQueue(&Q, &i);
                
                //9.遍历边表链表
                p = GL->adjList[i].firstedge;
                while (p) {
                    //10.判断是否已经标记过了
                    if(!visited[p->adjvex]){
                        //11.标记访问过了
                        visited[p->adjvex] = TRUE;
                        printf("%c ",GL->adjList[p->adjvex].data);
                        //12.入队
                        EnQueue(&Q, p->adjvex);
                    }
                    //13.遍历下一结点
                    p = p->next;
                }
            }
        }
    }
}

6.调试代码

    printf("邻接表广度优先遍历\n");

    MGraph G;

    GraphAdjList GL;

    CreateMGraph(&G);

    CreateALGraph(G,&GL);

    BFSTraverse(GL);

四、总结

图的广度遍历借助了队列来实现,性能较好,但是实现复杂图的深度优先遍历那样使用了递归实现简单,但是性能较差,实现开发中可以按需求和自己的喜好来选择。

关于文章中关于队列的实现使用的是顺序存储的逻辑,关于队列的顺序存储,请看文章# 06--队列的顺序实现