跳转至

图与图的遍历

部分内容和图片参考《王道数据结构2020》

一、图的基本概念

1.1 图的定义

图 G 由顶点集 Y 和边集 E 组成,记为 G=(V, E) ,其 中 V(G)表示图 G 中顶点的有限非空集;

E(G)表示图 G 中顶点之间的关系 ( 也就是边) 集合。若 V= {V1 , V2,..., Vn},则用|V|表示图 G 中顶点的个数,图的顶点个数也称图 G 的阶,E = {(u,v) | u ε V, v ε V},用 |E| 表示 图 G 中边的条数

注意: 线性表可以是空表,树可以是空树,但不存在空图

1.2 图的分类

1.2.1 有向图

若 E 是有向边(也称弧)的有限集合时,则图 G 为有向图 。 弧是顶点的有序对,记为< v, w >,其中 v,w 是顶点, v 称为弧尾,w 称为弧 头,< v, w >称为从顶点 v 到顶点 w 的弧,也称 v 邻接到 w,或 w 邻接自 ν 。

如下所示的有向图 G1 可表示为

G1 = (V1, E1) V1={1,2,3} E1 = {< 1, 2>, <2, 1 >, <2, 3>} 在这里插入图片描述

1.2.2 无向图

若 E 是无向边 (简称 边 )的有限集合时,则图 G 为无向图 。 边是顶点的无序对,记为 ( v, w)或(w, v),因为(v, w) = (w, v),其中 v, w 是顶 点。可以说顶点 w 和顶点 v 互 为邻接点。边( v, w)依附于顶点 w 和 v ,或者说边(ν, w)和顶点 v, w 相关联。

如下所示的无向图 G2 可表示为

G2 = (V2, E2) V2= {1, 2,3,4} E2 = {(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)} 在这里插入图片描述

1.3 图的性质

1.3.1 连通图

在无向图中,若从顶点 v 到顶点 w 有 路径 存在,则称 v 和 w 是连通的 。 若图 G 中任意两个顶点都是连通的,则称图 G 为连通图,否则称为非连通图。

若一个图有 n 个顶点,并且边数小于 n - 1 ,则此图必是非连通图

二、图的存储结构及基本操作

图的存储必须要完整、准确地反映顶点集和边集的信息 。 根据不同图的结构和算法 , 采用不同的存储方式将对程序的效率产生相当大的影响,因此所选的存储结构应适合于欲求解的问题 。

常见的图的存储方式有以下几种:

邻接矩阵邻接表 、邻接多重表、十字链表

2.1 邻接矩阵

所谓邻接矩阵存储,是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。

有向图、无向图对应的邻接矩阵如下图所示 在这里插入图片描述 图的邻接矩阵存储结构定义如下 :

#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType ; //顶点的数据类型
typedef int EdgeType ; //带权图中边上权值的数据类型
typedef struct{
    VertexType Vex[MaxVertexNum] ; //顶点表
    EdgeType Edge[MaxVertexNum] [MaxVertexNum] ; //邻接矩阵,边表
    int vexnum , arcnum ; //图的当前顶点数和弧数
}MGraph;

2.2 邻接表

所谓邻接表,是指对图 G 中的每个顶点 Vi 建立一个单链表,第 i 个单链表中的结点表示依附于顶点 Vi 的边 (对于有向图则是以顶点 Vi 为尾的弧),这个单链表就称为顶点 Vi 的边表 (对于有向图则称为出边表) 。 边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点: 顶点表结点和边表结点 ,如下所示

在这里插入图片描述

顶点表结点由顶点域( data ) 和指向第一条邻按边的指针 ( firstarc )构成,边表(邻接表)结点由邻接点域( adjvex )和指向下一条邻接边的指针域 (nextarc )构成 。

无向图和有向图的邻接表的实例分别如下所示 在这里插入图片描述 在这里插入图片描述

图的邻接表存储结构定义如下 :

#define MaxVertexNum 20 //图中顶点数目的最大值

typedef struct ArcNode {
    int adjvex;//顶点编号
    struct ArcNode *next;//顶点的下一条邻边
}ArcNode ;

