项目六图 - 数据结构

Download Report

Transcript 项目六图 - 数据结构

项目六 图
项目导读
图是比树更为复杂的非线性结构,在树中结点之间有明显
的层次关系,每一层上的数据元素可以与它上面一层中的多个
数据元素(即孩子结点)相关,但是只能和它上面一层的一个
数据元素(即双亲结点)相关。在图结构中,任意两个数据元
素之间均有可能相关。在现实生活中有许多问题可以用图表示,
所以图的应用相当广泛。在这一章里,我们主要介绍图的定义
及基本术语、图在计算机中的存储方法、图的遍历和图的应用。
学习目标
通过本章学习,要求掌握如下内容:
1.掌握图的基本概念。
2.熟练掌握图的存储结构。
3.熟练掌握图的深度优先遍历和广度优先遍历的方法和
算法。
4.掌握最小生成树的算法。
5.掌握最短路径的两个经典算法:迪杰斯特拉(Dijkstra)
和弗洛伊德(Floyed)算法。
6.掌握拓扑排序的概念,会求拓扑序列。
7.了解关键路径。
6.1 图的基本概念
6.1.1 图的定义
图(Graph),由两个集合V(G)和E(G)所组成,记
作G=(V,E),其中V(G)是图中顶点的非空有限集合,E
(G)是边的有限集合。
无向图(Undigraph),如果图中每条边都是顶点的无序
对,则称此图为无向图。无向图中的边称为无向边。无向边用
圆括号括起的两个相关顶点来表示。所以在无向图G1中,如图
6-1所示,(V1,V2)和(V2,V1)是表示同一条边。
有向图(Digraph),如果图中每条边都是顶点的有序对,则
称此图为有向图。有向图的边也称为弧,弧用尖括号括起的两
个相关顶点来表示,如<V2,V1 >即是图6-1中G2 的一条弧,
其中V2 称为弧头,V1 称为弧尾。但应注意:<V2,V1 >与
<V1,V2 >表示的是不同的弧。图6-1中的G2 图就是一个有向
图。
图6-1 有向图和无向图
图6-1所示的G1是有向图,它由V(G1)和E(G1)组成。
V(G1)={ V1,V2,V3}
E(G1)={< V1,V2 >,< V2,V1 >, <V2,V3 >, <V3,V2
>, <V1,V3 >}
其中,< V1,V2 >和< V2,V1 >是两条不同的弧。
图6-1所示的G2是无向图,它由V(G2)和E(G2)组成。
V(G2)={ V1,V2,V3,V4}
E(G2)={(V1,V2 ),(V2,V4 ), (V1,V3 ), (V3,
V4)}
其中,边(V1,V2 )和(V2,V1)代表同一条边,即
(V1,V2 )=(V2,V1)
在下面的讨论中,均假定不存在一个顶点到其自身的弧或
边。即若< Vi,Vj >∈E(G)或(Vi,Vj )∈E(G),则Vi≠Vj。
对有向图G=(V,E),如果弧< Vi,Vj >∈E(G),则称顶点
Vi邻接到顶点Vj,顶点Vj邻接自顶点Vi。弧< Vi,Vj >和顶点 Vi,
Vj 相关联。对于无向图G=(V,E),如果边(Vi,Vj )∈E
(G),则称顶点Vi和Vj 为邻接点,即 Vi和Vj 相邻接。边(Vi,
Vj )依附于顶点 Vi和Vj ,或者说边(Vi,Vj )和顶点 Vi,Vj
相关联。
6.1.2 图的基本术语
完全图(Completed Graph),由于图区分为无向图和有
向图,所以完全图也区分为无向完全图和有向完全图。
无向完全图(Completed Undigraph),若一个无向图
有n个顶点,且每一个顶点与其他n-1个顶点之间都有边,这样
的图称为无向完全图。对于一个具有n个顶点的无向完全图,
它共有n(n-1)/2条边。
有向完全图(Completed Digraph),若一个有向图有n
个顶点,且每一个顶点与其他n-1个顶点之间都有一条以该顶点
为弧尾的弧和以该顶点为弧头的弧,这样的图称为有向完全图。
因此,对于一个具有n个顶点的有向完全图,它共有n(n-1)
条弧。
很显然,在图6-1中的图都不是完全图,与
图6-1中的图具有相同顶点的完全图如图 6-2
所示。
图6-2 完全图
子图(Subgraph),设有两个图A和B且满足条件:V(B)
是V(A)的子集,E(B)是E(A)的子集,则称图B是图A的
子图。如图6-3所示。
图6-3 图与子图
路径(Path),在无向图G中,从顶点Vp 到Vq 的一条路
径是顶点序列(Vp,Vi1,Vi2,…,Vin,Vq),且(Vp,
Vi1),(Vi1,Vi2),…,(Vin,Vq )是E(G)中的边。
路径上边的数目称为路径长度。例如图6-4中的G1 图,( V1,
V2,V3)是无向图G1 的一条路径,其路径长度为2。但(V1,
V2,V3,V4)则不是图G1 的一条路径,因为(V3,V4 )不
是图G1 的一条边。
对于有向图,其路径也是有向的,路径由弧组成。例如图
6-4中的G2 图,(V1,V2,V3)是有向图G2 的一条路径,其
路径长度为2。而(V3,V2,V1)不是图G2 的一条路径,因
为<V3,V2 >不是图G2 的一条弧。
图6-4 图的路径
简单路径,如果一条路径上所有顶点(起始点和终止点除
外)彼此都是不同的,则称该路径是简单路径。对于有向图,
其简单路径也是有向的,简单路径由弧组成。
例如图6-4中的G1 图,(V1,V2,V3)和(V5,V4,V1,
V2,V3)都是简单路径,而(V1,V2,V3, V2)则不是一
条简单路径。对于图6-4中的G2 图,(V1,V2,V3)和(V1,
V2,V3,V4 ,V1)也都是简单路径。
回路(Cycle),在一条路径中,如果其起始点和终止点是同
一顶点,则称其为回路。对于图6-4中的G1 图(V1,V2,V3,
V5,V4)就是回路。对于G2 图(V1,V2,V3,V4,V1)是
回路。
连通图(Connected Graph)和强连通图,在无向图G中,
若从Vi 到Vj有路径,则称Vi 和Vj 是连通的。若G中任意两顶点
都是连通的,则称G是连通图。对于有向图而言,若有向图G
中每一对不同顶点V i 和V j 之间有从V i 到V j 和从V j 到Vi 的路
径,则称G为强连通图。
在图6-4中的G1 图是连通图,G2 图是强连通图。而图6-5
则不是连通图,图6-6也不是强连通图。
图6-5 非连通图
图6-6 非强连通图
连通分量和强连通分量,连通分量指的是无向图G中的极
大连通子图。在图6-5中有两个连通分量。
强连通分量指的是有向图G中的极大强连通子图。在图6-6
中则有三个强连通分量。
度(Degree)、入度(Indegree)和出度
(Outdegree),在无向图中,所谓顶点的度,就是指和该顶
点相关联的边数。例如图6-4中图G1顶点V1 的度为2。
在有向图中,以某顶点为弧头,即终止于该顶点的弧的数
目称为该顶点的入度;以某顶点为弧尾,即起始于该顶点的弧
的数目称为该顶点的出度;某顶点的入度和出度之和称为该顶
点的度。例如图6-4中顶点V1 的入度为2,出度为2,度为4。
权和网(Net),在一个图中,每条边都可以标上具有某
种含义的数值,该数值称为该边的权。边上带权的图称为带权
图,也称为网。如图6-7为有向带权图,图6-8为无向带权图。
图6-7 有向带权图
图6-8 无向带权图
6.2 图的存储结构
图的结构很复杂,表示图的存储结构有多种形式。常用的
图的存储结构有邻接矩阵、邻接表、多重链表等方法。本节介
绍最为常用的邻接矩阵和邻接表。
6.2.1 邻接矩阵
邻接矩阵是表示顶点之间相邻关系的矩阵,可以用一个二
维数组来表示。设G=(V,E),有n≥1个顶点,则G的邻接矩
阵A是按如下定义的一个n阶方阵。
例如,图6-9中表示的是图6-1中图G1和 图G2的邻接矩阵,
分别表示为矩阵A1 和A 2 。
图6-9 图与图的邻接矩阵
从图6-9中可以看出一个无向图的邻接矩阵是一个对角线为
零的对称矩阵,而有向图的邻接矩阵不一定对称。对于有n个
顶点的无向图的邻接矩阵,只需采用压缩存储形式存入上三角
(或下三角)矩阵。所以,无向图邻接矩阵的存储单元为
n(n+1)/2。
用邻接矩阵来表示一个具有n个顶点的有向图时,需要n2
个单元来存储邻接矩阵。
在C语言中,图的邻接矩阵存储表示如下:
#define MAX 50
int cost[MAX][MAX];
int creatcost(int cost[ ][MAX])
{ /*cost这个二维数组用于表示图的邻接矩阵*/
int vexnum,arcnum,i,j,k,v1,v2;
printf(″input vexnum,arcnum:\n″); /*输入图的顶点
数和弧数或边数*/
scanf(″%d,%d″,&vexnum,&arcnum);
for(i=1;i<=vexnum;i++)
for(j=1;j<=vexnum;j++)
cost[i][j]=0;
for(k=1;k<=arcnum;k++)
{
printf(″v1,v2=″);
scanf(″%d,%d″,&v1,&v2); /*输入所有边或所
有弧的一对顶点V1,V2*/
cost[v1][v2]=1;
cost[v2][v1]=1;
/*若为有向图则此语句删
除*/
}
return(vexnum);
}
main()
{
int i,j,vexnum;
vexnum=creatcost(cost); /*建立图的邻接矩阵*/
printf(″图的邻接矩阵的输出结果为:\n″);
for(i=1;i<=vexnum;i++)
{
for(j=1;j<=vexnum;j++)
printf(″ %d″,cost[i][j]);
printf(″\n″);
}
}
对于图6-1中的G2 图,运行时输入数据及运行结果显示如
下,其中带下划线的部分为用户输入的:
input vexnum,arcnum:
4,5
v1,v2=1,2
v1,v2=1,3
v1,v2=2,4
v1,v2=3,4
图的邻接矩阵的输出结果为:
0111
1001
1001
1110
6.2.2 邻接表
邻接表是图的一种链式存储结构。在邻接表中,为图中每
个顶点建立一个单链表,对应于邻接矩阵的一行。第i个链表中
的结点是与顶点i相关联的边(对有向图是以顶点i为始顶点的
弧)。链表中的每个结点有两个域:顶点域(adjvex)保存和i
有边相连的邻接顶点的编号,链域(next)指向含与顶点i相邻
的下一个邻接顶点的结点。链表中的结点类型可定义如下:
#define MAX 100
typedef struct node {int adjvex;/*顶点域*/
struct node *next ;/* 链域*/}ENODE;
为了能够快速访问任一顶点的链表,可以对每一个链表增
设一个表头结点,表头结点由两个域组成,其中数据域存放顶
点有关信息;链域指向链表中第一个结点,并以数组的形式存
储这些表头结点,表头结点类型可定义如下:
typedef struct { int data;/*数据域*/
ENODE *firstedge;/*链域指向链表中第一个结眯
*/}VNODE;
邻接表的类型定义如下:
typedef struct{int n,e;
VNODE adjlist[MAX];}GRAPH;
若无向图有n个项点、e条边,则它的邻接表需n个表头结
点和2e个表中结点。通常,在图的边比较稀疏的情况下,用邻
接表比用邻接矩阵节省存储空间。
在无向图的邻接表中,顶点i的度恰为第i个链表中的结点数;
而在有向图中,第i个链表中的结点个数只是顶点i的出度,为求
入度,必须遍历除第i个链表外的其他链表。在所有链表中其邻
接顶点域值为i的结点个数是顶点i的入度。有时为了确定顶点的
入度或以顶点i为终点的弧数,可以建立逆邻接表,即对每个顶
点i建立一个链接以顶点i为终点的弧表,图6-10给出加顶点信息
的图G1的邻接表、图G2的邻接表和逆邻接表。
图6-10 无向图和有向图的邻接表表示
下面讨论无向图的邻接表生成算法。该算法先将图的顶点
的数据输入到表头结点数组的数据域中。再将表头结点数组的
链域均置“空”。然后,逐个输入表示边的顶点编号对(i,j)
(i≥0,j≥0)。每输入一个顶点编号对(i,j),动态生成两个
结点,它们的邻接顶点域分别为j和i,并分别插入到顶点i和顶
点j链表之中,由于链表中结点链接次序与邻接顶点的编号无关,
故为简便起见,将新结点插入在链表的第一个结点之前。下面
给出生成邻接表算法:
#define MAX 100
#include <stdlib.h>
typedef struct node {int adjvex;/*顶点域*/
struct node *next ;/* 链域*/}ENODE;
typedef struct { int data;/*数据域*/
ENODE *firstedge;/*链域指向链表中第一个结眯*/}VNODE;
typedef struct{int n,e;
VNODE adjlist[MAX];}GRAPH;
GRAPH * creategraph(GRAPH *g)
{ENODE *p;
int i,j,k;
scanf(“%d%d”,&g->n,&g->e);/*输入顶点数和边数*/
for(k=1;k<=g->n;k++)
{scanf(“%d”,&g->adjlist[k].data);/*输入表头数组数据域的值*/
g->adjlist[k].firstedge=NULL;}/*表头数组链域的值为空*/
for(k=1;k<=g->e;k++)
{scanf(“%d%d”,&i,&j);/*输入有边连接的顶点对*/
p=(ENODE *)malloc(sizeof(ENODE));
p->adjvex=j;p->next=g->adjlist[i].firstedge;
g->adjlist[i].firstedge=p;
p=(ENODE *)malloc(sizeof(ENODE));
->adjvex=i,p->next=g->adjlist[j].firstedge;
g->adjlist[j].firstedge=p;/*结点i插入到第j个链表*/
}return(g);
}
main()
{ GRAPH *g;
g= (GRAPH *)malloc(sizeof(GRAPH ));
g=creategraph(g);
}
建立有向图的邻接表与此类似,只是在每输入一个顶点编
号对<i,j>时,仅需要动态生成一个结点j,并插入到顶点i链表中
即可。
6.3 图的遍历
和树的遍历类似,从图中的一个给定顶点出发系统地访问
图中所有顶点,并且使每个顶点仅被访问一次,这种运算被称
做图的遍历。然而,图的遍历要比树的遍历复杂得多,因为在
图中和同一个顶点有边相连的各顶点之间也可能有边,所以在
访问了某个顶点之后,可能顺着某条路径又回到已被访问过的
顶点。为了避免一个顶点被多次访问,可以设立一个标志数组
visited,初值置为0,数组元素visited[i]=1(0≤i≤n-1)表示顶点i被
访问过。通常有两种遍历图的方法,深度优先搜索遍历和广度
优先搜索遍历。它们对无向图或有向图都适用。
6.3.1 深度优先搜索
深度优先搜索是树的先根次序遍历的推广。假设从图的某
一顶点v出发进行遍历,首先访问顶点v,再访问一个与顶点v相
邻的顶点w,接着访问一个与顶点w相邻且示被访问的顶点,依
次类推,直至某个被访问的顶点的所有相邻顶点均被访问,就
从最后所访问的顶点开始,依次退回到尚有邻接顶点未曾访问
过的顶点u,并从u开始继续深度优先搜索。重复上述过程直至
图中所有顶点都被访问到为止。例如:从顶点1出发按深度优
先搜索遍历有向图6-11,顶点的访问顺序为1,2,6,5,7,3,
4,从1出发按深度优先搜索遍历无向图6-12,顶点的访问顺序
为1,2,5,3,4。
图6-11 有向图
图6-12 无向图
设无向图中有n个顶点,因此顶点编号为1到n,i为给定的
出发顶点编号。用邻接表表示图时,按深度优先搜索的递归算
法如下:
#define MAX 100
#include <stdlib.h>
typedef struct node {int adjvex;/*顶点域*/
struct node *next ;/* 链域*/}ENODE;
typedef struct { int data;/*数据域*/
ENODE *firstedge;/*链域指向链表中第一个结眯
*/}VNODE;
typedef struct{int n,e;
VNODE adjlist[MAX];}GRAPH;
int visited[MAX];
void dfs(GRAPH *g,int i)/*深度优先搜索算法
{ENODE *p;
printf(“%d”,g->adjlist[i].data);/*输出出发的i顶点*/
visited[i]=1;/第i个顶点的访问标志设为1*/
p=g->adjlist[i].firstedge;/*P指针指向与i顶点相邻接的第一个顶
点*/
while(p)/*判数P指针是否为空*/
{if(visited[p->adjvex]==0)/*如果P指针指向的结点的访问标志
为0*/
dfs(g,p->adjvex);/*调用深度搜索法的函数*/
p=p->next;/*P指针指向下一个结点*/
}
}
{GRAPH *g;
int i;
g =creategraph(g);/*见前一节,调用建立邻接表的函数*/
for(i=1;i<=g->n;i++) visited[i]=0;/*将每个顶点的访问标志设
为0*/
for(i=1;i<=g->n;i++) /*将图中的所有结点按深度优先搜索法
进行遍历*/
if(visited[i]==0) dfs(g,i);
}
程序运行结果为(其中带下划线的部分为用户输入):
4 5
1 2 3 4
1 2
1 3
1 4
2 3
3 4
1432
6.3.2 广度优先搜索
广度优先搜索的遍历过程如下:首先访问出发顶点v,然后
访问与顶点v相邻接的全部顶点w1,w2,…,wt,再依次访问
与w1,w2,…,wt邻接的没有被访问过的顶点。以此类推,
直到图中所有顶点都被访问到为止。例如,从顶点1出发按广
度优先搜索遍历有向图6-11,顶点的访问顺序为:1,2,3,4,
5,6,7。从顶点1出发按广度优先搜索遍历有向图6-12,顶点
的访问顺序为:1,2,3,4,5。由此可见,按广度优先搜索
遍历是按层次进行的,首先访问距起始点最近的相邻顶点,然
后逐层向外扩展,依次访问和起始点有路径相通且路径长度为
2,3,…的顶点。
从上述的搜索过程可见若顶点w1在顶点w2之前被访问,
则访问顶点w1的相邻顶点也应先于访问顶点w2的相邻顶点,
因此在广度优先搜索遍历时应设置队列存放已被访问的顶点。
下面以无向图的邻接表为存储结构,给出广度优先搜索的算法:
#define MAX 100
#include <stdlib.h>
typedef struct node {int adjvex;/*顶点域*/
struct node *next ;/* 链域*/}ENODE;
typedef struct { int data;/*数据域*/
ENODE *firstedge;/*链域指向链表中第一个结眯
*/}VNODE;
typedef struct{int n,e;
VNODE adjlist[MAX];}GRAPH;
int visited[MAX];
int queue[MAX];
int front=-1,rear=-1;
void in_queue(int x)/*入队操作*/
{if(rear==MAX-1) return;
queue[++rear]=x;
return;
}
int del_queue()/*出队操作*/
{int y;
if(rear==front) return(0);
y=queue[++front];
return(y);
}
void bft(GRAPH *g,int i)/*广度优先搜索算法*/
{ENODE *p;int k;
printf(“%d”,g->adjlist[i].data);/*输出访问的第一个顶点*/
visited[i]=1;/*第i个顶点访问标记设为1*/
in_queue(queue,i);/*第i个顶点入队*/
k=del_queue();
while(k)/*队列出队*/
{p=g->adjlist[k].firstedge;/*P指针指向第i个顶点的所邻接的第
一个顶点*/
while(p)/*P指针不为空时*/
{if(visited[p->adjvex])==0}/*判断访问标志是否为0*/
{printf(“%d”,g->adjlist[p->adjvex].data);/*输出被访问的顶点
*/
visited[p->adjvex]=1;/*访问标志设为1*/
}
p=p-> next;/*P指针指向下一个相邻接的顶点*/
}
k=del_queue();}
}
main()
{GRAPH *g;
int i;
g=creategraph(g);/*见前一节,调用建立邻接表的函数*/
for(i=1;i<=g->n;i++) visited[i]=0;/*将每个顶点的访问标志设
为0*/
for(i=1;i<=g->n;i++) /*将图中的所有结点按深度优先搜索法
进行遍历*/
if(visited[i]==0) bft(g,i);
}
程序运行结果(带下划线部分为用户输入):
4 5
1 2 3 4
1 2
1 3
1 4
2 3
3 4
1432
由于广度优先搜索遍历方法与深度优先搜索遍历方法的差
别仅仅是搜索顶点的顺序不同,所以两种遍历方法的时间代价
是相同的。无向图的这两种遍历方法也同样适用于有向图,请
读者自己完成。
6.4
最小生成树
设图G=(V,E)是个连通图,当从图任一顶点出发遍历
图G时,将边集E(G)分成两个集合T(G)和B(G)。其中
T(G)是遍历图时所经过的边的集合,B(G)是遍历图时未
经过的边的集合。显然,G1(V,T)是图G的子图。通常称子
图G1是连通图G的生成树。
一个连通图的生成树不一定是唯一的。如对于图6-12所示
的图G,当按深度和广度优先搜索法进行遍历就可以得到图613所示的两种不同生成树,并分别称为深度优先生成树和广度
优先生成树。
图6-13 生成树示例
对于有n个顶点的连通图,至少有n-1条边,而图的生成树
恰好有n-1条边,所以图的生成树是该图的极小连通子图。若在
图G的生成树中任意加一条属于边集B(G)中的边,则必然形
成回路。
如果连通图是一个网络,称该网络中所有生成树中权值总
和最小的生成树为最小生成树(也称最小代价生成树)。求网
络的最小生成树是一个具有重大实际意义的问题。例如,要在
n个城市之间建立一个通信网,需要建造n-1条通信线路。可以
把n个城市看作图的n个顶点,各个城市之间的通信线路看作边,
相应的建设费用作为边的权值,这样就构成了一个网络。由于
在n个城市之间,可能的线路有n(n-1)/2条,那么,如何选择其
中的n-1条线路,使总的建设费用为最小,这就是求该网络的最
小生成树的问题。
构造最小生成树的算法很多,普里姆(Prim)算法和克鲁
斯卡尔(Kruskal)算法构造最小生成树的常用方法。下面分别
介结:
6.4.1 普里姆(Prime)算法
普里姆(Prime)于1957年提出了构造最小生成树的一种
算法,该算法的要点是:按照将顶点逐个连通的步骤,把已连通
的顶点加入到集合U中,这个集合U开始时为空集。首先任选一
个顶点加入U,然后从依附于该顶点的边中选取权值最小的边
作为生成树的一条边,并将依附于该边且在集合U外的另一顶
点加入到U,表示这两个顶点已通过权值最小的边连通了。以
后,每次从一个顶点在集合U中而另一个顶点在U外的各条边中
选取权值最小的一条,作为生成树的一条边,并把依附于该边
且在集合U外的顶点并入U,依此类推,直到全部顶点都已连通
(全部顶点加入到U),即构成所要求的最小生成树。其构造
过程如图6-14所示(设首次加入到U中的顶点为1)。
图6-14 普里姆算法构造最小生成树的过程
为了便于在集合U和U外的顶点之间选择权最小的边,建
立两个数组closest和lowcost,closest[i]表示U中的一个顶
点,该顶点和不在U中的一个顶点i构成的边(i,closest[i])
具有最小的权;lowcost[i]表示边(i,closest[i])的权。
开始时,由于U的初值为{1},所以,closest[i]的值为1
(i=1,…,n),而lowcost[i]为边(1,i)的权(i=1,…,
n)。
本算法每一步扫描数组lowcost在不属于U的顶点中找出离
U最近的顶点令其为k,并打印边(k,closest[k])。然后修
改数组lowcost和closest,标记k已经加入U。这里采用图的邻
接矩阵存储结构,cost[i][j]是边(i,j)的权。如果不存
在边(i,j),则cost[i][j]的值为一个大于任何权而小于
无限大的常数(这里用32767表示)。
利用普里姆算法构造最小生成树的完整程序如下:
#define MAX 50
int creatcost(int cost[][MAX])
{
int vexnum,arcnum,i,j,v1,v2,w,k; /*输入图的顶点数和弧数或边
数*/
scanf("%d,%d",&vexnum,&arcnum);
for(i=1;i<=vexnum;i++)
for(j=1;j<=vexnum;j++)
cost[i][j]=32767;
for(k=1;k<=arcnum;k++)
{
printf("v1,v2,w=");
scanf("%d,%d,%d",&v1,&v2,&w);
/*输入所有边或所有弧的一对顶点V1 ,V2 及其权值*/
cost[v1][v2]=w;
cost[v2][v1]=w;
}
return(vexnum);
}
void prime(int cost[][MAX],int vexnum)
{/*利用普里姆算法产生从顶点1 开始的最小生成树*/
int lowcost[MAX],closest[MAX],i,j,k,min;
for(i=1;i<=vexnum;i++)
{
lowcost[i]=cost[1][i];
closest[i]=1;
}
closest[1]=-1;/*进入U集合内的顶点的标志为-1*/
for(i=2;i<=vexnum;i++) /*从U之外求离U中某一顶点最近的顶点
*/
{
min=32767;
k=0;
for(j=1;j<=vexnum;j++)
if(closest[j]!=-1&&lowcost[j]<min)
{
min=lowcost[j];
k=j;
}
if(k)
{
printf("(%d,%d) %d\n",closest[k],k,lowcost[k]);
/*输出边及其权值*/
closest[k]=-1;/*选中进入U中的顶点标志改为-1*/
for(j=2;j<=vexnum;j++)
if(closest[j]!=-1&&cost[k][j]<lowcost[j])
{
lowcost[j]=cost[k][j];
closest[j]=k;
/*k加入到U中*/
}
}
}
}
main()
{
int vexnum;
int cost[MAX][MAX];
vexnum=creatcost(cost);
/*建立图的邻接矩阵*/
prime(cost,vexnum);
}
对于图6-14中的(a)图,其邻接矩阵如图6-15所示,运
行时输入数据及运行结果显示如下,其中带下划线的部分为用
户输入的。
input vexnum,arcnum:
6,10
v1,v2,w=1,2,6
v1,v2,w=1,3,5
v1,v2,w=1,5,1
v1,v2,w=2,6,5
v1,v2,w=2,4,3
v1,v2,w=3,6,5
v1,v2,w=3,5,2
v1,v2,w=4,6,6
v1,v2,w=4,5,6
v1,v2,w=5,6,4
输出最小生成树中的边及其权值如下:
(1,6)1
(5,3)2
(5,6)4
(6,2)5
(2,4)3
图6-15 邻接矩阵
6.4.2 克鲁斯卡尔(Kruskal)算法
此算法于1956年由克鲁斯卡尔(Kruskal)提出,它从另
一途径求网络的最小生成树。假设连通网N=(V,E),则令
最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,
E1),其中E1为空集,即T中的每个顶点自成一个连通分量。
在E中选择权最小的边,若该边依附的顶点落在T中不同的分量
上,则将此边加入到T中,否则舍去此边选择下一条权最小的
边。依次类推,直到T中所有顶点都在同一连通分量上。
现以图6-16中的(a)图为例进行说明。
图6-16 克鲁斯卡尔算法构造最小生成树的过程
设此图用边集数组表示,且数组中各边的权值按由小到大
次序排列,如表6-1所示。这时可按数组下标顺序选取边,在选
择(1,6)、(3,5)、(2,4)、(5,6)时均无问题,保
留作为树T的边,当选择(1,3)边时,将与树T的已有边构成
回路,将其舍去。下一条边是(3,6)也与树T中的已有边构
成回路,也将其舍去,再下一条边是(2,6)被选入树T的边,
此时,树T中已有五条边,使N网中的所有顶点都在同一连通分
量上,即构成了N网的最小生成树。
beginvertex
endvertex
Weight
1
6
1
3
5
2
2
4
3
5
6
4
1
3
5
3
6
5
2
6
5
1
2
6
4
6
6
4
5
6
表6-1
图6-16中(a)图的边集数组
利用克鲁斯卡尔算法构造最小生成树的完整程序如下:
#define MAX 50
typedef struct edges
{ int bv,ev,w;
}EDGES;
EDGES edgeset[MAX]; /*定义边集数组,用于存储图的
各条边*/
int createdgeset()
/*建立边集数组的函数*/
{ int arcnum,i;
printf(″input arcnum:\n″);
scanf(″%d″,&arcnum);
/*输入图中的边数*/
for(i=1;i<=arcnum;i++)
{ printf(″bv,ev,w=″);
/*输入每条边的起、终点及
边上的权值*/
scanf(″%d,%d,%d″,&edgeset[i].bv,&edgeset
[i].ev,&edgeset[i].w);
}
return(arcnum);
/*返回图中的边数*/
}
sort(int n) /*对边集数组按权值升序排序的函数,其中n为数
组元素的个数,也即图的边数*/
{
int i,j;
EDGES t;
for(i=1;i<=n-1;i++)
for(j=i+1;j<=n;j++)
if(edgeset[i].w>edgeset[j].w)
{
t=edgeset[i];
edgeset[i]=edgeset[j];
edgeset[j]=t;
}
}
int seeks(int set[],int v)
/*确定顶点V所在的连通集
*/
{
int i=v;
while(set[i]>0)
i=set[i];
return(i);
}
kruskal(int e) /*利用克鲁斯卡尔算法求最小生成树,参数e
为边集数组中的边数*/
{
int set[MAX],v1,v2,i,j;
printf(″克鲁斯卡尔算法的输出为:\n″);
for(i=1;i<MAX;i++)
set[i]=0;
/*set数组的初值为0,表示每一个顶点
自成一个分量*/
i=1;
/*i表示待获取的生成树中的边在边集数组中的
下标*/
while(i<=e)
{ v1=seeks(set,edgeset[i].bv); /*确定边的起
始顶点所在的连通集*/
v2=seeks(set,edgeset[i].ev); /*确定边的终止顶点
所在的连通集*/
if(v1!=v2) /*当边所依附的两个顶点不在同一连通集时,
将该边加入
生成树*/
{
printf(″(%d,%d) %d\n″,edgeset[i].bv,
edgeset[i].ev,edgeset[i].w);
set[v1]=v2;
/*将v1 ,v2 设为在同一连通集中*/
}
i++;
}
}
main()
{
int i,arcnum;
arcnum=createdgeset(); /*建立图的边集数组,并返回其中
的边数*/
sort(arcnum);
/*对边集数组按权值升序排序*/
printf(″按权值由小到大排序的边集数组的输出结果为:″);
printf(″bv ev w \n″);
for(i=1;i<=arcnum;i++)
/*输出排序后的边集数组*/
printf(″%d %d %d\n″,edgeset[i].bv,edgeset[i].ev,
edgeset[i].w);
kruskal(arcnum);
/*利用克鲁斯卡尔算法求图的最
小生成树*/
}
对于图6-16中的(a)图,运行时输入数据及运行结果显
示如下,其中带下划线的部分为用户输入的。
input arcnum:
10
bv,ev,w=1,3,5
bv,ev,w=1,2,6
bv,ev,w=1,6,1
bv,ev,w=2,4,3
bv,ev,w=2,6,5
bv,ev,w=3,5,2
bv,ev,w=3,6,5
bv,ev,w=4,5,6
bv,ev,w=4,6,6
bv,ev,w=5,6,4
按权值由小到大排序的边集数组的输出结果为:
bv ev w
1
6 1
3
5 2
2
4 3
5
6 4
1
3 5
3
6 5
2
6 5
4
5 6
4
6 6
1
2 6
6.5 最短路径
图的最常见的应用之一是在交通运输和通讯网络中寻求两个结点之
间的最短路径。例如,我们用顶点表示城市,用边表示城市之间的公路,
则由这些顶点和边组成的图可以表示沟通各城市的公路网。把两个城市
之间的距离作为权值,赋给图中的边,就构成了带权图。
对于一个汽车司机或乘客来说,一般关心两个问题:
1.从甲地到乙地是否有公路?
2.从甲地到乙地可能有多条公路,那么,哪条公路路径最短或花费
代价最小?
这就是我们要讨论的最短路径问题。所谓的最短路径是指所经过的边
上的权值之和为最小的路径,而不是经过的边的数目为最少。根据实际
问题的需要,下面结合有向带权图来进行讨论。
首先,我们来明确两个概念:源点即路径的开始顶点;终点即路径
的最后一个顶点。之后给出两个算法:一个是求从某个源点到其他各顶
点的最短路径(即单元最短路径)的迪杰斯特拉(Dijk-stra)算法,另一
个是求每一对顶点之间的最短路径的弗洛伊德(Floyed)算法。
6.5.1 单源最短路径
对于如图6-17所示的有向带权图及其邻接矩阵,如何求从
顶点V1 出发到其他各顶点的最短路径呢?迪杰斯特拉(Dijkstra)
提出了按路径长度递增的次序产生最短路径的算法。该算法把
网(带权图)中所有顶点分成两个集合。凡以V1为源点已确定
了最短路径的终点并入S集合,S集合的初始状态只包含V1 ;
另一个集合V-S为尚未确定最短路径的顶点的集合。按各顶点
与V1 间的最短路径长度递增的次序,逐个把V-S集合中的顶点
加入到S集合中去,使得从V1 到S集合中各顶点的路径长度始
终不大于从V1 到V-S集合中各顶点的路径长度。为了方便地求
出从V1 到V-S集合中最短路径的递增次序,算法中引入一个辅
助数组dist。它的某元素dist[i],表示当前求出的从V1 到V i
的最短路径长度。这个路径长度不一定是真正的最短路径长度。
向量dist的初始值为邻接矩阵cost[][]中V1 行的值,这样,
从V1 到各顶点的最短路径中最短的一条路径长度应为
dist[w]=min{dist[i],(其中i取1,2,…,n)n为
顶点的个数}
设第一次求得的一条最短路径为< V1 ,W>,这时顶点W
应该从V-S中删除而并入S集合中。之后修改V-S集合中各顶点
的最短路径长度即数组dist的值。对于V-S集合中的某一顶点V i
来说,其当前的最短路径或者是< V1 ,V i >或者是< V1 ,W,
Vi >,不可能是其他选择。也就是说:
如果dist[w]+cost[w][V i ]<dist[i] 则dist[i]
=dist[w]+cost[w][Vi]
当V-S集合中各顶点的dist修改后,再从中挑选一个路径长
度最小的顶点,从V-S中删除,并入S中,重复上述过程,直到
求出各顶点的最短路径长度。
以图6-17为例,迪杰斯特拉算法运算过程如表6-2所示。
图6-17 有向图及其矩阵存储形式
表6-2 用迪杰斯特拉算法求从V1 到其余各顶点的最短路径
运算过程中dist数组的变化情况
终点
从V1到各终点的dist值和最短路径
V2
50
<V1, V2>
V3
10
<V1,V3>
V4
V5
50
<V1, V2>
45
<V1,V3,V4,V2
>
25
<V1,V3,V4>
45
<V1,V5>
45
<V1,V5>
45
<V1,V5>
45
<V1,V5>
W=V3
W=V4
W=V2
W=V5
V6
W
用迪杰斯特拉算法,求从某个源点到其他各顶点的最短路
径的完整程序如下:
#define MAX 50
int creatcost(int cost[ ][MAX])
{ /*cost这个二维数组用于表示图的邻接矩阵*/
int vexnum,arcnum,i,j,k,v1,v2,w; /*输入图的顶点
数和弧数或边数*/
printf(″input vexnum,arcnum:″);
scanf(″%d,%d″,&vexnum,&arcnum);
for(i=1;i<=vexnum;i++)
for(j=1;j<=vexnum;j++)
cost[i][j]=9999;
/*本例设9999代表无限大*/
for(k=1;k<=arcnum;k++)
{
printf(″v1,v2,w=″);
scanf(″%d,%d,%d″,&v1,&v2,&w);
/*输入所有边或所有弧的一对顶点V1,V2 */
cost[v1][v2]=w;
}
return(vexnum);
}
void dijkstra(cost,vexnum) /*用迪杰斯特拉算法求从源点
出发的最短路径*/
int cost[][MAX],vexnum;
{
int path[MAX],s[MAX],dist[MAX],i,j,n,w,v,
sum,min,v1;
/*S数组用于记录顶点V是否已经确定了最短路径,S[V]=1,
顶点V已经确定了最短路径,*/
/*S[V]=0,顶点V尚未确定最短路径。dist数组表示当前求
出的从V1 到Vi 的最短路径。path是路径数组,其中path[i]
表示从源点到顶点Vi 之间的最短路径上Vi 的前驱顶点*/
printf(″input v1:″);
scanf(″%d″,&v1);
/*输入源点V1 */
for(i=1;i<=vexnum;i++)
{dist[i]=cost[v1][i];
/*初始时,从源点V1 到各
顶点的最短路径为相应弧上的权*/
s[i]=0;
if(cost[v1][i]<9999)path[i]=v1; /*path记录当
前最短路径*/
}
s[v1]=1;
/*将源点加入S集合中*/
for(i=1;i<=vexnum;i++)
{
min=9999;
/*本例设各边上的权值均小于9999*/
for(j=1;j<=vexnum;j++) /*从S集合外找出距离源点最
近的顶点w*/
if((s[j]==0)&&(dist[j]<min))
{ min=dist[j];
w=j;
}
s[w]=1;
/ *将w加入S集合,即w已是求出最短路径
的顶点*/
for(v=1;v<=vexnum;v++)
if(s[v]==0)
if(dist[w]+cost[w][v]<dist[v])
{
dist[v]=dist[w]+cost[w][v]; /*修改V-S集合中各顶
点的最短路径长度*/
path[v]=w; /*修改V-S集合中各顶点的最短路径*/
}
}
printf(″the shortest path from%d to each vertex:\n″,v1);
for(i=1;i<=vexnum;i++) /*输出从某源点到其他各顶点的最短
路径*/
if(s[i]==1)
{
w=i;
while(w!=v1)
{
printf(″%d<--″,w);
w=path[w];
/*通过找到前驱顶点,反向输出最短路
径*/
}
printf(″%d ″,w);
printf(″ %d\n″,dist[i]);
}
else
{
printf(″%d<--%d″,i,v1);
printf(″9999\n″); /*不存在路径时,路径长度设为9999*/
}
}
main()
{ int vexnum;
int cost[MAX][MAX];
vexnum=creatcost(cost); /*建立图的邻接矩阵*/
dijkstra(cost,vexnum);
}
对于图6-17中的图,运行时输入数据及运行结果显示如下,其
中带下划线的部分为用户输入的。
input Vexnum,arcnum:
6,10
v1,v2,w=1,2,50
v1,v2,w=1,3,10
v1,v2,w=1,5,45
v1,v2,w=2,3,15
v1,v2,w=2,5,10
v1,v2,w=3,1,20
v1,v2,w=3,4,15
v1,v2,w=4,2,20
v1,v2,w=4,5,35
v1,v2,w=6,4,3
input v1:1
the shortest path from 1 to each Vertex:
2<--4<--3<--1 45
3<--1
10
4<--3<--1
25
5<--1
45
6<--1
9999
对于上述程序,读者可在输入V1 值时给出任意一个顶点,
从而求出从任一源点出发到其他各顶点的最短路径。
以图6-17中的图为例,表6-3所示为按迪杰斯特拉算法求
从顶点V1 出发到其他各顶点的最短路径的过程中,各辅助数组
中值的变化过程。
表6-3 迪杰斯特拉算法执行过程中,各辅助数组中值的变化
S
Dist
Path
{1}
1. 2 3 4 5 6
∞ 50 10 ∞ 45 ∞
1 2 3 4 5 6
1 1 3 1
{1,3}
∞ 50 10 25 45 ∞
1 1 3 1
{1,3,4}
∞ 45 10 25 45 ∞
4 1 3 1
{1,3,4,2}
∞ 45 10 25 45 ∞
4 1 3 1
{1,3,4,2,5}
∞ 45 10 25 45 ∞
4 1 3 1
{1,3,4,2,5}
∞ 45 10 25 45 ∞
4 1 3 1
6.5.2 每对顶点之间的最短路径
要求得每一对顶点之间的最短路径,可以这样进行:每次
以一个顶点为源点,重复执行迪杰斯特拉算法n次。此外还可
采用专为解决此问题而设计的算法———弗洛伊德(Floyed)
算法,这是弗洛伊德1962年提出的,此算法比较简单,易于理
解和编程。
弗洛伊德(Floyed)算法仍从图的带权邻接矩阵cost出发,
其基本思想是:
设立两个矩阵分别记录各顶点间的路径和相应的路径长度。
矩阵P表示路径,矩阵A表示路径长度。
那么,如何求得各顶点间的最短路径长度呢?初始时,复
制网的邻接矩阵cost为矩阵A,即顶点V i 到顶点V j 的最短路径
长度A[i][j]就是弧<V i ,Vj >所对应的权值,我们将它记
为A(0) ,其数组元素A[i][j]不一定是从Vi 到Vj 的最短路
径长度。要想求得最短路径长度,要进行n次试探。
对于从顶点Vi 到顶点Vj 的最短路径长度,首先考虑让路径
经过顶点V1 ,比较路径(V i ,Vj )和(Vi ,V1 ,Vj )的长
度取其短者为当前求得的最短路径。对每一对顶点都作这样的
试探,可求得A(1) 。然后,再考虑在A(1) 的基础上让路径经过
顶点V 2 ,于是求得A(2)。依此类推,一般地,如果从顶点V i
到顶点V j 的路径经过新顶点V k 使得路径缩短,则修改A(K)[i]
[j]=A(K-1)[i][k]+A(K-1)[k][j],所以,A(K)[i]
[j]就是当前求得的从顶点V i 到顶点V j 的最短路径长度,且
其路径上的顶点(除源点和终点外)序号均不大于k。这样经过
n次试探,就把几个顶点都考虑到相应的路径中去了。最后求
得的A(n) 就一定是各顶点间的最短路径长度。
综上所述,弗洛伊德算法的基本思想是递推地产生两个n
阶的矩阵序列。其中,表示最短路径长度的矩阵序列是A(0),
A(1),A(2),A(3),…,A(k),…,A(n),递推关系为
A(0)[i][j]=cost[i][j]
A(k)[i][j]=min{A(k)[i][j],A(k-1)[i][k]+A(k-1)
[k][j]}
(1<=i,j,k<=n)
现在来看如何在求得最短路径长度的同时求解最短路径。
初始矩阵P的各元素都赋-1。P[i][j]=-1表示V i 到V j 的路
径是直接可达,中间不经过其他顶点。以后,当考虑路径经过
某个顶点V k 时,如果使路径更短,则修复A(k)[i][j]的同
时令P[i][j]=k,即P[i][j]中存放的是从V i 到V j 的路
径上所经过的某个顶点(若P[i][j]≠-1)。那么,如何求
得从V i 到V j 的路径上的全部顶点呢?这只需要编写一个递归过
程即可解决,因为所有最短路径的信息都包含在矩阵P中了。
设经过n次拭探后,P[i][j]=k,即从Vi 到Vj 的最短路径经
过顶点Vk (若k≠0)。该路径上还有哪些顶点呢?这只要去查P
[i][k]和P[k][j],以此类推,直到所查元素为-1。
对于图6-18所示的有向带权图,按照弗洛伊德算法产生的
两个矩阵序列如图6-19所示。
图6-18 有向带权图及其邻接矩阵
图6-19 Floyed算法执行过程中数组A和P的变化过程
用弗洛伊德算法,求每一对顶点之间的最短路径的完整程序如
下:
#define MAX 50
int creatcost(int cost[ ][MAX])
{ /*cost这个二维数组用于表示图的邻接矩阵*/
int vexnum,arcnum,i,j,k,v1,v2,w; /*输入图的顶点
数和弧数或边数*/
printf(″input vexnum,arcnum:″);
scanf(″%d,%d″,&vexnum,&arcnum);
for(i=1;i<=vexnum;i++)
for(j=1;j<=vexnum;j++)
cost[i][j]=9999; /*本例设9999代表无限大*/
for(k=1;k<=arcnum;k++)
{
printf(″v1,v2,w=″);
scanf(″%d,%d,%d″,&v1,&v2,&w); /*输入所有
边或所有弧的一对顶点V1 ,V2 */
cost[v1][v2]=w;
}
return(vexnum); /*返回图中的顶点数*/
}
int p[MAX][MAX]; /*定义存放路径的数组P*/
void floyed(int cost[ ][MAX],int vexnum)
{/*用弗洛伊德算法求每一对顶点之间的最短路径*/
int a[MAX][MAX],i,j,k;
for(i=1;i<=vexnum;i++)
for(j=1;j<=vexnum;j++)
{
a[i][j]=cost[i][j]; /*给数组A和P数组赋初值*/
p[i][j]=-1;
}
for(i=1;i<=vexnum;i++)
/*同一顶点间的最短路径为
零*/
a[i][i]=0;
for(k=1;k<=vexnum;k++) /*通过递推求最短路径长度和
路径*/
{
for(i=1;i<=vexnum;i++)
for(j=1;j<=vexnum;j++)
if(a[i][k]+a[k][j]<a[i][j])
{
a[i][j]=a[i][k]+a[k][j];
p[i][j]=k;
}
}
printf(″shortest distance of each pair of nodes:\n″);
for(i=1;i<=vexnum;i++) /*输出每对顶点间的最短路径*/
{
for(j=1;j<=vexnum;j++)
printf(″%d″,a[i][j]);
printf(″\n″);
}
printf(″shortest path of each pair of nodes:\n″); /*输出
每一对顶点间的最短路径上的各个点*/
for(i=1;i<=vexnum;i++)
for(j=1;j<=vexnum;j++)
{
printf(″%d-->″,i);
putpath(i,j);
printf(″%d\n″,j);
}
}
putpath(int i,int j) /*输出一对顶点间的最短路径上的各个
点*/
{ int k;
k=p[i][j];
if(k==-1)
return;
putpath(i,k);
printf(″%d-->″,k);
putpath(k,j);
}
main()
{
int vexnum;
int cost[MAX ][MAX];
vexnum=creatcost(cost); /*建立图的邻接矩阵*/
floyed(cost,vexnum); /*调用算法求每一对顶点间的最
短路径*/
}
对于图6-18中的图,运行时输入数据及运行结果显示如下,其
中带下划线的部分为用户输入的。
input vexnum,arcnum:
3,5
v1,v2,w=1,2,4
v1,v2,w=1,3,11
v1,v2,w=2,3,2
v1,v2,w=3,1,3
v1,v2,w=2,1,6
shortest distance of each pair of nodes:
0 4 6
5 0 2
3 7 0
shortest path of each pair of nodes:
1-->1
1-->2
1-->2-->3
2-->3-->1
2-->2
2-->3
3-->1
3-->1-->2
3-->3
6.6 拓扑排序
利用没有回路的有向图描述一项工程或对工程的进度进行
描述是非常方便的。除最简单的情况之外,几乎所有的工程都
可分解为若干个称作活动的子工程,子工程之间受着一定的约
束。例如,某些子工程的开始必须在另一些子工程结束之后。
对于工程,人们普遍关心的是两个方面的问题:第一,工程是
否顺利进行;第二,估算整个工程完成所必须的最短时间。这
里所说的第一个问题就是本节所要讨论的有向图的拓扑排序问
题,而第二个问题则是下一节所要讨论的关键路径问题。
6.6.1 AOV网
在有向图中若以顶点表示活动,用有向边表示活动之间的
优先关系,则这样的有向图称为以顶点表示活动的网(Activity
On Vertex Network),简称AOV网。
在AOV网中若从顶点Vi 到顶点Vj 有一条有向路径,则Vi
是Vj 的前趋,Vj 是Vi 的后继。若<V i ,V j >是网中的一条弧,
则V i 是V j 的直接前趋,V j 是V i 的直接后继。
例如,计算机专业的学生必须学完一系列规定的课程后才
能毕业。这可看作一个工程,用图6-20所示的网来表示。网中
的顶点表示各门课程的教学活动,有向边表示各门课程的制约
关系。例如在图6-20中的一条弧<V3 ,V 5 >表示“C程序设计”
是“数据结构”的直接前趋,也就是说“C程序设计”这一教
学活动一定要安排在“数据结构”这一教学活动之前。
在AOV网中,由弧表示的优先关系具有传递性,如顶点V2
是顶点V5 的前趋,而顶点V5 是顶点V6 的前趋,则顶点V2 也
是顶点V6 的前趋。并且在AOV网中不能出现有向回路,如果
存在回路,则说明某项“活动”能否进行要以自身任务的完成
作为先决条件,显然,这样的工程是无法完成的。如果要检测
一个工程是否可行,首先就得检查对应的AOV网是否存在回路。
检查AOV网中是否存在回路的方法就是拓扑排序。
课程代号 课程名称 先修课程
1
高等数学
无
2
程序设计基础 无
3
C程序设计
1,2
4
离散数学
1
5
数据结构
2,3,4
6
编译方法
4,5
7
操作系统
5
图6-20 表示课程间关系的AOV网
6.6.2 拓扑(Topology)排序的实现
拓扑有序序列:它是由AOV网中的所有顶点构成的一个线
性序列,在这个序列中体现了所有顶点间的优先关系。即若在
AOV网中从顶点Vi 到顶点Vj 有一条路径,则在序列中 Vi 排在
V j 的前面,而且在此序列中使原来没有先后次序关系的顶点之
间也建立起人为的先后关系。
拓扑排序:构造拓扑有序序列的过程称为拓扑排序。例如,
对于图6-20可有如下的拓扑有序序列:(V 1,V2 ,V3 ,V4 ,
V5 ,V6 ,V7 )和(V2 ,V1 ,V3 ,V4 ,V5 ,V6 ,
V7),由此可知,有向图的拓扑序列不是唯一的,所以学生选
课次序也不是唯一的,只要符合任一选课的拓扑序列是合理的。
对AOV网进行拓扑排序的步骤是:
1.在网中选择一个没有前趋的顶点且输出之;
2.在网中删去该顶点,并且删去从该顶点出发的全部有
向边;
3.重复上述两步,直到网中所有顶点都被输出,说明网
中不存在有向回路;若网中顶点未被全部输出,说明网中存在
有向回路。
对于图6-20,其拓扑排序过程如图6-21所示。
图6-21 拓扑排序过程
下面讨论拓扑排序的算法实现。根据拓扑排序的方法,把
入度为零的顶点插入一个队列,按顺序输出。而顶点的入度可
以记录在邻接表数组的数据域中,即记录在adjlist[v].data中。
拓扑排序的完整程序如下。
#include<stdlib.h>
#define MAX 50
typedef struct arcnode
/*定义表结点*/
{
int vextex;
struct arcnode *next;
}ARCNODE;
typedef struct vexnode /*定义头结点*/
{ int data;
ARCNODE*firstarc;
}VEXNODE;
VEXNODE adjlist[MAX]; /*定义表头向量adjlist*/
int creatadjlist()
/*建立邻接表*/
{ ARCNODE*ptr;
int arcnum,vexnum,k,v1,v2;
printf(″input vexnum,arcnum:\n″);
scanf(″%d,%d″,&vexnum,&arcnum); /*输入图的顶点数
和边数(弧数)*/
for(k=1;k<=vexnum;k++)
{ adjlist[k].firstarc=NULL; /*邻接链表的adjlist数组各元
素的链域赋初值*/
adjlist[k].data=0;
/*各顶点的入度赋初值0*/
}
for(k=1;k<=arcnum;k++)
/*为adjlist数组的各元素分别建
立各自的链表*/
{
printf(″v1,v2=″);
scanf(″%d,%d″,&v1,&v2);
/*输入弧<V1 ,V2
>*/
ptr=(ARCNODE*)malloc(sizeof(ARCNODE));
/*给结点V1 的相邻结点V2 分配内存空间*/
ptr->vextex=v2;
ptr->next=adjlist[v1].firstarc;
adjlist[v1].firstarc=ptr;
点V1 之后*/
adjlist[v2].data++;
}
return(vexnum);
}
void toposort(int n)
点的个数*/
{
int queue[MAX];
int front=0,rear=0;
int v,w,n1;
ARCNODE *p;
/*将相邻结点V2 插入表头结
/*顶点V2 的入度加1*/
/*拓扑排序算法,n为图中顶
n1=0;
for(v=1;v<=n;v++)
/*循环检测入度为0的顶点并
入队*/
if(adjlist[v].data==0)
{
rear=(rear+1)%MAX;
queue[rear]=v;
}
printf(″result of toposort is:\n″);
while(front!=rear)
{
front=(front+1)%MAX;
v=queue[front];
printf(″%d ″,v);
/*输出入度为0的顶点并计数*/
n1++;
p=adjlist[v].firstarc;
while(p!=NULL)
/*删除由顶点V出发的所有的弧*/
{
w=p->vextex;
adjlist[w].data--;
/*将邻接于顶点V的顶点的入度
减1*/
if(adjlist[w].data==0) /*将入度为0的顶点入队*/
{ rear=(rear+1)%MAX;
queue[rear]=w;
}
p=p->next;
/*p指向下一个邻接于顶点V的顶点*/
}
}
if(n1<n)
/*输出的顶点个数小于图的顶点个数,则拓扑
排序失败*/
printf(″not a set of partial order\n″);
}
main()
{
int i,n;
ARCNODE*p;
n=creatadjlist();
/*建立邻接表并返回顶点的
个数*/
toposort(n);
/*对于具有n个顶点的图进行拓扑排序
*/
}
对于图6-20,运行时输入数据及运行结果显示如下,其中带下
划线的部分为用户输入的。
input vexnum,arcnum:
7,9
v1,v2=1,3
v1,v2=1,4
v1,v2=2,3
v1,v2=2,5
v1,v2=3,5
v1,v2=4,5
v1,v2=4,6
v1,v2=5,6
v1,v2=5,7
result of toposort is:
1 2 4 3 5 7 6
如果给定的有向图有n个顶点e条边,那么建立邻接表的时
间为O(e)。在拓扑排序的过程中,搜索入度为零的顶点的时间
为O(n),顶点入队和出队要执行n次,入度减1的顶点操作执
行e次,所以总的执行时间为O(n+e)。
6.6 拓扑排序
1.图(Graph)是一种非线性数据结构,图中的每个元素
既可有多个直接前趋,也可有多个直接后继。图分为有向图和
无向图,有向图中的边(又称弧)是顶点的有序对,无向图中
的边是顶点的无序对。
2.图的存储结构常用的有邻接矩阵、邻接表二种。
3.若要系统地访问图中的每个顶点,可以采用深度优先
搜索和广度优先搜索遍历算法。
4.对于图的应用有求最小生成树问题、最短路径问题和
拓扑排序问题。求图的最小生成树可以采用普里姆算法
(Prime)和克鲁斯卡尔算法(Kruskal);求最短路径既可用
迪杰斯特拉(Dijkstra)算法求从某个源点到其他各顶点的最短
路径,也可用弗洛伊德(Floyed)算法求每一对顶点之间的最
短路径;而拓扑排序所得到的拓扑序列则是判别某项工程能否
顺利进行及各子工程开工顺序安排的依据,若拓扑序列没有构
造成功,则该工程不能顺利进行,若拓扑序列构造成功,则可
按拓扑序列的顺序安排各子工程的开工顺序。
习题6
一、选择题
1.图中有关路径的定义是( )。
A.由顶点和相邻顶点对构成的边所形成的序列
B.由不同
顶点所形成的序列
C.由不同边所形成的序列
D.上述定义都不
是
2.设无向图的顶点个数为n,则该图最多有( )条边。
A.n-1
B.n(n-1)/2
C. n(n+1)/2
D.0
E.n2
3.一个n个顶点的连通无向图,其边的个数至少为( )。
A.n-1
B.n
C.n+1
D.nlog2n;
4.要连通具有n个顶点的有向图,至少需要( )条边。
A.n-l
B.n
C.n+l
D.2n
5.n个结点的完全有向图含有边的数目(
)。
A.n*n
B.n(n+1) C.n/2
D.n*(n-l)
6.一个有n个结点的图,最少有( )个连通分量,最多
有( )个连通分量。
A.0
B.1
C.n-1
D.n
7.在一个无向图中,所有顶点的度数之和等于所有边数
( )倍,在一个有向图中,所有顶点的入度之和等于所有顶
点出度之和的( )倍。
A.1/2
B.2
C.1
D.4
8.用有向无环图描述表达式(A+B)*((A+B)/A),至少
需要顶点的数目为( )。
A.5
B.6
C.8
D.9
9.用DFS遍历一个无环有向图,并在DFS算法退栈返回
时打印相应的顶点,则输出的顶点序列是( )。
A.逆拓扑有序
B.拓扑有序
C.无序的
10.下面结构中最适于表示稀疏无向图的是( ),适于
表示稀疏有向图的是( )。
A.邻接矩阵
B.逆邻接表 C.邻接多重表
D.十字链
表 E.邻接表
11.下列哪一种图的邻接矩阵是对称矩阵?( )
A.有向图
B.无向图
C.AOV网
D.AOE
网
12. 关键路径是事件结点网络中( )。
A.从源点到汇点的最长路径
B.从源点到汇点的最短路
径
C.最长回路
D.最短回路
二、判断题
1.树中的结点和图中的顶点就是指数据结构中的数据元素。
( )
2.在n个结点的无向图中,若边数大于n-1,则该图必是连通图。
( )
3. 有e条边的无向图,在邻接表中有e个结点。( )
4. 有向图中顶点V的度等于其邻接矩阵中第V行中的1的个数。
( )
5.强连通图的各顶点间均可达。( )
6.邻接多重表是无向图和有向图的链式存储结构。( )
7. 十字链表是无向图的一种存储结构。( )
8. 无向图的邻接矩阵可用一维数组存储。( )
9.用邻接矩阵法存储一个图所需的存储单元数目与图的边数
有关。( )
10.有n个顶点的无向图, 采用邻接矩阵表示, 图中的边数
等于邻接矩阵中非零元素之和的一半。( )
11. 有向图的邻接矩阵是对称的。( )
12. 用邻接矩阵存储一个图时,在不考虑压缩存储的情况
下,所占用的存储空间大小与图中结点个数有关,而与图的边
数无关。( )
13. 求最小生成树的普里姆(Prim)算法中边上的权可正可负。
( )
14.拓扑排序算法把一个无向图中的顶点排成一个有序序
列。( )
三、填空题
1.判断一个无向图是一棵树的条件是______。
2.有向图G的强连通分量是指______。
3.一个连通图的______是一个极小连通子图。
4.具有10个顶点的无向图,边的总数最多为______。
5.若用n表示图中顶点数目,则有_______条边的无向图成为完
全图。
6.G是一个非连通无向图,共有28条边,则该图至少有______
个顶点。
7. 在有n个顶点的有向图中,若要使任意两点间可以互相到达,则
至少需要______条弧。
8.在有n个顶点的有向图中,每个顶点的度最大可达______。
9.设G为具有N个顶点的无向连通图,则G中至少有______条边。
10.n个顶点的连通无向图,其边的条数至少为______。
11.如果含n个顶点的图形形成一个环,则它有______棵生成树。
四、 应用题
1.(1).如果G1是一个具有n个顶点的连通无向图,那
么G1最多有多少条边?G1最少有多少条边?
(2).如果G2是一个具有n个顶点的强连通有向图,那么
G2最多有多少条边?G2最少有多少条边?
(3).如果G3是一个具有n个顶点的弱连通有向图,那么
G3最多有多少条边?G3最少有多少条边?
2.n个顶点的无向连通图最少有多少条边?n个顶点的有
向连通图最少有多少条边?
4.证明:具有n个顶点和多于n-1条边的无向连通图G一定
不是树。
5.证明对有向图的顶点适当的编号,可使其邻接矩阵为
下三角形且主对角线为全0的充要条件是该图为无环图。
6.用邻接矩阵表示图时,矩阵元素的个数与顶点个数是否相关?与
边的条数是否有关?
7.请回答下列关于图(Graph)的一些问题:
(1).有n个顶点的有向强连通图最多有多少条边?最少有多少条
边?
(2).表示有1000个顶点、l000条边的有向图的邻接矩阵有多少
个矩阵元素?是否稀疏矩阵?
(3).对于一个有向图,不用拓扑排序,如何判断图中是否存在
环?
8.解答问题。设有数据逻辑结构为:
B = (K, R), K = {k1, k2, …, k9}
R={<k1, k3>, <k1, k8>, <k2, k3>,<k2, k4>, <k2, k5>, <k3, k9>,<k5,
k6>, <k8, k9>, <k9, k7>, <k4, k7>, <k4, k6>}
(1).画出这个逻辑结构的图示。
(2).相对于关系r, 指出所有的开始接点和终端结点。
(3).分别对关系r中的开始结点,举出一个拓扑序列的例子。
五、算法设计题
1.设无向图G有n个顶点,m条边。试编写用邻接表存储
该图的算法。(设顶点值用1~n或0~n-1编号)
2.给出以十字链表作存储结构,建立图的算法,输入(i,j,v)
其中i,j为顶点号,v为权值。
3.设有向G图有n个点(用1,2,…,n表示),e条边,写一算法根
据其邻接表生成其反向邻接表,要求算法复杂性为O(n+e)。
项目实 训 5
实训目的要求:
深理解图的基本概念和存储方式。
握以邻接链表为存储结构的有向图上,实现有向图的拓扑
排序方法。
实训内容:
设有向图如图6-22所示,将该图以邻接表的形式进行存储,
并对其实现拓扑排序。
输入数据并观察输出结果。
图6-22 有向图
项目实训参考程序
#include <stdio.h>
#include <stdlib.h>
#define VEXTYPE int
#define MAXLEN 40
typedef struct nodee
{int adjvex;
struct nodee *next;
}EDGENODE;
typedef struct
{VEXTYPE vertex;
int id;
EDGENODE *link;
}VEXNODE;
typedef struct
{VEXNODE adjlist[MAXLEN];
int vexnum,arcnum;
int kind;
}ADJGRAPH;
ADJGRAPH creat_adjgraph()
{/*建立有向图的邻接链表结构*/
EDGENODE *p;
int i,s,d;
ADJGRAPH adjg;
adjg.kind = 1;
printf("\n\n输入顶点数和边数(用逗号隔开) : ");
scanf("%d,%d", &s,&d);fflush(stdin);
adjg.vexnum = s;
/*存放顶点数在
adjg.vexnum中 */
adjg.arcnum = d;
/*存放边点数在
adjg.arcnum中*/
printf("\n\n");
for(i = 0; i < adjg.vexnum; i++)
/*邻接链表顶点初始化*/
{printf("输入顶点 %d 的值 : ", i + 1);
scanf("%d", &adjg.adjlist[i].vertex); /*输入顶点的值*/
fflush(stdin);
adjg.adjlist[i].link = NULL;
adjg.adjlist[i].id = 0;}
printf("\n\n");
for(i = 0; i < adjg.arcnum; i++)
{printf("输入第 %d 条有向弧的起始顶点和终止顶点(用
逗号隔开): ", i + 1);
scanf("%d,%d",&s,&d);
/*输入弧的起始顶点
和终止顶点*/
fflush(stdin);
while(s < 1 || s > adjg.vexnum || d < 1 || d > adjg.vexnum)
{printf("输入错,重新输入: ");
scanf("%d,%d", &s,&d);}
s --;
d --;
p = (EDGENODE*)malloc(sizeof(EDGENODE));
/*每条
弧对应生成一个结点*/
p->adjvex = d;
p->next = adjg.adjlist[s].link;
/*结点插入对应的单
链表上*/
adjg.adjlist[s].link = p;
adjg.adjlist[d].id++;}
/*弧的终端顶点入度加
1*/
return adjg;
}
void topsort(ADJGRAPH ag)
{
int i, j, k, m, n, top;
EDGENODE *p;
n = ag.vexnum;
top = -1;
/*拓扑排序中用到的栈初始
化*/
for(i = 0; i < n; i++)
if(ag.adjlist[i].id == 0)
/*入度为0的结点压入一
链栈,top指向栈顶结点*/
{ ag.adjlist[i].id = top;
top = i;}
m = 0;
printf("\n\n有向图拓扑排序结果 : ");
while(top != -1)
/*栈不空,进行拓扑排序*/
{j = top;
/*取栈顶元素*/
top = ag.adjlist[top].id;
/*删除一个栈顶元素*/
printf(" %d", ag.adjlist[j].vertex);/*输出一个拓扑排序
结点即栈顶元素*/
m++;
p = ag.adjlist[j].link;
while(p != NULL)
/*如果拓扑排序结点有
出度*/
{k = p->adjvex;
ag.adjlist[k].id--;
/*删除相关的弧,即弧终点
结点的入度减1*/
if(ag.adjlist[k].id == 0)
/*出现新的入度为0的顶
点,将其入栈*/
{ag.adjlist[k].id = top;
top = k;}
p = p->next;}}
if(m < n)
printf("\n\n有向图中有环路 !\n\n"); /*拓扑排序过程中输出
的顶点数<有向图的顶点数*/
}
main()
{ ADJGRAPH ag;
printf("\n有向图的拓扑排序\n");
ag = creat_adjgraph();
/*建立有向图的邻接链表结构*/
topsort(ag);
/*进行拓扑排序*/
printf("\n\n");
}
程序运行结果(加下划线的为用户自己输入):
输入顶点数和边数(用逗号隔开) :5,7
输入顶点 1 的值 : 1
输入顶点 2 的值 : 2
输入顶点 3 的值 : 3
输入顶点 4的值 : 4
输入顶点 5 的值 : 5
输入第1条有向弧的起始顶点和终止顶点(用逗号隔开): 1,2
输入第2条有向弧的起始顶点和终止顶点(用逗号隔开): 1,4
输入第3 条有向弧的起始顶点和终止顶点(用逗号隔开): 1,5
输入第 4条有向弧的起始顶点和终止顶点(用逗号隔开): 2,3
输入第5 条有向弧的起始顶点和终止顶点(用逗号隔开): 4,3
输入第 6条有向弧的起始顶点和终止顶点(用逗号隔开): 5,3
输入第 7 条有向弧的起始顶点和终止顶点(用逗号隔开): 5,4
有向图的拓扑排序1 2 5 4 3