第
5 
章图

教学重点

教学难点

教学目标

教学提示

图的基本术语;图的存储表示;图的遍历;图的经典应用

图的遍历;最小生成树;最短路径;拓扑排序;关键路径

(1)解释图的定义和基本术语,设计图的抽象数据类型定义;
(2)辨析图的深度优先和广度优先遍历方法,针对图结构给出遍历结果;
(3)辨析图的邻接矩阵和邻接表存储方法,分析查找邻接点等操作的执
行过程和效率,画出存储示意图,描述存储结构定义;
(4)基于图的邻接矩阵和邻接表存储,设计构造图、遍历等算法;
(5)陈述Pim 
算法和Kkl算法的基本思想,说明贪心选择策略,描
解释算法的程序实现,评价时间
性能; 述算法执(r) 行过程中存储(r) 单(s) 元(a) 的变化,(u) 
(6)陈述Dijkstra算法的基本思想,设计存储结构,描述求解过程中存储
单元的变化,解释Dijkta算法的程序实现,评价时间性能;
(7)陈述Flo建立递推关系式,描述求解过程中存储
单元的变化,解释Flyd算法的程序实现,评价时间性能;yd算法的基本思(s) 想,(r) 
(8)解释AOV 
网的定说明拓扑序列的含义,陈述拓扑排序算法的基
本思想,描述算法执行过程中存储单元的变化,评价时间性能; 义,(o) 
(9)辨析AOV 
网和AOE 
网,论证关键路径和关键活动对工程进度的影
响,分析关键路径的求解思想,给出算法执行过程中存储单元的变化
对于本章的教学要抓住一条明线:图的逻辑结构→图的存储结构→图的
遍历→图的经典应用。对于图的逻辑结构,要从图的定义出发,在与线
性表和树的定义进行比较的基础上,深刻理解图结构的逻辑特征。对于
图的存储结构,以如何表示图中顶点之间的逻辑关系为出发点,掌握图
的不同存储结构以及它们之间的关系,并学会在实际问题中修改存储
结构。
图的遍历是本章的重点和难点,注意讲授思路。首先从逻辑上(即不涉
及存储结构)掌握图的遍历操作,理解伪代码算法;其次在与树的遍历进
行比较的基础上,提出图遍历的关键问题和解决办法;最后基于邻接矩
阵和邻接表存储结构实现图的遍历操作。
对于本章的经典算法(Prim 
算法、Kruskal算法、Dijkstra算法、Floyd 
算
法和拓扑排序算法等)
, 
首先要理解算法的基本思想,并将算法思想用顶
层伪代码进行描述,然后在分析算法执行过程的基础上得出算法采用的
存储结构,最后才能掌握具体的算法


146
数据结构———从概念到
C 
实现(第2版) 


5.引言
1 

课件5-
1 
在线性结构中,数据元素之间仅具有线性关系,每个数据元素最多只有一个前驱和一
个后继;在树结构中,结点之间具有层次关系,每个结点最多只有一个双亲,但可以有多个
孩子;在图结构中,任意两个顶点之间都可能有关系,每个顶点都可以有多个邻接点。图
结构具有极强的表达能力,很多问题抽象出的数据模型是图结构。下面请看两个例子。
例5-
1 
七巧板涂色问题。假设有如图5-1(a)所示的七巧板,使用至多4种不同颜色
对七巧板涂色,要求每个区域涂一种颜色,相邻区域的颜色互不相同。如何求得涂色方案
呢? 为了识别不同区域的相邻关系,可以将七巧板的每个区域看成一个顶点,如果两个区
域相邻,则这两个顶点之间有边相连,从而将七巧板抽象为图结构,如图5-1(b)所示。


图5-
1 
七巧板及其数据模型

例5-
2 
农夫过河问题。一个农夫带着一只狼、一只羊和一筐菜,想从河一边(左岸)
乘船到另一边(右岸), 由于船太小,农夫每次只能带一样东西过河,但是如果没有农夫看
管,则狼会吃羊,羊会吃菜。农夫怎样过河才能把每样东西安全地送过河呢? 在某一时
刻,农夫、狼、羊和菜或者在河的左岸,或者在河的右岸,可以用0表示在河的左岸,用1表
示在河的右岸,如图5-2(a)所示,例如1010 表示农夫和羊在河的右岸、狼和菜在河的左
岸。将每一个可能的状态抽象为一个顶点,边表示状态转移发生的条件,从而将农夫过河
问题抽象为一个图模型,如图5-2(b)所示。