typedef struct VNode{
    VertexType data;//顶点数据
    ArcNode *first;//顶点的第一条邻边
}VNode,AdjList[MaxVertexNum];

typedef struct{
    AdjList vertices;
    int vexnum, arcnum;//顶点总数和边总数
}ALGraph;

2.3 图的基本操作

图的基本操作是独立于图的存储结构的。而对于不同的存储方式 , 操作算法的具体实现会有着不同的性能 。在设计具体算法的实现时,应考虑采用何种存储方式的算法效率会更高 。 图的基本操作主要包括(抽象考虑,忽略各变量的类型):

  • Adjacent (G, x , y ):判断图 G 是否存在边< x, y>或(x,y)
  • Neighbors (G, x ) :列出图 G 中与结点 x 邻接的边
  • InsertVertex (G, x ) : 在图 G 中插入顶点 x
  • DeleteVertex (G, x ) : 从图 G 中删除顶点 x
  • AddEdge (G, x , y ): 若无向边 (x, y) 或有向边< x, y>不存在,则向图 G 中添加该边。
  • RemoveEdge (G, x , y ) : 若无向边(x,y)或有向边< x, y>存在,则从图 G 中删除该边。
  • FirstNeighbor (G, x ) : 求图 G 中顶点 x 的第一个邻接点,若有则返回顶点号。若 x 没有邻接点或图中不存在 x ,则返回-1
  • NextNeighbor (G, x , y ) :假设 图 G 中顶点 y 是顶点 x 的一个邻接点,返回除 y 外顶点 x 的下一个邻接点的顶点号, 若 y 是 x 的最后一个邻接点,则返回-1
  • Get_edge_value (G , x , y ): 获取图 G 中边(x,y)或< x, y>对应的权值
  • Set_edge_value(G , x , y , v ): 设置图 G 中边(x,y)或< x, y>对应的权值为 v

三、图的遍历

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

图的遍历算法主要有两种:广度优先搜索和深度优先搜索

3.1 广度优先搜索

广度优先搜索( Breadth-First-Search BFS ) 类似于二叉树的层序遍历算法。基本思想是:首先访问起始顶点 v,接着由 v 出发,依次访问 v 的各个未访问过的邻接顶点 W1,W2,...,Wi,然后依次访问 W1 , W2,..., Wi 的所有未被防问过的邻接顶点;再从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点,直至图中所有顶点都被访问过为止 。 若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为起始点,重复上述过程,直至图中所有顶点都被访问到为止 。

假设有下图所示的无向图

在这里插入图片描述

假设从 a 结点开始访问,a 先入队 。 此时队列非空,取出队头元素 a ,由于 b ,c 与 a 邻接且未被访问过,于是依次访问 b,c ,并将 b, c 依次入队 。队列非空,取出队头元素 b ,依次访问与 b 邻接且未被访问的顶点 d, e ,并将 d,e 入队(注意 : a 与 b 也邻接,但 a 己置访问标记,故不再重复访问) 。此时队列非空,取出队头元素 c,访 问与 c 邻接且未被访问的顶点 f , g, 并将 f , g 入队。此时,取出队头元素 d ,但与 d 邻接且未被访问的顶点为空,故不进行任何操作。继续取出队头元素 e ,将 h 入队列......最终取出队头元素 h 后, 队列为空,从而循环自动跳出 。遍历结果为 abcdefgh

算法实现

//@Author: mrsilvers@163.com
//@Date: 2021-01-06 18:07:43
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

/*图相关结构定义*/
#define MaxVertexNum 20

typedef int VertexType;

typedef struct ArcNode{
    int adjvex;//顶点编号
    struct ArcNode *next;//顶点的下一条邻边
}ArcNode;

typedef struct VNode{
    VertexType data;//顶点数据
    ArcNode *first;//顶点的第一条邻边
}VNode,AdjList[20];

typedef struct{
    AdjList vertices;
    int vexnum, arcnum;//顶点总数和边总数
}ALGraph;

/*队列相关结构定义*/
typedef int ElemType;
//链队列定义
typedef struct Qnode{
    ElemType data;
    struct Qnode *next;
}Qnode, *Queuep;

typedef struct{
    Queuep front;
    Queuep rear;
}linkQueue;

