图的基本概念
从离散数学的角度来看,图由两个集合V和E组成,其中V是图中顶点的有限集合,记作V(G),E是图中两个顶点(注意这两个顶点不一定是完全不同的)连接成的边的有限集合,记作E(G)。
如果G中存在两个顶点连接成的边是笛卡尔积,即存在某一个点到另一个点的方向,那么称G为有向图,否则为无向图。在有向图中,<i,j>和<j,i>是两条不同的边。
图的基本术语
这里主要从数据结构来看,就不从离散的角度来那么详细地解释这些术语了。
上图中a为无向图,b为有向图。
端点,邻接点和环
对于无向图中的某条边<i,j>,称点i和点j为该边的的两个端点,边<i,j>和点i,j相关联,i和j互为邻接点,如果i和j相等,称该边为无向图中的环。
图a中,v1和v2为边e2的两个端点,边e4和点v2,v5相关联,v2和v3互为邻接点,e1为图a中的环。
这对有向图的边<i,j>同样成立,在有向图中,i称为起始端点,又称起点或始点,j称为终止端点,简称终点。顶点j为顶点i的出边邻接点,顶点i为顶点j的入边邻接点,,如果i和j相等,称该边为有向图中的环。
图b中,a和d为边e4的两个端点,a为起点,d为终点,顶点d为顶点a的出边邻接点,顶点a为顶点d的入边邻接点,e1为图b中的环。
如果两条边关联的两个顶点相同,则称这两条边平行,这两条边(或者更多)统称为多重边,在数据结构中讨论的图一般都是没有多重边的。
图a中的e5和e6,图b中的e2和e3即为对应图的多重边。
顶点的度,入度和出度
无向图中,和一个顶点关联的边的数量称为该顶点的度,更一般的说,即为以该顶点为端点的边的数量。同理在有向图中,以一个顶点为起点的边的数量为该点的入度,以该顶点为终点的边的数量为该点的出度,入度和出度之和称为该顶点的度。
图a中,v5的度为3,v1为4,图b中,a的的出度为4,入度为1。
有一个较为显然的结论,就是一个图中所有顶点的度等于边数的两倍,结论的证明对图中的边分别按照边和邻接的顶点计数即可。
完全图
对于无向图而言,如果每个结点与其余的n-1个结点都相邻,那么称该图为无向完全图,例如下图就是一个四阶的无向完全图。
对于有向图而言,如果每个结点p到另外n-1个结点xi都分别有<p,xi>,<xi,p>两条边,那么称该有向图为有向完全图。下图为四阶无向完全图和四阶有向完全图。
如果将一个有向图转化为无向图,有向图的每条边都转化成无向图中得到一条无向边,转化后的无向图为无向完全图,那么称原图为竞赛图。竞赛图多在图论类的数学问题中用到,这里只做提及。
稠密图和稀疏图
顶点数量相同的情况下,边相对多的称为稠密图,边相对少的称为稀疏图。界定的条件如图所示。
子图
对于图G,若V_为顶点集V的子集,以V_和G中两个端点都在V_中的边所组成的边集E_构成的图称为图G的子图。例如下图中,图M为图G的子图。
需要注意的是,子图的存在是由顶点集的子集导出的,不是V的任意子集V_和E的任意子集E_都能够构成图G的子图。
通路,回路,路径和路径长度
在图G中,从顶点i到顶点j如果存在一个以i为首,以j为尾的序列i,x1,x2······xt,j,且满足<i,x1>,<x1,x2>······<xt,j>都属于边集E,那么说明顶点i到顶点j相通,序列为i到j的通路,i记作始点,j记作终点,如果i=j,那么该路称为回路。
如果序列中的边都互异,那我们称该序列为简单通路(回路),如果顶点互异(顶点互异边也互异),那么称为初级通路(圈)。在数据结构中,需要顶点不同才称序列为简单通路(回路),这点与离散中不同,需要注意。
无论通路还是回路,我们都称它为点i到j的路径,路径的长度为经过的边的数量。离散数学中路径的写法是顶点与边的交替,数据结构中路径的写法只为顶点序列(因为数据结构中的图都为简单图,每两个点只有一条边)。
连通图和强连通图,连通分量和强连通分量
在无向图中,如果任意两个顶点都连通,那么称该图为连通图。对于有向图中,如果对于任意的两个顶点i和j,从i到j连通且从j到i也连通,那么称该图为强连通图。具体例子如下:
无向图中的极大连通子图称为无向图的连通分量。这个极大代表着当前无向图的子图为一个连通图,且无论添加进任何一个新的V中的顶点,新构成的子图都不是连通图,但不代表当前连通分量的阶数是所有连通分量的最大。连通分量在连通图中只有一个,在非连通图中有多个。
用同样的方法可以定义有向图中的强连通分量。
一个图为强连通图的充要条件是在图中存在一条经过每个顶点至少一次的回路。于是,在非强连通图中找强连通分量的方法,就是寻找有向环然后不断地扩大有向环。
同时可以知道,强连通图边最少的情况即是当它仅为一个朴素的有向环。
权和网
如果图中地每条边都附上一个数值,这个数值则称为权,边上带权的图则称为网。
图的存储结构和基本运算
有关图的存储结构非常的多也非常的复杂,书上介绍的则是较为简单的几个。
邻接矩阵存储方法
对于无权图,用A[i][j]来存储点i到点j的边数,其中无向图不计方向,对于边<i,j>,A[i][j]=A[j][i]=1;有向图计方向,对于边<i,j>,A[i][j]=1,A[j][i]=0(由于数据结构中不存在多种边,所以用01表示)。
如果是带权图,A[i][j]=点i到点j的花费,即权值。当i等于j时,A[i][j]=0,i与j不存在边时,A[i][j]=INF(INF表示∞的意思)。
struct VertexType{
int no; InfoType info;};//顶点类型,no为顶点编号,info为顶点的其他信息
//edges为邻接矩阵,n为顶点个数,e为边的数量,vexs存放顶点的信息
struct MatGraph{
int edges[MAXV][MAXV]; int n,e; VertexType vexs[MAXV];};
这种存储结构比较适合边较多的稠密图,并且在邻接矩阵中判断两个顶点之间是否有边,权值为多少都是非常快速的。
邻接矩阵是顺序结构存储的,基本操作的实现非常简单,就不多作赘述了。
邻接表存储方式
图的邻接表存储,是一种结合了顺序与链式的存储方法。邻接矩阵中每i行对应的一维数组相当于表示第i个结点到所有结点的边的情况(是否存在边)。但是前面也说了邻接矩阵适用的是稠密图,即边比较多的情况,如果顶点的数量很多,边的数量相对较少,使用邻接矩阵存储对空间的利用率是非常差的。
于是可以将表示第i个结点到所有结点的边的情况(是否存在边)的一维数组转化为单链表,如果结点i到结点j存在边,就在结点i对应的单链表中插入结点j和边的信息。这样空间的复杂度为O(V+E)(V为点的个数,E为边的个数),比起邻接矩阵O(V2)的复杂度要好很多。
存储结构中,头结点和邻接点用两种结构体,分别存储顶点和边的信息。
//adjvex为当前邻接点的编号,nextarc指向下一个邻接点,weight为头结点到邻接点构成的边的权值
struct ArcNode{
int adjvex; ArcNode *nextarc; int weight;};
//邻接表头结点类型,info为顶点的其他信息,firstarc指向单链表的第一个结点
struct VNode{
InfoType info; ArcNode *firstarc;};
//n为顶点个数,e为边的数量,adjlist为n个顶点对应的单链表的头结点
struct AdjGraph{
VNode adjlist[MAXV]; int n,e;};
如果有向图中,某个顶点的单链表中存储以该店为终点的边的信息和起点的信息,这样构成的邻接表称为逆邻接表。
邻接表基本操作的实现
主要就是创建,输出,销毁三个基本操作。
创建邻接表
一般保存图都是用邻接矩阵的二维数组A进行保存的,创建邻接表就是将邻接矩阵的存储方式转化为邻接表的存储方式。
//创建图的邻接表
void CreateAdj(AdjGraph *&G,int A[MAXV][MAXV],int n,int e){
ArcNode *p; G=(AdjGraph *)malloc(sizeof(AdjGraph));
for (int i=0;i<n;i++) G->adjlist[i].firstarc=NULL;//给邻接表中所有头结点的指针域置空
//检查邻接矩阵的每条边,对端点相异的边进行转化
for (int i=0;i<n;i++) for (int j=n-1;j>=0;j--) if (A[i][j]!=0&&A[i][j]!=INF){
p=(ArcNode *)malloc(sizeof(ArcNode));
p->adjvex=j; p->weight=A[i][j];//邻接结点赋值,权值赋值
//因为邻接表的单链表中结点的顺序没有意义,采用头插法在单链表中插入结点p
p->nextarc=G->adjlist[i].firstarc; G->adjlist[i].firstarc=p;
}G->n=n; G->e=e;
}
打印邻接表
打印就是一条一条链的打印即可。
//输出邻接表G
void DispAdj(AdjGraph *G){
ArcNode *p;
for (int i=0;i<G->n;i++){
p=G->adjlist[i].firstarc;//遍历i个单链表
printf("%3d: ",i);
while (p!=NULL){
//普通遍历单链表的方式打印邻接点编号和权值