图5-
2 
农夫过河问题及其数据模型


第
5 
章 图 471

5.图的逻辑结构
2 


5.2.1 
图的定义和基本术语课件5-
2 
在图中常常将数据元素称为顶点(vertex)。

1. 
图的定义
图(graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示
为
G=(E)


V,
其中,
G 
表示一个图,
V 
是
G 
中顶点的集合,
E 
是
G 
中顶点之间边的集合。例如,在
图5-a) 顶点集合
V 
={v0,v2,v3,v4}, ={(v1), 

3(所示的G1中, v1,边的集合Ev0,
(v3),(v2),(v4),(v3),()}。

v0,v1,v1,v2,v2,v4 
在图中,若顶点vi 
和vj 
之间的边没有方向,则称这条边为无向边,用无序偶对(vi, 
vj)表示;若从顶点vi 
到vj 
的边有方向,则称这条边为有向边(也称为弧,以区别于无向
vi 
vj

边), 用有序偶对<vi,vj 
>表示,称为弧尾,称为弧头。如果图的任意两个顶点之

间的边都是无向边,则称该图为无向图(undirectedgraph), 否则称该图为有向图

(directedgraph)。例如,图5-3(a)所示是一个无向图,图5-3(b)所示是一个有向图。


图5-
3 
图的示例

在图中,权(weight)通常是指对边赋予的有意义的数值量①。在实际应用中,权可以
有具体的含义。例如,对于城市交通线路图,边上的权表示该条线路的长度或者等级;对
于工程进度图,边上的权表示活动所需要的时间等。边上带权的图称为带权图或网图
(networkgraph)。例如,图5-3(c)所示是一个无向网图,图5-3(d)所示是一个有向网图。

2. 
图的基本术语
(1)邻接、依附。
vi,i
在无向图中,对于任意两个顶点vi 
和vj 
,若存在边(vj 
), 则称顶点v和vj 
互为
adaevi,aee)

