408数据结构—图

一、图的基本定义

图G由顶点集V和边集E组成,记为G=(V,E)
其中,任何一条边都必须连着2个顶点
注意:线性表可以是空表,树可以是空树,但是图不可以是空图
图必须有顶点,但是可以没有边,也就是说只有顶点也可以是图

以下是关于图的基本术语,一定要记住

  • 有向图(弧),无向图(边)
    注意,有向图的弧<v,w>,v称为弧尾,w称为弧头
  • 简单图
    满足两个要求
    1.不存在重复边(两个顶点之间的边数不能大于1条)
    2.不存在顶点到自身的边
    不满足以上规则的图称为多重图
    考试讨论简单图
  • 完全图(简单完全图)
    无向图边数|E|=0到n(n-1)/2
    任意两个顶点之间都存在边的图称为完全图
    无向完全图:有n(n-1)/2条边,也就是C n2
    有向完全图:有n(n-1)条边,就是无向图边数的两倍
  • 子图
    讨论子图,必须首先是图,所以不能是图中随便取一部分顶点和边组成就叫子图了
  • 生成子图
    子图包含原图的所有顶点
  • 连通,连通图和连通分量(针对于无向图)
    在无向图中,若顶点v到顶点w有路径存在,则称v和w是连通
    若图G的任意两个顶点都是连通的,则称图G为连通图
    无向图中的极大连通子图称为连通分量
    G是连通图,最少有n-1条边(如图1),小于这个数就是非连通图
    G是非连通图,最多有C n-1 2条边(如图2)
    请添加图片描述
  • 强连通图,强连通分量(针对于有向图)
    若有一对顶点u,v,从v到w和w到v之间都有路径,则称这两个顶点是强连通
    若图中任意一对顶点都是强连通的,则称这个图是强连通图
    有向图中的极大强连通子图称为有向图的强连通分量
    判断对错:
    强连通有向图的任何顶点到其他所有顶点都有弧(×)是由路径,不是弧,弧是箭头!
    问一个图有多少个强连通子图,首先知道几个子图的顶点是肯定不能有交集的
    若一个顶点只有出边/入边,则这个点单独作为一个强连通分量,剩下就按定义找
    一个有向图是强连通图,最少需要n条边
    请添加图片描述
  • 生成树,生成森林(针对于无向图)
    连通图的生成树是,包含图中全部顶点的一个极小连通子图
    若顶点数为n,则生成树有n-1条边
    此时多一条边就会有回路,少一条边就会变成非连通图
    生成树不是唯一的
    连通分量的生成树构成生成森林

区分一下极大连通子图和极小连通子图
极大连通子图:子图必须连通,包含尽可能多的顶点和边(连通分量)
极小连通子图:子图必须连通,但是要使得边数最少(生成树)
若图本来就不是连通的,子部分包含其本身所有的顶点和边,这个子部分就是极大连通子图
极小连通子图肯定是无环的,想一想?

  • 顶点的度,入度,出度
    所有节点的度之和=图的边数×2(有向,无向)
    度=入度+出度(有向)
    所有结点的入度之和=所有节点的出度之和=边数(有向)
  • 边的权和网
    权值就是边上带有某种含义的数值
    带权图又称为网
  • 稠密图,稀疏图
    模糊的概念,边数少就稀疏,变数多就稠密
  • 路径,路径长度,回路
    一个有n个顶点的图,若边数大于n-1,则一定有回路(环)
    路径长度是路径上边的条数,和权值没关系
  • 简单路径,简单回路
    简单路径:路径上没有重复顶点
    比如说,回路就不是简单路径,因为回路是说首位两个顶点是一样的
    简单回路:除了第一个和最后一个顶点可以相同,其他顶点也不允许重复
  • 距离
    距离不是路径长度
    距离是最短路径的路径长度
    若两个顶点之间没有路径,则距离的值是∞
  • 有向树
    其实就是树的样子,是有向图
    一个顶点的入度为0(根),其余节点入度均为1的有向图
    并不是强连通图

