图
Reference:《数据结构》 安徽大学出版社
可同时参照:https://blog.csdn.net/Sensente/article/details/100887588(离散数学中的图论基础定义)
作为数据结构的图,仅有两个构成要素:顶点和边。
其是一种多对多关系的抽象描述
图比之前介绍的其他结构要复杂的多:
线性结构中结点之间是线性关系,一个结点最多只有一个直接前驱和直接后继。
树形结构呈现明显的层次关系,每个结点最多只有一个双亲结点,却可以有多个孩子结点。
而图结构任意两个顶点都可能发生邻接关系。
一、图的基本概念和术语定义
1.1 基本概念
图由两部分构成:顶点集合和边的集合E,计作G=(V,E)
V(Vertex)顶点:表示数据元素。
E(Edge)边:表示顶点对的关系,分为有向边和无向边(弧,arc)
无向边,简称边,由两个顶点的无序偶构成,“(顶点1,顶点2)”表示,两顶点可以互换。即E1 = (V1,V2) = (V2,V1)
有向边,弧。E2=(V1,V2)≠(V2,V1)
弧表示一种单向关系(离散数学)
1.2 图的图形化表示(略)
1.3 定义和术语
1.3.1 无向图和有向图
无向图:每条边都是无向边
有向图:每条边都是弧、有向边
混合图:既有有向边又有无向边(一般不做讨论)
1.3.2 网络(带权图)
网(network) 指的是边或弧上带有权值的图,又称带权图。
网可以是无向的,也可以是有向的。
权值表示两个顶点间的距离。
1.3.3 子图
已知图G=(V,E),若另一个图G1=(V1,E1)是图G中选取部分顶点和部分边(弧)构成,即V1⊆V,E1⊆E,则称G1是G的子图。
1.3.4 邻接( Adjacent )
若两个顶点之间有边相连,则称这两个顶点邻接(相邻的)。
无向图 G=(V,E),若顶点 u、w 之间有一条边 (u, w)∈E,则称顶点 u、w 互为邻接点。
有向图 G=(V,E),若顶点 u、w 之间有一条弧 <u, w>∈E,则称 u 邻接 w,w邻接自(于)u。
1.3.5度(Degree)
度指的是一个顶点关联的边的数量。对于有向图还可以区分入度和出度:
入度指的是射入/指向顶点的边的数量
出度指的是该点射出/指向其他顶点的边的数量
有向图:度 = 入度+出度
其中,度和边是有相关性的:
无向图中:图的边数 = 图的顶点度数/2;
有向图中:入度之和=出度之和; 图的边数 = 入度之和/出度之和/图的顶点度数之和/2;
1.3.6 路径(Path)
通俗的说是从一个顶点途径一些顶点和边到达另一个顶点中依次经过的顶点和边的序列。
对于无向图,路径是双向可达的,对于有向图,路径往往是单向可达。
路径长度:一条路径上经过的边数(弧数)
1.3.7 回路
路径上第一个顶点和最后一个顶点相同的闭合路径叫做回路,或称为环(Loop)
1.3.8 简单路径
路径中途径的顶点不重复的叫做简单路径
1.3.9 简单回路
除了第一个顶点和最后一个顶点外,中间途径顶点不重复的闭合路径叫做简单路径(环)
1.3.10 连通图
连通(可达) :图G中,如果从顶点U到W有路径,则称U和W是联通的。
连通图:无向图G中,如果任意两个顶点之间都有路径,或是连通,则称G是连通图。
1.3.11 连通分量
无向图中分割出来的极大连通子图。非连通图可视为由若干连通分量(连通子图)构成。
1.3.12 强连通图
有向图G中,若任意两个顶点之间都有路径,或是连通的,则称G为强连通图。或有向图中任意两个顶点间可以相互到达。
1.3.13 强连通分量
有向图中分割出来的极大连通子图。非强连通有向图由若干强连通分量构成。
1.3.14 无向完全图
若无向图G中任意两个顶点间都有一条边相连,称其为无向完全图。
N个顶点的无向完全图有N*(N-1)/2条边。
1.3.15 有向完全图
若有向图G中任意两个顶点之间都有一条弧(有向边)相连,称其为有向完全图。
1.3.16 (无向)树
若无向图连通并且无回路,则称为(无向)树,除此之外,树还有以下几种描述:
·连通的无环图
·有N-1条边的连通图
·有最少边的连通图
N个点N-1条边的连通图
1.3.17 有向树
仅有一个顶点入度为0,其余顶点入度均为1的有向图。其中入度为0的顶点为其根。
1.3.18 连通图的生成树
一个N个顶点的连通图(强连通图),其生成树是它的一个极小的连通子图,它含有图中的全部顶点,但只有足以构成一棵树的N-1条边。
1.3.19 非连通图的生成森林 (spanning forest)
生成森林 (spanning forest):一个非连通图的生成森林由若干棵互不相交的树组成,含有图中的全部顶点,但是只有足以构成若干棵不相交的树的边(弧)。
请注意:此处树的定义与之前章节的树并不相同,在图论中,树只是一种特殊的图。
1.4图的顶点编号
(略)
二、图的储存结构
邻接矩阵表示法
邻接链表(邻接表)表示法
邻接多重表表示法
十字链表表示法等(Dancing Links)
以下使用第一、二种。
2.1邻接矩阵
这里的图指无向图或有向图,不含网(带权图)。矩阵A中,若顶点vi到vj之间有边或弧连接,则Aij=1;否则Aij=0。
有关邻接矩阵的一些结论:
·无向图:
邻接矩阵是对称的
第i行或j列 1 的个数就是该顶点的度
图的边数 = 矩阵中 1 的个数 / 2
·有向图:
因为边的方向性,邻接矩阵不一定对称
第i行“1”的个数是顶点vi的出度,第i列“1”的个数是顶点vi的入度
图的边数=矩阵中“1”的个数。
网的表示即用边权代替1
下列给出的定义式并不通用。
邻接矩阵存储结构描述
#define INF 65535 //定义无穷大,也可以是其它很大的数字
#define MaxVerNum 1000
//定义最大顶点个数,可根据需要定义最大顶点数
typedef char elementType;
//定义图中顶点的数据类型,这里不妨设为char类型
typedef int cellType;
//定义邻接矩阵中元素的数据类型,这里不妨设为int型
//对无权图,1-相邻(有边),0-不相邻(无边)
//对有权图,为边的权值,无边为无穷大。
typedef enum{ UDG, UDN, DG, DN } GraphKind;
//枚举图的类型--无向图,无向网,有向图,有向网
typedef struct GraphAdjMatrix
{ //顶点数组,存放顶点元素的值
elementType data[MaxVerNum];
cellType AdjMatrix[MaxVerNum][MaxVerNum];
//邻接矩阵,元素类型为cellType
int VerNum; //顶点数
int ArcNum; //弧(边)数
GraphKind gKind;
//图的类型:0-无向图;1-无向网;
// 2-有向图;3-有向网
//此项用以区分图的类型,为可选分量,可以取消。
//此项也可以直接定义为整型,而不用枚举定义。
} Graph; //图的类型名
图的邻接矩阵优缺点:
优点:直观、易于实现。
缺点:对于稀疏图毫无优势。
2.2 邻接表
1. 顶点表 顶点表存储顶点信息,可用n个结点的顺序表存储。
表中每个元素(结点)表示图中的一个顶点。
顶点表的结点结构:数据域+指针域。
数据域存放顶点数据元素或顶点相关的其它信息; 指针域指向此顶点对应的边链表,即第一个邻接顶点。
2. 边链表 边链表保存顶点表中对应顶点的边的信息,即邻接顶点信息。
边链表结点结构 可由3个部分构成:邻接点域+信息域+指针域。
邻接点域—保存邻接点信息,比如邻接点在顶点表中的编号等;
信息域—这个域可选的,保存边的相关信息,比如在网中,可用来保存边的权值。
指针域—指向下一条边(下一个邻接点)。
定义边链表的结点结构
typedef struct eNode
{ //邻接顶点信息,此处为顶点编号,从1开始
int adjVer;
//边链表中表示边的相关信息,比如表的权值
eInfoType eInfo;
//指向边链表中的下一个结点。
struct eNode* next;
}EdgeNode; //边链表结点类型
//定义顶点表的结点结构
typedef struct vNode
{
elementType data; //存放图中顶点的数据值
EdgeNode* firstEdge;
//指向此顶点关联的第一条边的指针,即边链表的头指针
//注意:fristEdge指针与边链表结点中的next指针类型相同
}VerNode; //顶点表结点类型
//定义图的整体结构
typedef struct GraphAdjLinkList
{ //顶点表,此为数组(顺序表),存放顶点信息
//数组的元素为VerNode结构类型
VerNode VerList[MaxVerNum];
int VerNum; //顶点数
int ArcNum; //弧(边)数
GraphKind gKind;
//图的类型:0-无向图;1-无向网;2-有向图;3-有向网
//此项用以区分图的类型,为可选分量,可以取消。
//此项也可以直接定义为整型,而不用枚举定义。
}Graph; //图的类型名
对无向图和网计数所有边链表的结点数,除以2即是边数
对有向图和网通过邻接表或逆邻接表计数边链表结点数即是边数。
邻接表的优点:
作为链式储存结构,可以动态申请内存,有以下几个方面优点:
便于求边。
稀疏图空间效率极高。
缺点:
判断两个顶点是否有边(弧)相对复杂。
图的遍历
·深度优先(DFS)
访问当前结点V
访问当前对所有V的邻接点执行DFS
在本例中
firstAdj( G, v ):返回图G中顶点v的第一个邻接点。若不存在邻接点(编号),则返回0;
nextAdj( G, v, w ):返回图G中顶点v的邻接点中处于w之后的那个邻接点。若不存在这样的邻接点(编号),则返回0;
通过这两个函数,可依次求出一个顶点的所有邻接点。
DFS&DFSTraverse
void DFS( Graph G, int v )
{ //从编号v的顶点开始对图G进行深度优先搜索遍历
int w;
visit(G, v); //访问顶点v
visited[v]=TRUE; //设置v已经访问标志
w=firstAdj(G,v); //求v的第一个邻接点,返回其编号给w
while(w!=0) //v的邻接点循环
{ //从没有访问过的邻接点出发递归深度遍历
if(!visited[w])
DFS(G,w); //对v的邻接点w递归DFS
w=nextAdj(G,v,w); //找v 的w后的下一个邻接点
}
}
void DFSTraverse( Graph G, int v )
{
int i; //顶点编号
for(i=1;i<=G.VerNum;i++) //初始化访问标记数组
visited[i]=false;
DFS(G,v); //遍历指定顶点v所在的连通分量
for(i=1;i<=G.VerNum;i++)
{ //循环遍历图中所有其它的连通分量
if(!visited[i])
{
DFS(G,i); //遍历i所在的连通分量
}
}
}
(2) 基于邻接表的实现
void DFS( Graph G, int v )
{
visit(G,v); //访问顶点v,如:cout<<Data[v]<<"\t";
visited[v]=TRUE; //标记编号为v的顶点已经访问
EdgeNode *p;
//p初始化为顶点v的边链表的头指针
p=G.VerList[v].firstEdge;
while(p)
{
if(!visited[ p->adjVer ]) // p->adjVer 为v的邻接点编号
DFS( G, p->adjVer ); //递归深度遍历顶点v的邻接点
p=p->next; //p指向v的下一个邻接点
}
}
DFS算法分析:
时间复杂度
基于邻接矩阵:O(N^2)
基于邻接表:O(N+E) (E stands for edge nums)
·广度优先(BFS)
访问V0
访问V0的所有邻接点
假设访问的V1,V2...
依次访问其邻接点直到为空
(层次遍历序)
需要队列辅助。
void BFS( Graph G, int v )
{
int w;
Queue Q; //初始化队列
visit(v); //访问顶点v
visited[v]=TRUE; //标记顶点v已访问
enQueue(Q,v); //v入队
while (!queueEmpty(Q))
{
getFront(Q,v) //取队头元素到v
outQueue(Q); //队头元素出队
w=firstAdj(G,v); //查找v的第一个邻接点
while (w!=0) //循环搜索v的每一个邻接点
{
if ( !visited[w] ) //邻接点w未被访问
{
visit(G,w); //访问顶点w
visited[w]=TRUE; //标记顶点w已经访问
enQueue(Q,w); //w入队
}
//查找顶点v的在w之后的下一个邻接点
w=nextAdj(G,v,w);
}}}
算法分析:
时间复杂度:
基于邻接矩阵:O(N^2)
基于邻接表:O(N+E) (E stands for edge nums)
最小生成树
https://blog.csdn.net/Sensente/article/details/102492607
二分图(KM/匈牙利)
https://blog.csdn.net/Sensente/article/details/96148276
最短路(SPFA,Dijkstra,Floyd)