邻接点②(jcnt), 同时称边(vj)依附(dhr于顶点vi 
和vj 
。
在有向图中,对于任意两个顶点vi 
和vj 
,若存在弧<vi,vj 
>,则称顶点vi 
邻接到
vj 
,顶点vj 
邻接自vi, vj 
>依附于顶点vi 
。在不致混淆的情况下,通

同时称弧<vi,和vj 

①本书只讨论边上带权的图,且权值是非负整数的情况。
② 在线性结构中,数据元素之间的逻辑关系表现为前驱———后继;在树结构中,结点之间的逻辑关系表现为双
亲———孩子;在图结构中,顶点之间的逻辑关系表现为邻接。

1 48 数据结构———从概念到C 实现(第2 版) 
常称vj 是vi 的邻接点。
(2)顶点的度、入度、出度。
在无向图中,顶点v 的度(degree)是指依附于该顶点的边的个数,记为TD(v)。在
具有n 个顶点e 条边的无向图中,有下式成立: 
Σn-1 
i=0TD(vi)=2e (5-1) 
在有向图中,顶点v 的入度(in-degree)是指以该顶点为弧头的弧的个数,记为
ID(v);顶点v 的出度(out-degree)是指以该顶点为弧尾的弧的个数,记为OD(v)。在具
有n 个顶点e 条边的有向图中,有下式成立: 
Σn-1 
i=0ID(vi)=Σn-1 
i=0OD(vi)=e (5-2) 
(3)无向完全图、有向完全图。
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图(undirected 
completegraph)。含有n 个顶点的无向完全图有n(n-1)/2条边。
在有向图中,如果任意两顶点之间都存在方向互为相反的两条弧,则称该图为有向完
全图(directedcompletegraph)。含有n 个顶点的有向完全图有n(n-1)条边。
(4)稀疏图、稠密图。
称边数很少的图为稀疏图(sparsegraph),反之,称为稠密图①(densegraph)。
(5)路径、路径长度、回路。
在无向图G=(V,E )中,顶点vp 到vq 之间的路径(path)是一个顶点序列vp = 
vi0vi1…vim =vq,其中,(vij-1,vij)∈E(1≤j≤m );如果G 是有向图,则<vij-1,vij>∈ 
E(1≤j≤m )。路径上边的数目称为路径长度(pathlength)。第一个顶点和最后一个顶
点相同的路径称为回路(circuit)。显然,在图中路径可能不唯一,回路也可能不唯一。
(6)简单路径、简单回路。
在路径序列中,顶点不重复出现的路径称为简单路径(simplepath)。除了第一个顶
点和最后一个顶点之外,其余顶点不重复出现的回路称为简单回路(simplecircuit)。通
常情况下,路径指的都是简单路径,回路指的都是简单回路。
(7)子图。
对于图G=(V,E)和G'=(V',E'),如果V'.V 且E'.E,则称图G'是G 的子图② 
(subgraph)。图5-4给出了子图的示例,显然,一个图可以有多个子图。
(8)连通图、连通分量。
在无向图中,若顶点vi 和vj(i≠j)之间有路径,则称vi 和vj 是连通的。若任意顶
点vi 和vj(i≠j)之间均有路径,则称该图是连通图(connectedgraph)。例如,图5-4(a) 
是连通图,图5-5(a)是非连通图。非连通图的极大连通子图称为连通分量(connected 
component),极大的含义是指在满足连通的条件下,包括所有连通的顶点以及和这些顶
①
② 
稀疏和稠密本身就是模糊的概念,稀疏图和稠密图常常是相对而言的。显然,最稀疏图的边数是0,最稠密
图是完全图,边数达到最多。
通俗地说,子图是原图的一部分,是由原图中一部分顶点和这些顶点之间的一部分边构成的图。

第 
5 
章 图 491

点相关联的所有边。图5-5(a)所示非连通图有两个连通分量,如图5-5(b)所示。


图5-
4 
子图的例子


图5-
5 
非连通图及连通分量

(9)强连通图、强连通分量。
i≠j), 
在有向图中,对任意顶点vi 
和vj(若从顶点vi 
到vj 
均有路径,则称该有向图
是强连通图①(stronglyconnectedgraph)。图5-6(a)是强连通图,图5-6(b)是非强连通
图。非强连通图的极大强连通子图②称为强连通分量(stronglyconnectedcomponent)。
图5-6(b)所示非强连通图有两个强连通分量,如图5-6(c)所示。


图5-
6 
强连通图、非强连通图及强连通分量

5.2.2 
图的抽象数据类型定义
图是一种与具体应用密切相关的数据结构,它的基本操作往往随应用不同而有很大
差别。下面给出一个图的抽象数据类型定义的例子,简单起见,基本操作仅包含图的遍
历,针对具体应用,需要重新定义其基本操作。

①在有向图中,若顶点vi 
和vj 
是连通的,则顶点vi 
和vj 
必在同一条回路上。
② 此处极大的含义同连通分量,是指在满足连通的条件下,包括所有连通的顶点以及和这些顶点相关联的所有边。

1 50 数据结构———从概念到C 实现(第2 版) 
ADT Graph 
DataModel 
顶点的有穷非空集合和顶点之间边的集合
Operation 
CreatGraph 
输入: n 个顶点e 条边 
功能: 图的建立 
输出: 构造一个含有n 个顶点e 条边的图 
DestroyGraph 
输入: 无 
功能: 图的销毁 
输出: 释放图占用的存储空间 
DFTraverse 
输入: 遍历的起始顶点v 
功能: 从顶点v 出发深度优先遍历图 
输出: 图的深度优先遍历序列 
BFTraverse 
输入: 遍历的起始顶点v 
功能: 从顶点v 出发广度优先遍历图 
输出: 图的广度优先遍历序列
endADT 
5.2.3 图的遍历操作
图的遍历是指从图中某顶点出发,对图中所有顶点访问①一次且仅访问一次。图的
遍历操作和树的遍历操作类似,但由于图结构本身的复杂性,所以图的遍历操作也比较复
杂。在图的遍历中要解决的关键问题如下。
(1)在图中,没有一个确定的开始顶点,任意一个顶点都可作为遍历的起始顶点,那
么,如何选取遍历的起始顶点? 
(2)从某个顶点出发可能到达不了所有其他顶点,例如非连通图,从一个顶点出发, 
只能访问它所在连通分量上的所有顶点,那么,如何才能遍历图的所有顶点? 
(3)由于图中可能存在回路,某些顶点可能会被重复访问,那么,如何避免遍历不会
因回路而陷入死循环? 
(4)在图中,一个顶点可以和其他多个顶点相邻接,当这样的顶点访问过后,如何选
取下一个要访问的顶点? 
问题(1)的解决:既然图中没有确定的开始顶点,那么可从图中任一顶点出发,不妨
将顶点进行编号,先从编号小的顶点开始。在图中,由于任何两个顶点之间都可能存在
边,顶点没有确定的先后次序,所以,顶点的编号不唯一。在图的存储实现上,一般采用一
维数组存储图的顶点信息,因此,可以用顶点的存储位置(即下标)表示该顶点的编号。为
① 此处访问的含义同树的遍历中访问的含义。不失一般性,在此将访问定义为输出顶点的数据信息。

第 
5 
章 图 511

了和C语言中的数组保持一致,图的编号从0开始。

问题(2)的解决:要遍历图中所有顶点,只需多次重复从某一顶点出发进行图的遍
历。以下仅讨论从某一顶点出发遍历图的问题。

问题(3)的解决:为了在遍历过程中便于区分顶点是否已被访问,设置一个访问标
志数组visited[n](其初值为未被访问标志0,

n 
为图中顶点的个数), 如果某顶点
i 
已被访
问,则将该顶点的访问标志visitei]