二、图的存储

1.邻接矩阵法

定义

使用一维数组存放顶点的信息
使用二维数组存放的信息,即各个顶点之间的邻接关系
邻接矩阵:存储顶点之间的邻接关系的二维数组
A[i][j]在有边时为1,无边时为0
对于带权图而言,有边时存放权值,否则存放0或者♾
无向图的邻接矩阵是对称矩阵,规模很大的话可以压缩存储

以下是图的邻接矩阵存储结构的定义

#define MaxVertexNum 100
typedef char VertexType;
typedef int EdgeType;
typedef Struct{
    VertexType vex[MaxVertexNum];//一维数组,顶点表
    EdgeType edge[MaxVertexNum][MaxVertexNum];//二维数组,邻接矩阵,边表
    int vexnum,edgenum;//图的当前顶点数,边数
}MGraph;

当邻接矩阵里的元素只表示存在与否,我们可以使用0,1的枚举类型
邻接矩阵的时间复杂度:O(n²)=O(n)(存顶点)+ O(n²)(存矩阵)

邻接矩阵的特点

无向图顶点i的度:邻接矩阵第i行 非零元素的个数
有向图顶点i的出度:邻接矩阵第i行 非零元素的个数
有向图顶点i的入度:邻接矩阵第i列 非零元素的个数
邻接矩阵存储,很容易确定图中任意两个顶点之间是否有边相连,但是如果要统计数目,则需要将行列每个元素都检测一遍,需要的代价大

A²[i][j]:顶点i到顶点j,长度为2的路径数目

2.邻接表法

当存储稀疏图时,使用邻接矩阵会浪费掉许多的空间,引入邻接表法

定义