//顶点结果输出函数
void visit(ALGraph *g, int v){
    printf("遍历到顶点:%d \n",g->vertices[v].data);
}

bool visited[MaxVertexNum];//辅助数组
linkQueue l;//队列

/*队列操作方法实现*/
//带头节点链表方法实现
void InitQueue(linkQueue &l){
    printf("初始化队列...\n");
    l.front=l.rear=(Queuep)malloc(sizeof(Qnode));
    if(!l.front){
        perror("InitQueue:");
        exit(-1);
    }
    l.front->next=NULL;
}

void EnQueue(linkQueue &l, ElemType d){
    printf("元素 %d 入队列...\n", d);
    Queuep p;
    p=(Queuep)malloc(sizeof(Qnode));
    if(!p){
        perror("EnQueue:");
        exit(1);
    }
    p->next=NULL;
    p->data=d;
    l.rear->next=p;
    l.rear=p;
}

void DeQueue(linkQueue &l, ElemType &d){
    printf("元素 %d 出队列...\n", d);
    Queuep p;
    if(l.front==l.rear){
        perror("DeQueue:");
        exit(1);
    }
    p=l.front->next;
    d=p->data;
    l.front->next=p->next;
    if(p==l.rear)
        l.rear=l.front;
    free(p);
}

bool IsEmpty(linkQueue &l){
    if(l.front == l.rear){
        return true;
    }
    return false;
}

/*图的操作方法实现*/
//返回图G中顶点v的第一个邻接点在G的顶点数组中的位置i,若v没有邻接点或者图中不存在v,则返回-1
int FirstNeighbor(ALGraph &g, int v){
    VNode vv;
    //判断图g是否存在顶点v
    if(v<g.vexnum && v>=0){
        vv = g.vertices[v];
        if(vv.first->adjvex < g.vexnum){//判断该边连接的顶点是否在图g内
            printf("顶点 %d 的第一个邻接点是 %d\n", v, vv.first->adjvex);
            return vv.first->adjvex;
        }
    }
    return -1;
}

//i是图G中顶点v的一个邻接点(编号),该函数返回除(编号是)邻接点i外的顶点v的下一个邻接点的编号,若i是v的最后一个邻接点(编号),则返回-1
int NextNeighbor(ALGraph &g, int v, int i){
    ArcNode arc;
    VNode vv;
    //判断图g是否存在顶点v
    if(v<g.vexnum && v>=0){
        vv = g.vertices[v];
        arc = *(vv.first);//顶点v的第一条邻边
        while(arc.next!=NULL){
            if(arc.adjvex == i){
                if(arc.next != NULL){
                    printf("顶点 %d :当前邻接点 %d ,下一个邻接点是 %d\n", v, i, arc.next->adjvex);
                    sleep(1);
                    return arc.next->adjvex;
                }
                break;
            }
            arc = *(arc.next);
        }
    }
    return -1;
}

//根据顶点数据返回顶点在顶点表中的索引
int getIndex(ALGraph *g, int data){
    for(int i=0;i<g->vexnum;i++){
        if(g->vertices[i].data == data){
            printf("数据 %d 的顶点序号:%d\n", data, i);
            return i;
        }
    }
    return -1;
}

//创建图
void CreateGraph(ALGraph &g){
    int i, index;
    int temp_data;
    ArcNode *arcnode;
    printf("请输入顶点总数(20个以内):");
    scanf("%d", &(g.vexnum));
    printf("请输入总的边数(190条以内):");
    scanf("%d", &(g.arcnum));
    printf("\n\n创建顶点数据...\n\n");
    for(i=0;i<g.vexnum;i++){
        printf("请输入序号为 %d 的顶点的数据: ", i+1);
        scanf("%d", &(g.vertices[i].data));
        g.vertices[i].first = NULL;
    }
    printf("\n\n确定顶点之间的边的关系...\n\n");
    for(i=0;i<g.vexnum;i++){
        for(int j=0;j<g.vexnum-1;j++){//一个顶点最多有:(最大顶点数-1)个邻接点
            printf("(%d/%d)输入数据为 %d 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): ",i+1, g.vexnum, g.vertices[i].data);
            scanf("%d", &temp_data);
            if(temp_data==88){
                break;
            }
            index = getIndex(&g, temp_data);
            arcnode = (ArcNode *)malloc(sizeof(ArcNode));
            if(!arcnode){
                perror("CreateGraph:");
                exit(1);
            }
            arcnode->adjvex = index;
            //头插法建立链表
            arcnode->next = g.vertices[i].first;
            g.vertices[i].first = arcnode;
        }
    }
    printf("图创建完成...\n\n");
}