d[置为1。
问题(4)的解决:这就是遍历次序的问题。图的遍历通常有深度优先遍历和广度优
先遍历两种方式,这两种遍历次序对无向图和有向图都适用。
深度优先遍历①(depth-firsttraverse)类似于树的前序遍历。从图中某顶点
v 
出发进
行深度优先遍历的基本思想如下。

(1)访问顶点v;
(2)从
v 
的未被访问的邻接点中选取一个顶点w,然后从
w 
出发进行深度优先
遍历;
(3)重复上述两步,直至图中所有和
v 
有路径相通的顶点都被访问到
。
显然,深度优先遍历图是一个递归过程,算法思想用伪代码描述如下②
:
算法:DFTraverse 
输入:顶点的编号v 
输出:无

1. 访问顶点v;修改标志visited[v]=1; 
2.w=顶点v的第一个邻接点; 
3.while(w存在)
3.f(w未被访问)从顶点w出发递归执行该算法;
1i

3.=顶点v的下一个邻接点;
2w

图5-7给出了对无向图进行深度优先遍历的过程示例,在访问v0 后选择未曾访问的
邻接点v1,访问v1 后选择未曾访问的邻接点v4,由于v4 没有未曾访问的邻接点,递归返
回到顶点v1,选择未曾访问的邻接点v2,从v2 出发进行深度优先遍历,以此类推,得到深
度优先遍历序列为v0v1v4v2v3v5。

广度优先遍历(breadth-firsttraverse)类似于树的层序遍历。从图中某顶点
v 
出发
进行广度优先遍历的基本思想如下。

(1)访问顶点v; 
v2,…,

(2)依次访问
v 
的各个未被访问的邻接点v1,vk 
;
(3)分别从v1,vk 
出发依次访问它们未被访问的邻接点,
v2,…,直至图中所有与顶

① 深度优先遍历算法由约翰·霍普克洛夫特和罗伯特·陶尔扬发明。当他们的研究成果在ACM 上发表以
后,引起学术界很大的轰动,深度优先遍历算法在信息检索、人工智能等领域得到成功应用。
② 该算法不依赖于图的存储结构,不涉及具体的实现细节,仅描述算法的基本思路。读者要学习并掌握这种用
伪代码描述顶层算法的方法。

152
数据结构———从概念到
C 
实现(第2版) 


图5-
7 
无向图的深度优先遍历示例

点
v 
有路径相通的顶点都被访问到。
广度优先遍历图是以顶点
v 
为起始点,由近至远,依次访问和
v 
有路径相通且路径

长度为1,2,…的顶点。为了使“先被访问顶点的邻接点”先于“后被访问顶点的邻接点” 

被访问,设置队列存储已被访问的顶点。例如,对图5-8所示有向图进行广度优先遍历, 

访问v0 后将v0 入队;将v0 出队并依次访问v0 的未曾访问的邻接点v1 和v2,将v1 和

v2 入队;将v1 出队并访问v1 的未曾访问的邻接点v4,将v4 入队;重复上述过程,得到顶
点访问序列v0v1v2v4v3,图5-9给出了广度优先遍历过程中队列的变化。


图5-
8 
一个有向图图5-
9 
广度优先遍历过程中队列的变化

广度优先遍历的算法思想用伪代码描述如下: 

算法:BFTraverse 
输入:顶点的编号v 
输出:无

1. 队列Q初始化; 
2. 访问顶点v;修改标志visited[v]=1;顶点v入队列Q; 
3.while(队列Q非空)
3.=队列Q的队头元素出队;
1v

2 w= 顶点v的第一个邻接点;

3.
3.3 while(w存在)
3.1 如果w未被访问,则
3.
3.
访问顶点w;修改标志visited[w]=1;顶点w入队列Q; 
3.2 w= 顶点v的下一个邻接点; 

第5 章 图 1 53 
5.3 图的存储结构及实现
图是一种复杂的数据结构,表现在顶点之间的逻辑关系(即邻接关系)错综复杂。从
图的定义可知,一个图包括两部分:顶点的信息以及顶点之间边的信息。无论采用什么
方法存储图,都要完整、准确地反映这两方面的信息。
在图中,任意两个顶点之间都可能存在边,所以无法通过顶点的存储位置反映顶点之
间的邻接关系,因此图没有顺序存储结构。一般来说,图的存储结构应根据具体问题的要
求来设计,下面介绍两种常用的存储结构———邻接矩阵和邻接表。
5.3.1 邻接矩阵
1.邻接矩阵的存储结构定义 
图的邻接矩阵(adjacencymatrix)存储也称为数组表示法,是用一个一维数组存储图
中的顶点,用一个二维数组存储图中的边(即各顶点之间的邻接关系),存储顶点之间邻接
关系的二维数组称为邻接矩阵。设图G =(V,E )有n 个顶点,则邻接矩阵是一个n×n 
的方阵,定义为
edge[i][j]= 1 (vi,vj)∈E 或<vi,vj >∈E 
0 否则{ (5-3) 
若G 是网图,则邻接矩阵定义为
edge[i][j]= 
wij (vi,vj)∈E 或<vi,vj >∈E 
0 i=j 
∞ 否则
ì
.
í
.. 
..
(5-4) 
其中,wij表示边(vi,vj)或弧<vi,vj>上的权值;∞表示一个计算机允许的、大于所有边
上权值的数。图5-10所示为一个无向图及其邻接矩阵存储示意图,图5-11所示为一个
有向网图及其邻接矩阵存储示意图。
图5-10 一个无向图及其邻接矩阵存储示意图图5-11 一个有向网图及其邻接矩阵存储示意图
显然,无向图的邻接矩阵一定是对称矩阵,而有向图的邻接矩阵则不一定对称。下面
给出邻接矩阵的存储结构定义: 
#define MaxSize 10 /*假设图中最多顶点个数*/ 
typedef char DataType; /*图中顶点的数据类型,假设为char 型*/ 
typedef struct /*定义邻接矩阵存储结构*/ 
课件5-3

1 54 数据结构———从概念到C 实现(第2 版) 
{ 
DataType vertex[MaxSize]; /*存储顶点的一维数组*/ 
int edge[MaxSize][MaxSize]; /*存储边的二维数组*/ 
int vertexNum, edgeNum; /*图的顶点数和边数*/ 
} MGraph; 
2.邻接矩阵的实现
在图的邻接矩阵存储中容易实现下述基本操作。
(1)对于无向图,顶点i 的度等于邻接矩阵中第i 行(或第i 列)非零元素的个数。对
于有向图,顶点i 的出度等于邻接矩阵中第i 行非零元素的个数;顶点i 的入度等于邻接
矩阵中第i 列非零元素的个数。
(2)判断顶点i 和j 之间是否存在边,只需测试邻接矩阵中相应位置的元素edge[i] 
[j],若其值为1,则有边;否则,顶点i 和j 之间不存在边。
(3)找顶点i 的所有邻接点,可依次判别顶点i 与其他顶点之间是否有边(无向图) 
或顶点i 到其他顶点是否有弧(有向图)。
下面讨论邻接矩阵存储的其他基本操作。
(1)图的建立。建立一个含有n 个顶点e 条边的图,假设建立无向图,算法用伪代码
描述如下: 
算法:CreatGraph 
输入:顶点的数据信息a[n],顶点个数n,边的个数e 
输出:图的邻接矩阵
1.存储图的顶点个数和边的个数; 
2.将顶点信息存储在一维数组vertex中; 
3.初始化邻接矩阵edge; 
4.依次输入每条边并存储在邻接矩阵edge中: 
4.1 输入边依附的两个顶点的编号i和j; 
4.2 将edge[i][j]和edge[j][i]的值置为1; 
下面给出建立一个无向图的邻接矩阵算法的C语言实现。 
void CreatGraph(MGraph *G,DataType a[],int n,int e) 
{ 
int i,j,k; 
G->vertexNum = n; G->edgeNum = e; 
for (i =0; i < G->vertexNum; i++) /*存储顶点信息*/ 
G->vertex[i]= a[i]; 
for (i =0; i < G->vertexNum; i++) /*初始化邻接矩阵*/ 
for (j =0; j < G->vertexNum; j++)