8.1 图的基本概念
1.图的定义
图(Graph)是由非空的顶点集合和一个描述顶点之间关系――边(或者弧)的集合组成,其形式化定义为:
G=(V,E)
V={vi| vi∈dataobject}
E={( vi,vj)| vi, vj ∈V ∧P(vi, vj)}
其中,G 表示一个图,V 是图G 中顶点的集合,E 是图G 中边的集合,集合E 中P(vi,vj)表示顶点vi 和顶点vj 之间有一条直接连线,即偶对(vi,vj)表示一条边。图8.1 给出了一个图的示例,在该图中:
集合V={v1,v2,v3,v4,v5};
集合E={(v1,v2),(v1,v4),(v2,v3),(v3,v4),(v3,v5),(v2,v5)}。
2.图的相关术语
(1)无向图。在一个图中,如果任意两个顶点构成的偶对(vi, vj)∈E 是无序的,即顶点之间的连线是没有方向的,则称该图为无向图。如图8.1 所示是一个无向图G1。
(2)有向图。在一个图中,如果任意两个顶点构成的偶对(vi, vj)∈E 是有序的,即顶点之间的连线是有方向的,则称该图为有向图。如图8.2 所示是一个有向图G2:
G2=(V2,E2)
V2={v1,v2,v3,v4}
E2={<v1,v2>,<v1,v3>,<v3,v4>,<v4,v1>}
(3)顶点、边、弧、弧头、弧尾。图中,数据元素vi 称为顶点(vertex );P(vi, vj)表示在顶点vi 和顶点vj 之间有一条直接连线。如果是在无向图中,则称这条连线为边;如果是在有向图中,一般称这条连线为弧。边用顶点的无序偶对(vi, vj)来表示,称顶点vi和顶点vj 互为邻接点,边(vi, vj)依附于顶点vi 与顶点vj;弧用顶点的有序偶对<vi, vj>来表示,有序偶对的第一个结点vi 被称为始点(或弧尾),在图中就是不带箭头的一端;有序偶对的第二个结点vj 被称为终点(或弧头),在图中就是带箭头的一端。
(4)无向完全图。在一个无向图中,如果任意两顶点都有一条直接边相连接,则称该图为无向完全图。可以证明,在一个含有n 个顶点的无向完全图中,有n(n-1)/2 条边。
(5)有向完全图。在一个有向图中,如果任意两顶点之间都有方向互为相反的两条弧相连接,则称该图为有向完全图。在一个含有n 个顶点的有向完全图中,有n(n-1)条边。
(6)稠密图、稀疏图。若一个图接近完全图,称为稠密图;称边数很少的图为稀疏图。
(7)顶点的度、入度、出度。顶点的度(degree)是指依附于某顶点v 的边数,通常记为TD (v)。在有向图中,要区别顶点的入度与出度的概念。顶点v 的入度是指以顶点为终点的弧的数目。记为ID (v);顶点v 出度是指以顶点v 为始点的弧的数目,记为OD (v)。
有TD (v)=ID (v)+OD (v)。
例如,在G1 中有:
TD(v1)=2 TD(v2)=3 TD(v3)=3 TD(v4)=2 TD(v5)=2
在G2 中有:
ID(v1)=1 OD(v1)=2 TD(v1)=3
ID(v2)=1 OD(v2)=0 TD(v2)=1
ID(v3)=1 OD(v3)=1 TD(v3)=2
ID(v4)=1 OD(v4)=1 TD(v4)=2
可以证明,对于具有n 个顶点、e 条边的图,顶点vi 的度TD (vi)与顶点的个数以及边的数目满足关系:
(8)边的权、网图。与边有关的数据信息称为权(weight)。在实际应用中,权值可以有某种含义。比如,在一个反映城市交通线路的图中,边上的权值可以表示该条线路的长度或者等级;对于一个电子线路图,边上的权值可以表示两个端点之间的电阻、电流或电压值;对于反映工程进度的图而言,边上的权值可以表示从前一个工程到后一个工程所需要的时间等等。边上带权的图称为网图或网络(network)。如图8.3 所示,就是一个无向网图。如果边是有方向的带权图,则就是一个有向网图。
(9)路径、路径长度。顶点vp 到顶点vq 之间的路径(path)是指顶点序列vp,vi1,vi2, …,vim,vq.。其中,(vp,vi1),(vi1,vi2),…,(vim,.vq)分别为图中的边。路径上边的数目称为路径长度。图8.1 所示的无向图G1 中,v1→v4→v3→v5 与v1→v2→v5 是从顶点v1 到顶点v5 的两条路径,路径长度分别为3 和2。
(10)回路、简单路径、简单回路。称vi 的路径为回路或者环(cycle)。序列中顶点不重复出现的路径称为简单路径。在图8.1 中,前面提到的v1 到v5 的两条路径都为简单路径。除第一个顶点与最后一个顶点之外,其他顶点不重复出现的回路称为简单回路,或者简单环。如图8.2 中的v1→v3→v4→v1。
(11)子图。对于图G=(V,E),G’=(V’,E’),若存在V’是V 的子集,E’是E的子集,则称图G’是G 的一个子图。图8.4 示出了G2 和G1 的两个子图G’和G’’。
(12)连通的、连通图、连通分量。在无向图中,如果从一个顶点vi 到另一个顶点vj(i≠j)有路径,则称顶点vi 和vj 是连通的。如果图中任意两顶点都是连通的,则称该图是连通图。无向图的极大连通子图称为连通分量。图8.5 (a)中有两个连通分量,如图8.5 (b)所示。
(13)强连通图、强连通分量。对于有向图来说,若图中任意一对顶点vi 和vj(i≠j)均有从一个顶点vi 到另一个顶点vj 有路径,也有从vj 到vi 的路径,则称该有向图是强连通图。有向图的极大强连通子图称为强连通分量。图8.2 中有两个强连通分量,分别是{v1,v2,v3}和{v4},如图8.6 所示。
(14)生成树。所谓连通图G 的生成树,是G 的包含其全部n 个顶点的一个极小连通子图。它必定包含且仅包含G 的n-1 条边。图8.4(b)G”示出了图8.1(a)中G1 的一棵生成树。在生成树中添加任意一条属于原图中的边必定会产生回路,因为新添加的边使其所依附的两个顶点之间有了第二条路径。若生成树中减少任意一条边,则必然成为非连通的。
(15)生成森林。在非连通图中,由每个连通分量都可得到一个极小连通子图,即一棵生成树。这些连通分量的生成树就组成了一个非连通图的生成森林。
(1) CreatGraph(G)输入图G 的顶点和边,建立图G 的存储。
(2)DestroyGraph(G)释放图G 占用的存储空间。
(3)GetVex(G,v)在图G 中找到顶点v,并返回顶点v 的相关信息。
(4)PutVex(G,v,value)在图G 中找到顶点v,并将value 值赋给顶点v。
(5)InsertVex(G,v)在图G 中增添新顶点v。
(6)DeleteVex(G,v)在图G 中,删除顶点v 以及所有和顶点v 相关联的边或弧。
(7)InsertArc(G,v,w)在图G 中增添一条从顶点v 到顶点w 的边或弧。
(8)DeleteArc(G,v,w)在图G 中删除一条从顶点v 到顶点w 的边或弧。
(9)DFSTraverse(G,v)在图G 中,从顶点v 出发深度优先遍历图G。
(10)BFSTtaverse(G,v)在图G 中,从顶点v 出发广度优先遍历图G。
在一个图中,顶点是没有先后次序的,但当采用某一种确定的存储方式存储后,存储结构中顶点的存储次序构成了顶点之间的相对次序,这里用顶点在图中的位置表示该顶点的存储顺序;同样的道理,对一个顶点的所有邻接点,采用该顶点的第i 个邻接点表示与该顶点相邻接的某个顶点的存储顺序,在这种意义下,图的基本操作还有:
(11)LocateVex(G,u)在图G 中找到顶点u,返回该顶点在图中位置。
(12)FirstAdjVex(G,v)在图G 中,返回v 的第一个邻接点。若顶点在G 中没有邻接顶点,则返回“空”。
(13)NextAdjVex(G,v,w)在图G 中,返回v 的(相对于w 的)下一个邻接顶点。若w 是v 的最后一个邻接点,则返回“空”。
8.2 图的存储表示—邻接矩阵
图是一种结构复杂的数据结构,表现在不仅各个顶点的度可以千差万别,而且顶点之间的逻辑关系也错综复杂。从图的定义可知,一个图的信息包括两部分,即图中顶点的信息以及描述顶点之间的关系――边或者弧的信息。因此无论采用什么方法建立图的存储结构,都要完整、准确地反映这两方面的信息。
下面介绍几种常用的图的存储结构。
所谓邻接矩阵(Adjacency Matrix)的存储结构,就是用一维数组存储图中顶点的信息,用矩阵表示图中各顶点之间的邻接关系。假设图G=(V,E)有n 个确定的顶点,即V={v0,v1,…,vn-1},则表示G 中各顶点相邻关系为一个n×n 的矩阵,矩阵的元素为:
其中,wij 表示边(vi,vj)或<vi,vj>上的权值;∞表示一个计算机允许的、大于所有边上权值的数。
用邻接矩阵表示法表示图如图8.7 所示。
用邻接矩阵表示法表示网图如图8.8 所示。
从图的邻接矩阵存储方法容易看出这种表示具有以下特点:
① 无向图的邻接矩阵一定是一个对称矩阵。因此,在具体存放邻接矩阵时只需存放上(或下)三角矩阵的元素即可。
② 对于无向图,邻接矩阵的第i 行(或第i 列)非零元素(或非∞元素)的个数正好是第i 个顶点的度TD(vi)。
③ 对于有向图,邻接矩阵的第i 行(或第i 列)非零元素(或非∞元素)的个数正好是第i 个顶点的出度OD(vi)(或入度ID(vi))。
④用邻接矩阵方法存储图,很容易确定图中任意两个顶点之间是否有边相连;但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
这是用邻接矩阵存储图的局限性。
下面介绍图的邻接矩阵存储表示。
在用邻接矩阵存储图时,除了用一个二维数组存储用于表示顶点间相邻关系的邻接矩阵外,还需用一个一维数组来存储顶点信息,另外还有图的顶点数和边数。故可将其形式描述如下:
#define MaxVertexNum 100 /*最大顶点数设为100*/
typedef char VertexType; /*顶点类型设为字符型*/
typedef int EdgeType; /*边的权值设为整型*/
typedef struct {
VertexType vexs[MaxVertexNum]; /*顶点表*/
EdeType edges[MaxVertexNum][MaxVertexNum]; /*邻接矩阵,即边表*/
int n,e; /*顶点数和边数*/
}Mgragh; /*Maragh 是以邻接矩阵存储的图类型*/
建立一个图的邻接矩阵存储的算法如下:
void CreateMGraph(MGraph *G)
{/*建立有向图G 的邻接矩阵存储*/
int i,j,k,w;
char ch;
printf("请输入顶点数和边数(输入格式为:顶点数,边数):\n");
scanf("%d,%d",&(G->n),&(G->e));/*输入顶点数和边数*/
printf("请输入顶点信息(输入格式为:顶点号<CR>):\n");
for (i=0;i<G->n;i++) scanf("\n%c",&(G->vexs[i])); /*输入顶点信息,建立顶点表*/
for (i=0;i<G->n;i++)
for (j=0;j<G->n;j++) G->edges[i][j]=0; /*初始化邻接矩阵*/
printf("请输入每条边对应的两个顶点的序号(输入格式为:i,j):\n");
for (k=0;k<G->e;k++)
{scanf("\n%d,%d",&i,&j); /*输入e 条边,建立邻接矩阵*/
G->edges[i][j]=1; /*若加入G->edges[j][i]=1;,*/
/*则为无向图的邻接矩阵存储建立*/
}
}/*CreateMGraph*/
8.2 图的存储表示—邻接表
邻接表(Adjacency List)是图的一种顺序存储与链式存储结合的存储方法。邻接表表示法类似于树的孩子链表表示法。就是对于图G 中的每个顶点vi,将所有邻接于vi 的顶点vj 链成一个单链表,这个单链表就称为顶点vi 的邻接表,再将所有点的邻接表表头放到数组中,就构成了图的邻接表。在邻接表表示中有两种结点结构,如图8.9 所示。
一种是顶点表的结点结构,它由顶点域(vertex)和指向第一条邻接边的指针域(firstedge)构成,另一种是边表(即邻接表)结点,它由邻接点域(adjvex)和指向下一条邻接边的指针域(next)构成。对于网图的边表需再增设一个存储边上信息(如权值等)的域(info),网图的边表结构如图8.10 所示。
邻接表表示的形式描述如下:
#define MaxVerNum 100 /*最大顶点数为100*/
typedef struct node{ /*边表结点*/
int adjvex; /*邻接点域*/
struct node * next; /*指向下一个邻接点的指针域*/
/*若要表示边上信息,则应增加一个数据域info*/
}EdgeNode;
typedef struct vnode{ /*顶点表结点*/
VertexType vertex; /*顶点域*/
EdgeNode * firstedge; /*边表头指针*/
}VertexNode;
typedef VertexNode AdjList[MaxVertexNum]; /*AdjList 是邻接表类型*/
typedef struct{
AdjList adjlist; /*邻接表*/
int n,e; /*顶点数和边数*/
}ALGraph; /*ALGraph 是以邻接表方式存储的图类型*/
建立一个有向图的邻接表存储的算法如下:
void CreateALGraph(ALGraph *G)
{/*建立有向图的邻接表存储*/
int i,j,k;
EdgeNode * s;
printf("请输入顶点数和边数(输入格式为:顶点数,边数):\n");
scanf("%d,%d",&(G->n),&(G->e)); /*读入顶点数和边数*/
printf("请输入顶点信息(输入格式为:顶点号<CR>):\n");
for (i=0;i<G->n;i++) /*建立有n 个顶点的顶点表*/
{scanf("\n%c",&(G->adjlist[i].vertex)); /*读入顶点信息*/
G->adjlist[i].firstedge=NULL; /*顶点的边表头指针设为空*/
}
printf("请输入边的信息(输入格式为:i,j):\n");
for (k=0;k<G->e;k++) /*建立边表*/
{scanf("\n%d,%d",&i,&j); /*读入边<Vi,Vj>的顶点对应序号*/
s=(EdgeNode*)malloc(sizeof(EdgeNode)); /*生成新边表结点s*/
s->adjvex=j; /*邻接点序号为j*/
s->next=G->adjlist[i].firstedge; /*将新边表结点s 插入到顶点Vi 的边表头部*/
G->adjlist[i].firstedge=s;
}
}/*CreateALGraph*/
算法8.2
若无向图中有n 个顶点、e 条边,则它的邻接表需n 个头结点和2e 个表结点。显然,在边稀疏(e<<n(n-1)/2)的情况下,用邻接表表示图比邻接矩阵节省存储空间,当和边相关的信息较多时更是如此。
在无向图的邻接表中,顶点vi 的度恰为第i 个链表中的结点数;而在有向图中,第i个链表中的结点个数只是顶点vi 的出度,为求入度,必须遍历整个邻接表。在所有链表中其邻接点域的值为i 的结点的个数是顶点vi 的入度。有时,为了便于确定顶点的入度或以顶点vi 为头的弧,可以建立一个有向图的逆邻接表,即对每个顶点vi 建立一个链接以vi为头的弧的链表。例如图8.12 所示为有向图G2(图8.2)的邻接表和逆邻接表。
在建立邻接表或逆邻接表时,若输入的顶点信息即为顶点的编号,则建立邻接表的复杂度为O(n+e),否则,需要通过查找才能得到顶点在图中位置,则时间复杂度为O(n·e)。
在邻接表上容易找到任一顶点的第一个邻接点和下一个邻接点,但要判定任意两个顶点(vi 和vj)之间是否有边或弧相连,则需搜索第i 个或第j 个链表,因此,不及邻接矩阵方便。
8.2 图的存储表示—十字链表
十字链表(Orthogonal List)是有向图的一种存储方法,它实际上是邻接表与逆邻接表的结合,即把每一条边的边结点分别组织到以弧尾顶点为头结点的链表和以弧头顶点为头顶点的链表中。在十字链表表示中,顶点表和边表的结点结构分别如图8.13 的(a)和(b)所示。
在弧结点中有五个域:其中尾域(tailvex)和头(headvex)分别指示弧尾和弧头这两个顶点在图中的位置,链域hlink 指向弧头相同的下一条弧,链域tlink 指向弧尾相同的下一条弧,info 域指向该弧的相关信息。弧头相同的弧在同一链表上,弧尾相同的弧也在同一链表上。它们的头结点即为顶点结点,它由三个域组成:其中vertex 域存储和顶点相关的信息,如顶点的名称等;firstin 和firstout 为两个链域,分别指向以该顶点为弧头或弧尾的第一个弧结点。例如,图8.14(a)中所示图的十字链表如图8.14(b)所示。若将有向图的邻接矩阵看成是稀疏矩阵的话,则十字链表也可以看成是邻接矩阵的链表存储结构,在图的十字链表中,弧结点所在的链表非循环链表,结点之间相对位置自然形成,不一定按顶点序号有序,表头结点即顶点结点,它们之间而是顺序存储。有向图的十字链表存储表示的形式描述如下:
#define MAX_VERTEX_NUM 20
typedef struct ArcBox {
int tailvex,headvex; /*该弧的尾和头顶点的位置*/
struct ArcBox * hlink, tlink; /分别为弧头相同和弧尾相财的弧的链域*/
InfoType info; /*该弧相关信息的指针*/
}ArcBox;
typedef struct VexNode {
VertexType vertex:
ArcBox fisrin, firstout; /*分别指向该顶点第一条入弧和出弧*/
}VexNode;
typedef struct {
VexNode xlist[MAX_VERTEX_NUM]; /*表头向量*/
int vexnum,arcnum; /*有向图的顶点数和弧数*/
}OLGraph;
下面给出建立一个有向图的十字链表存储的算法。通过该算法,只要输入n 个顶点的信息和e 条弧的信息,便可建立该有向图的十字链表,其算法内容如下。
void CreateDG(LOGraph **G)
/*采用十字链表表示,构造有向图G(G.kind=DG)*/
{ scanf (&(*G->brcnum),&(*G->arcnum),&IncInfo); /*IncInfo 为0 则各弧不含其实信息*/
for (i=0;i<*G->vexnum;++i) /*构造表头向量*/
{ scanf(&(G->xlist[i].vertex)); /*输入顶点值*/
*G->xlist[i].firstin=NulL;*G->xlist[i].firstout =NULL; /*初始化指针*/
}
for(k=0;k<G.arcnum;++k) /*输入各弧并构造十字链表*/
{ scanf(&v1,&v2); /*输入一条弧的始点和终点*/
i=LocateVex(*G,v1); j=LocateVex(*G,v2); /*确定v1 和v2 在G 中位置*/
p=(ArcBox*) malloc (sizeof(ArcBox)); /*假定有足够空间*/
*p={ i,j,*G->xlist[j].fistin,*G->xlist[i].firstout,NULL} /*对弧结点赋值*/
/*{tailvex,headvex,hlink,tlink,info}*/
*G->xlist[j].fisrtin=*G->xlist[i].firstout=p; /*完成在入弧和出弧链头的插入*/
if (IncInfo) Input( p->info); /*若弧含有相关信息,则输入*/
}
}/*CreateDG*/
算法8.3
在十字链表中既容易找到以为尾的弧,也容易找到以vi 为头的弧,因而容易求得顶点的出度和入度(或需要,可在建立十字链表的同时求出)。同时,由算法8.3 可知,建立十字链表的时间复杂度和建立邻接表是相同的。在某些有向图的应用中,十字链表是很有用的工具。
8.2 图的存储表示—邻接多重表
邻接多重表(Adjacency Multilist)主要用于存储无向图。因为,如果用邻接表存储无向图,每条边的两个边结点分别在以该边所依附的两个顶点为头结点的链表中,这给图的某些操作带来不便。例如,对已访问过的边做标记,或者要删除图中某一条边等,都需要找到表示同一条边的两个结点。因此,在进行这一类操作的无向图的问题中采用邻接多重表作存储结构更为适宜。
邻接多重表的存储结构和十字链表类似,也是由顶点表和边表组成,每一条边用一个结点表示,其顶点表结点结构和边表结点结构如图8.15 所示。
其中,顶点表由两个域组成,vertex 域存储和该顶点相关的信息firstedge 域指示第一条依附于该顶点的边;边表结点由六个域组成,mark 为标记域,可用以标记该条边是否被搜索过;ivex 和jvex 为该边依附的两个顶点在图中的位置;ilink 指向下一条依附于顶点ivex的边;jlink 指向下一条依附于顶点jvex 的边,info 为指向和边相关的各种信息的指针域。
例如,图8.16 所示为无向图8.1 的邻接多重表。在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点,则每个边结点同时链接在两个链表中。可见,对无向图而言,其邻接多重表和邻接表的差别,仅仅在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。因此,除了在边结点中增加一个标志域外,邻接多重表所需的存储量和邻接表相同。在邻接多重表上,各种基本操作的实现亦和邻接表相似。邻接多重表存储表示的形式描述如下:
#define MAX_VERTEX_NUM 20
typedef emnu{ unvisited,visited} VisitIf;
typedef struct EBox{
VisitIf mark: /*访问标记*/
int ivex,jvex; /*该边依附的两个顶点的位置*/
struct EBox ilink, jlink; /*分别指向依附这两个顶点的下一条边*/
InfoType info; /*该边信息指针*/
}EBox;
typedef struct VexBox{
VertexType data;
EBox fistedge; /*指向第一条依附该顶点的边*/
}VexBox;
typedef struct{
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum,edgenum; /*无向图的当前顶点数和边数*/
}AMLGraph;