//广度优先算法核心
void BFS(ALGraph *g, int v){
    int w;
    visit(g,v) ;
    visited[v] = true;
    EnQueue(l, v);
    while(!IsEmpty(l)){
        DeQueue(l, v);
        for(w=FirstNeighbor(*g,v); w>=0; w=NextNeighbor(*g,v,w)){
            if(!visited[w]){
                visit(g, w);
                visited[w] = true;
                EnQueue(l, w);
            }
        }
    }
}

//广度优先算法实现
void BFSTraverse(ALGraph g){
    printf("开始遍历图(广度优先遍历)...\n\n");
    int i;
    for(i=0;i<g.vexnum;i++){
        visited[i] = false;
    }
    InitQueue(l);
    for(i=0;i<g.vexnum;i++){
        if(!visited[i])
            BFS(&g,i);
    }
}

int main(){
    ALGraph g;
    CreateGraph(g);
    BFSTraverse(g);
    return 0;
}
由于使用了引用类型,代码需保存为.cpp文件,编译运行,结果如下
silvers@HP-ZHAN:/tmp$ ./testt 
请输入顶点总数(20个以内):6
请输入总的边数(190条以内):7


创建顶点数据...

请输入序号为 1 的顶点的数据: 1
请输入序号为 2 的顶点的数据: 2
请输入序号为 3 的顶点的数据: 3
请输入序号为 4 的顶点的数据: 4
请输入序号为 5 的顶点的数据: 5
请输入序号为 6 的顶点的数据: 6


确定顶点之间的边的关系...

(1/6)输入数据为 1 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 2
数据 2 的顶点序号:1
(1/6)输入数据为 1 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 4
数据 4 的顶点序号:3
(1/6)输入数据为 1 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(2/6)输入数据为 2 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 5
数据 5 的顶点序号:4
(2/6)输入数据为 2 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(3/6)输入数据为 3 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 5
数据 5 的顶点序号:4
(3/6)输入数据为 3 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(4/6)输入数据为 4 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 2
数据 2 的顶点序号:1
(4/6)输入数据为 4 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(5/6)输入数据为 5 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 4
数据 4 的顶点序号:3
(5/6)输入数据为 5 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(6/6)输入数据为 6 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 6
数据 6 的顶点序号:5
(6/6)输入数据为 6 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
图创建完成...

开始遍历图(广度优先遍历)...

初始化队列...
遍历到顶点:1 
元素 0 入队列...
元素 0 出队列...
顶点 0 的第一个邻接点是 3
遍历到顶点:4 
元素 3 入队列...
顶点 0 :当前邻接点 3 ,下一个邻接点是 1
遍历到顶点:2 
元素 1 入队列...
元素 0 出队列...
顶点 3 的第一个邻接点是 1
元素 3 出队列...
顶点 1 的第一个邻接点是 4
遍历到顶点:5 
元素 4 入队列...
元素 1 出队列...
顶点 4 的第一个邻接点是 3
遍历到顶点:3 
元素 2 入队列...
元素 2 出队列...
顶点 2 的第一个邻接点是 4
遍历到顶点:6 
元素 5 入队列...
元素 5 出队列...
顶点 5 的第一个邻接点是 5
silvers@HP-ZHAN:/tmp$ 
这组输入创建的图正是前面2.2节的有向图G,程序里面使用88表示停止当前顶点边关系的建立

3.2 深度优先搜索

基本思想如下:首先访问图中某 一起始顶点 ν ,然后由 v 出发,访问与 v 邻接且未被访问的任一顶点 W1,再访问与 W1 邻接且未被访问的任一顶点 W2 ...... 重复上述过程 。当不能再继续向下访问时,依次退回到最近被的问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程, 直至图中所有顶点均被切问过为止。