核心:对每个顶点vi建立一个单链表。第i个单链表中的结点表示依附于vi的边(对于有向图就是以顶点vi为尾的弧)
我们把单链表称为顶点vi的边表(对于有向图就叫出边表
边表的头指针和顶点的数据信息采用顺序存储,称为顶点表
所以在邻接表中,存在两种结点:
顶点表结点(王道上面是data,firstarc)在这里插入图片描述
边表结点(王道上面是adjvex,nextarc)在这里插入图片描述
具体应呈现这样的结构:
在这里插入图片描述
以下是邻接表存储结构的定义

#define MaxVertexNum 100
typedef struct ArcNode{   //边表结点
    int adjvex;
    struct ArcNode *nextarc;
    //Infotype info;
}ArcNode;
typedef struct VNode{
    VertexType data;
    ArcNode *firstarc;
}VNode,AdjList[MaxVertexNum];
typedef struct{
    AdjList vertices;//邻接表
    int vexnum,arcnum;//顶点数和弧数
}ALGraph;//邻接表存储的图的类型

特点

1.若G是无向图,则所需的存储空间是O(|V|+2|E|),有冗余
若G是有向图,则所需的存储空间是O(|V|+|E|)
邻接表如果有奇数个边表节点,则它一定不是无向图,因为无向图一条边用两个结点表示的
2.面对稀疏图时,采用邻接表会极大的节省存储空间
3.邻接表适合找出某一个顶点的所有邻边
邻接矩阵适合用来确定两个顶点之间是否有边
在邻接矩阵中如果想找到所有邻边,则需要遍历一整行O(n)
4.求度的问题
(无向图)遍历某个顶点的边表;
(有向图)求出度遍历边表,求入度需要遍历全部的邻接表,进行计数
5.邻接表不唯一,边表中结点的顺序可以不一样
邻接矩阵是唯一的
在这里插入图片描述

3.十字链表(适用于有向图)

也分为两种节点:顶点结点和弧结点
顶点结点,3个域: data(顶点信息,如名称) | firstin(指向以该结点为弧头的第一个弧结点) |firstout(指向以该顶点为弧尾的第一个弧结点)
顶点节点之间是顺序存储的
弧结点,4或5个域:tailvex(弧尾顶点编号)|headvex(弧头顶点编号)| hlink(指向弧头相同的下一个弧结点)| tlink(指向弧尾相同的下一个弧结点)| info(存放信息,如权值)
弧结点可省略info域
十字链表的方法方便寻找有向图的出度和入度
图的十字链表不是唯一的,但是一个十字链表唯一确定一个图
空间:O(|V|+|E|)

4.邻接多重表(适用于无向图)

解决普通邻接表实现求两个顶点之间是否存在边的问题
也分为两种节点:顶点结点和边结点
边结点,5个域:ivex(依附的顶点1编号)| jvex(依附的顶点2编号)| ilink(指向下一条依附于ivex的边)| jlink(指向下一条依附于jvex的边)| info(存储边的相关信息,如权值)
顶点结点,2个域:data(顶点相关信息)| firstedge(第一条依附于改顶点的边)

特点

1.所有依附于同一个顶点的边串联在同一链表中
2.每条边依附于两个顶点,所以每个边结点同时连在两个链表中
3.无向图的邻接多重表与邻接表的区别:一条边在邻接表中用两个节点表示,在邻接多重表中用一个节点表示

空间:O(|V|+|E|)
邻接多重表也是不唯一的

三、图的遍历

1.广度优先搜索(BFS)

定义

类似于二叉树的层序遍历算法,主要的思想是从一个顶点v开始访问,再访问v的所有邻接顶点w1,w2,…,接着从这些访问过的顶点出发,继续访问自己的所有邻接结点……直至所有顶点都被访问过为之。若此时图中尚有顶点未被访问,则选另一个没有访问的顶点出发,重复以上过程
Dijksra单源最短路径算法和Prim最小生成树算法也应用了类似的思想

特点

是一种分层的查找过程,每向前走一步可能访问一批顶点
不会回退(如DFS),所以BFS不是一个递归的算法
因为涉及分层,所以必须借助一个辅助队列

代码

实现BFS的伪代码如下

bool visited[MAX_VERTEX_NUM];
void BFSTraverse(Graph G){
     //初始化标记数组,这个数组用来判断是否访问过
     for(int i=0;i<G.vexnum;i++) visited[i]=FALSE;
     IniteQueue(Q);
     for(int i=0;i<G.vexnum;i++){
     //if的作用是,防止还有别的连通分量,一次扫不完
        if(visited[i]!=1) BFS(G,i);
     }
}

对于无向图,调用BFS的次数=连通分量数
对于有向图,调用BFS的次数需要根据具体情况分析

比如在这里插入图片描述
这是一个连通分量,但是一次BFS肯定不能遍历完全

以下是具体实现BFS的代码:
使用邻接表

void BFS(ALGraph G,int i){
     visit[i];
     visited[i]=TRUE;
     EnQueue(i);
     while(!isEmpty(Q)){
         DeQueue(Q,v);//队头结点出队
         for(p=v.vertices[v].firstarc;p;p=p->nextarc){
            int w;
            w==p->adjvex;
            if(visited[w]==FALSE){
              visit(w);
              visited(w)=TRUE;
              EnQueue(Q,w);
            }
         }
     }
}

使用邻接矩阵

void BFS(MGraph G,int i){
     visit[i];
     visited[i]=TRUE;
     EnQueue(i);
     while(!isEmpty(Q)){
         DeQueue(Q,v);//队头结点出队
         int w;
         for(w=0;w<G.vexnum;w++){
            if(visited[w]==FALSE&&G.edge[v][w]==1){
              visit(w);
              visited(w)=TRUE;
              EnQueue(Q,w);
            }
         }
     }
}

visited[]数组的作用:防止某个节点被重复访问

性能分析

空间
无论邻接表还是邻接矩阵,都需要使用队列
队列在最坏情况下装满所有顶点,所以空间复杂度为O(|V|)

时间
计算遍历时间复杂度时只用记住,遍历顶点和边各一遍就可以
邻接表:入队所有点O(|V|),遍历所有边时,我们说有向图是|E|,无向图是2|E|,事实上他们俩都是O(|E|),所以总共O(|V|+|E|)
邻接矩阵:入队所有点O(|V|),遍历所有边O(|V|²),相加总共O(|V|²)

BFS求单源最短路径问题

我们回顾一下路径是什么,是两点之间的边的个数,和权值没有关系
BFS的一个性质是由近到远来遍历的

什么是单源最短路径?
取一个顶点当作单源,该顶点到图中任何一个节点都求一个最短路径,放在一个数组中即可,在以下的代码中,u是单源,d[i]是u到i的最短路径

void BFS_MIN_Distance(Graph G,int u){
     for(int i=0;i<G.vexnum;i++) d[i]=;
     visited[u]=TRUE;
     d[u]=0;//自己到自己的最短路径是0
     EnQueue(Q);
     while(!isEmpty(Q)){
       DeQueue(Q,u);
       for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
          if(!visited[w]){
            visited[w]=TRUE;
            d[w]=d[u]+1;
            EnQueue(Q,w);
          }
     }
}

广度优先生成树

在一次BFS的遍历过程中,我们可以得到一棵遍历树,叫做广度优先生成树
通过这棵树,我们可以知道第一次访问某个点,是从哪条边过去的
一种遍历的方式对应一棵树
于是乎,由于邻接矩阵是唯一的,所以遍历时顺序是唯一的,生成树是唯一的
但是我们说邻接表是不唯一的,遍历顺序不唯一,生成树也是不唯一的
这个结论BFS,DFS都是相同的

2.深度优先搜索(DFS)

定义

深度优先搜索(DFS)相当于树的先序遍历,即尽可能“深”的访问一个图
它的基本思想是,首先访问其实顶点v,再访问v邻接的,尚未访问的第一个结点w再访问再访问w邻接的,尚未访问的第一个结点p,……当无法再向下访问时,依次退回到最近被访问的顶点,直到所有顶点都被访问一遍为止。若此时图中尚有顶点未被访问,则选另一个没有访问的顶点出发,重复以上过程。

代码

递归形式的伪代码如下

bool visited[MAX_VERTEX_NUM];
void DFSTraverse(Graph G){
    for(int i=0;i<G.vertexnum;i++) visited[i]=FALSE;
    for(int i=0;i<G.vertexnum;i++){
       if(!visited[i]) DFS(G,i);
    }
}

使用邻接表实现DFS

void DFS(ALGraph G,int i){
    visit(i);
    visited[i]=TRUE;
    for(p=G.vertices[i].firstarc;p;p=p->nextarc){
       int j=p->adjvex;
       if(!visited[j]) DFS(G,j);
    }
}

使用邻接矩阵实现DFS

void DFS(MGraph G,int i){
   visit(i);
   visited[i]=FALSE;
   for(int j=0;j<G.vertexnum;j++){
      if(!visited[j]==FALSE&&G.edge[i][j]==1) DFS(G,i);
   }
}

性能分析

空间
DFS算法是一个递归算法,需要一个递归工作栈,所以空间复杂度是O(|V|)
时间
和BFS完全一致
邻接矩阵:O(|V|²)
邻接表:O(|V|+|E|)

生成树和生成森林

与BFS类似,邻接矩阵的树唯一,邻接表的树是不唯一的

3.关于图的遍历与连通性的总结

无向图
连通:只需一次DFS/BFS
非连通:调用次数=连通分量数
有向图
起始点到各个顶点有路径:1次DFS/BFS
强连通:从任意一个顶点出发,1次DFS/BFS
非连通:视情况而定

四、 图的应用

相关算法已上传至b站
最小生成树及最短路径:【(自用)数据结构—prim,kruskal,dijkstra,floyd算法】https://www.bilibili.com/video/BV12iN3e1Egb?vd_source=e04cd6af7a236dd28995c282d6bae29e
拓扑排序和最短路径:【什么?你还不会拓扑排序和关键路径吗】https://www.bilibili.com/video/BV1gUNye2ERM?vd_source=e04cd6af7a236dd28995c282d6bae29e

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值