在这里插入图片描述

深度优先搜索的遍历过程: 首先访问 a, 并置 a 访问标记;然后访问与 a 邻接且未被访问的顶点 b ,置 b 访问标记;然后访问与 b 邻接且未被访问的顶点 d,置 d 访问标记。此时 d 己没有未被访问过的邻接点,故返回上一个访问过的顶点 b ,访问与其邻接且未被访问的顶点 e,置 e 访问标记......以此类推,直至图中所有的顶点都被访问一次。遍历结果为 abdehcfg

算法实现

//深度优先核心算法
void DFS(ALGraph *g, int v){
    visit(g, v);
    visited[v] = true;
    for(int w=FirstNeighbor(*g, v);w>=0;w=NextNeighbor(*g, v, w)){
        if(!visited[w]){
            DFS(g, w);
        }
    }
}

//深度优先遍历算法实现
void DFSTraverse(ALGraph g){
    printf("\n\n开始遍历图(深度优先遍历)...\n\n");
    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);
        }
    }
}
读者可以把上述代码加入到3.1节广度优先遍历算法的代码中,在main函数中加入函数DFSTraverse的调用即可。

下面是合并后的代码编译运行的结果,数据依然使用前面2.2节的有向图G

silvers@HP-ZHAN:/tmp$ ./testt 
请输入顶点总数(20个以内):6
请输入总的边数(190条以内):7


创建顶点数据...

请输入序号为 1 的顶点的数据: 1
请输入序号为 2 的顶点的数据: 2
请输入序号为 3 的顶点的数据: 3
请输入序号为 4 的顶点的数据: 4
请输入序号为 5 的顶点的数据: 5
请输入序号为 6 的顶点的数据: 6


确定顶点之间的边的关系...

(1/6)输入数据为 1 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 2
数据 2 的顶点序号:1
(1/6)输入数据为 1 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 4
数据 4 的顶点序号:3
(1/6)输入数据为 1 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(2/6)输入数据为 2 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 5
数据 5 的顶点序号:4
(2/6)输入数据为 2 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(3/6)输入数据为 3 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 5
数据 5 的顶点序号:4
(3/6)输入数据为 3 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(4/6)输入数据为 4 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 2
数据 2 的顶点序号:1
(4/6)输入数据为 4 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(5/6)输入数据为 5 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 4
数据 4 的顶点序号:3
(5/6)输入数据为 5 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
(6/6)输入数据为 6 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 6
数据 6 的顶点序号:5
(6/6)输入数据为 6 的顶点的邻接点(输入88表示结束该顶点的邻接点关系输入): 88
图创建完成...

开始遍历图(广度优先遍历)...

初始化队列...
遍历到顶点:1 
元素 0 入队列...
元素 0 出队列...
顶点 0 的第一个邻接点是 3
遍历到顶点:4 
元素 3 入队列...
顶点 0 :当前邻接点 3 ,下一个邻接点是 1
遍历到顶点:2 
元素 1 入队列...
元素 0 出队列...
顶点 3 的第一个邻接点是 1
元素 3 出队列...
顶点 1 的第一个邻接点是 4
遍历到顶点:5 
元素 4 入队列...
元素 1 出队列...
顶点 4 的第一个邻接点是 3
遍历到顶点:3 
元素 2 入队列...
元素 2 出队列...
顶点 2 的第一个邻接点是 4
遍历到顶点:6 
元素 5 入队列...
元素 5 出队列...
顶点 5 的第一个邻接点是 5


开始遍历图(深度优先遍历)...

遍历到顶点:1 
顶点 0 的第一个邻接点是 3
遍历到顶点:4 
顶点 3 的第一个邻接点是 1
遍历到顶点:2 
顶点 1 的第一个邻接点是 4
遍历到顶点:5 
顶点 4 的第一个邻接点是 3
顶点 0 :当前邻接点 3 ,下一个邻接点是 1
遍历到顶点:3 
顶点 2 的第一个邻接点是 4
遍历到顶点:6 
顶点 5 的第一个邻接点是 5
silvers@HP-ZHAN:/tmp$