概述
图是一种非常复杂的非线性结构,并且具有极强的表达能力,现实世界中的许多问题都可以抽象为图结构。本章是整个数据结构课程的难点和重点,本章知识点的组织结构如下图所示:
重点/难点/要点
本章的重点是:
- 图的基本术语;
- 图的各种存储表示:
- 图的两种遍历的思想及算法:
- 图的各种应用。
本章的难点是:
- 运用图的遍历算法解决图的其他相关问题;
- 最小生成树算法;
- 最短路径算法;
- 拓扑排序算法:
- 关键路径算法。
图学习要点:
对于本章的学习要抓住一条明线:图的逻辑结构→图的存储结构→图的应用举例。对于图的逻辑结构,要从图的定义出发,抓住要点,在与线性表的定义和树的定义比较的基础上,深刻理解图结构的逻辑特征,通过具体实例理解图的基本术语,在与树的遍历进行比较的基础上,从逻辑上掌握图的遍历操作,最后给出图的抽象数据类型定义。对于图的存储结构,以如何表示图中顶点之间的逻辑关系为出发点,掌握图的不同存储结构以及它们之间的关系,并学会在实际问题中修改存储结构。在理解图的存储结构和遍历操作的基础上,基于邻接矩阵和邻接表存储结构实现图的遍历操作。
图有很多重要应用,这些重要应用构成了本章的难点,对这些重要应用的学习,首先要把握其基本思想,其次,掌握算法的执行过程和顶层伪代码描述,再次,分析算法采用的存储结构和引入的辅助数据结构,最后才能掌握具体的算法。
知识点整理
- 图是由顶点的有穷非空集合和顶点之间边的集合组成。如果图的任意两个顶点之间的边都是无向边,则称该图为无向图,则称该图为无向图,否则称该图为有向图。
- 在无向图中,对于任意顶点 v i v_i vi和 v j v_j vj,,若存在边 ( v i , v j ) (v_i,v_j) (vi,vj),则称顶点 v i v_i vi和 v j v_j vj互为邻接点。在有向图中,对于任意顶点 v i v_i vi和 v j v_j vj,若存在弧 < v i , v j > <v_i,v_j> <vi,vj>,则称顶点 v j v_j vj是 v i v_i vi的邻接点。
- 含有n个顶点的无向完全图共有 n × ( n − 1 ) / 2 n\times(n-1)/2 n×(n−1)/2条边;含有n个顶点的有向完全图共有 n × ( n − 1 ) n\times(n-1) n×(n−1)条边。
- 在无向图中,顶点v的度是指依附于该顶点的边的个数;在有向图中,顶点v的入度是指以该顶点为弧头的弧的个数,顶点v的出度是指以该顶点为弧尾的弧的个数。
- 在图中,权通常是指对边赋予的有意义的数值量,边上带权的图称为网或网图。
- 在无向图 G = ( V , E ) G=(V,E) G=(V,E)中,顶点 v p v_p vp到 v q v_q vq之间的路径是一个顶点序列 v p = v i 0 , v i 1 , … , v i m v_p=v_{i0},v_{i1},…,v_{im} vp=vi0,vi1,…,vim其中, ( v i j − 1 , v i j ) ∈ E ( 1 ≤ j ≤ m ) (v_{ij-1},v_{ij})\in E(1≤j≤m) (vij−1,vij)∈E(1≤j≤m);如果 G G G是有向图,则 < v i j − 1 , v i j > ∈ E ( 1 ≤ j ≤ m ) <v_{ij-1},v_{ij}>\in E(1≤j≤m) <vij−1,vij>∈E(1≤j≤m)。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路。
- 在无向图中,若任意顶点 v i v_i vi和 v j ( i ≠ j ) v_j(i\neq j) vj(i=j)之间有路径,则称该图是连通图,非连通图的极大连通子图称为连通分量;在有向图中,对任意顶点 v i v_i vi和 v j ( i ≠ j ) v_j(i\neq j) vj(i=j),若从顶点 v i v_i vi到 v j v_j vj和从顶点 v j v_j vj到 v i v_i vi均有路径,则称该有向图是强连通图,非强连通图的极大强连通子图称为强连通分量。
- 连通图 G G G的生成树是包含 G G G中全部顶点的一个极小连通子图。图的生成树可以在遍历过程中得到。
- 图的遍历通常有深度优先遍历和广度优先遍历两种方式。图的深度优先遍历是以递归方式进行的,需用栈记载遍历路线:图的广度优先遍历是以层次方式进行的,需用队列保存已访问的顶点。
- 为了在图的遍历过程中区分顶点是否已被访问,设置一个访问标志数组visited[n],其初值为未被访问标志“0”,如果某个顶点已被访问,则将该顶点的访问标志置为“1”。
- 图的存储结构有邻接矩阵、邻接表、十字链表、邻接多重表等,前面两个需要重点掌握。
- 图的邻接矩阵存储用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(邻接矩阵):图的邻接表存储由边表和顶点表组成,图中每个顶点的所有邻接点构成一个边表,所有边表的头指针和存储顶点信息的一维数组构成顶点表。
- 最小生成树是无向连通网中代价最小的生成树。最小生成树具有MST性质,Prim算法和Kruskal算法是两个利用MST性质构造最小生成树的经典算法。Prim算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),适用于求稠密网的最小生成树;Kruskal算法的时间复杂度为 O ( e l o g 2 e ) O(elog_2e) O(elog2e),适用于求稀疏网的最小生成树。
- 在网图中,最短路径是指两顶点之间经历的边上权值之和最少的路径。Dijkstra 算法按路径长度递增的次序产生单源点最短路径,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。Floyd算法采用迭代的方式求得每一对顶点之间的最短路径,时间复杂度为 O ( n 3 ) O(n^3) O(n3)。
- AOV网是用顶点表示活动,用弧表示活动之间的优先关系的有向图,测试AOV网是否存在回路的方法,就是对AOV网进行拓扑排序。
- AOE网是用顶点表示事件,用有向边表示活动,边上的权值表示活动的持续时间的有向图,计算完成整个工程的最短工期,找出关键活动的方法是对AOE网求关键路径。
练习
图的逻辑结构
1.带权图指的是(B)。
A.顶点带权的无向图或有向图
B.边上带权的无向图或有向图
C.有回路的无向图或有向图
D.无回路的无向图或有向图
2.在图结构中,逻辑关系表现为邻接,相互邻接的顶点之间具有逻辑关系。(√)
3.最稀疏的图是(B),最稠密的图是(C)。
A.空图 B.零图 C.完全图 D.满图
4.在无向图中,路经可能不唯一;在有向图中,路经是唯一的。(×)
5.一个具有n个顶点的图,其子图可以有
2
n
2^n
2n个。(×)
6.若无向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)有两个连通分量
G
1
=
(
V
1
,
E
1
)
G_1=(V_1,E_1)
G1=(V1,E1)和
G
2
=
(
V
2
,
E
2
)
G_2=(V_2,E_2)
G2=(V2,E2),则有(A)。
A.
∣
V
1
∣
+
∣
V
2
∣
=
∣
V
∣
,
∣
E
1
∣
+
∣
E
2
∣
=
∣
E
∣
|V_1|+|V_2|=|V|,|E_1|+|E_2|=|E|
∣V1∣+∣V2∣=∣V∣,∣E1∣+∣E2∣=∣E∣
B.
∣
V
1
∣
+
∣
V
2
∣
<
∣
V
∣
,
∣
E
1
∣
+
∣
E
2
∣
<
∣
E
∣
|V_1|+|V_2|<|V|,|E_1|+|E_2|<|E|
∣V1∣+∣V2∣<∣V∣,∣E1∣+∣E2∣<∣E∣
C.
∣
V
1
∣
+
∣
V
2
∣
=
∣
V
∣
,
∣
E
1
∣
+
∣
E
2
∣
<
∣
E
∣
|V_1|+|V_2|=|V|,|E_1|+|E_2|<|E|
∣V1∣+∣V2∣=∣V∣,∣E1∣+∣E2∣<∣E∣
D.
∣
V
1
∣
+
∣
V
2
∣
<
∣
V
∣
,
∣
E
1
∣
+
∣
E
2
∣
=
∣
E
∣
|V_1|+|V_2|<|V|,|E_1|+|E_2|=|E|
∣V1∣+∣V2∣<∣V∣,∣E1∣+∣E2∣=∣E∣
7.若有向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)有两个连通分量
G
1
=
(
V
1
,
E
1
)
G_1=(V_1,E_1)
G1=(V1,E1)和
G
2
=
(
V
2
,
E
2
)
G_2=(V_2,E_2)
G2=(V2,E2),则有(C)。
A.
∣
V
1
∣
+
∣
V
2
∣
=
∣
V
∣
,
∣
E
1
∣
+
∣
E
2
∣
=
∣
E
∣
|V_1|+|V_2|=|V|,|E_1|+|E_2|=|E|
∣V1∣+∣V2∣=∣V∣,∣E1∣+∣E2∣=∣E∣
B.
∣
V
1
∣
+
∣
V
2
∣
<
=
∣
V
∣
,
∣
E
1
∣
+
∣
E
2
∣
<
=
∣
E
∣
|V_1|+|V_2|<=|V|,|E_1|+|E_2|<=|E|
∣V1∣+∣V2∣<=∣V∣,∣E1∣+∣E2∣<=∣E∣
C.
∣
V
1
∣
+
∣
V
2
∣
=
∣
V
∣
,
∣
E
1
∣
+
∣
E
2
∣
<
=
∣
E
∣
|V_1|+|V_2|=|V|,|E_1|+|E_2|<=|E|
∣V1∣+∣V2∣=∣V∣,∣E1∣+∣E2∣<=∣E∣
D.
∣
V
1
∣
+
∣
V
2
∣
<
=
∣
V
∣
,
∣
E
1
∣
+
∣
E
2
∣
=
∣
E
∣
|V_1|+|V_2|<=|V|,|E_1|+|E_2|=|E|
∣V1∣+∣V2∣<=∣V∣,∣E1∣+∣E2∣=∣E∣
对任意一个图,从某顶点出发进行一次深度优先或广度优先遍历,可访问图的所有顶点。(×)思路:考虑非连通图
对于下图所示无向图,回答下列问题:
(1)顶点
v
0
v_0
v0的度是(B),顶点
v
1
v_1
v1的度是(C)。
A.1 B.2 C.3 D.4
(2)顶点
v
0
v_0
v0的邻接点是(C)。
A.
v
1
v
2
v
3
v_1v_2v_3
v1v2v3 B.
v
1
v
2
v_1v_2
v1v2 C.
v
1
v
3
v_1v_3
v1v3 D.
v
2
v_2
v2
(3)顶点
v
0
v_0
v0到
v
1
v_1
v1的最短路径长度是(A)。
A.1 B.2 C.3 D.4
(4)从顶点
v
0
v_0
v0出发的深度优先遍历序列是(D),广度优先遍历序列是(D)
A.
v
0
v
2
v
3
v
1
v_0v_2v_3v_1
v0v2v3v1 B.
v
0
v
3
v
2
v
1
v_0v_3v_2v_1
v0v3v2v1 C.
v
0
v
2
v
1
v
3
v_0v_2v_1v_3
v0v2v1v3 D.
v
0
v
1
v
3
v
2
v_0v_1v_3v_2
v0v1v3v2
10.对于下图所示有向网图,回答下列问题:
(1)顶点
v
0
v_0
v0的入度是(B),出度是(C)。
A.0 B.1C.2 D.3
(2)顶点
v
0
v_0
v0的邻接点是(C)。
A.
v
1
v
2
v
3
v_1v_2v_3
v1v2v3 B.
v
1
v
2
v_1v_2
v1v2 C.
v
2
v
3
v_2v_3
v2v3 D.
v
1
v_1
v1
(3)顶点
v
0
v_0
v0到
v
3
v_3
v3的最短路径长度是(C)。
A.1 B.2 C.7 D.8
(4)从顶点
v
0
v_0
v0出发的深度优先遍历序列是(B),广度优先遍历序列是(B)。
A.
v
0
v
1
v
2
v
3
v_0v_1v_2v_3
v0v1v2v3 B.
v
0
v
2
v
3
v
1
v_0v_2v_3v_1
v0v2v3v1 C.
v
0
v
3
v
1
v
2
v_0v_3v_1v_2
v0v3v1v2 D.
v
0
v
1
v
3
v
2
v_0v_1v_3v_2
v0v1v3v2
图的邻接矩阵存储结构及实现
图的邻接矩阵采用数组方式进行存储,因此属于顺序存储结构。(×)
无向图的邻接矩阵一定是对称的,有向图的邻接矩阵一定是不对称的。(×)
用邻接矩阵存储图,所占用的存储空间大小只与图中顶点个数有关,与图的边数无关。(√)
图采用邻接矩阵存储,查找某顶点的所有邻接点,时间复杂度是(B)。
A.
O
(
1
)
O(1)
O(1) B.
O
(
n
)
O(n)
O(n) C.
O
(
n
+
e
)
O(n+e)
O(n+e) D.
O
(
n
2
)
O(n^2)
O(n2)
基于邻接矩阵存储的广度优先遍历图算法如下,请在横线处填写适当的语句或表达式。
void MGraph<DataType>::BFTraverse(int v)
{
int w,j,Q[Maxsize];
int front=-1,rear=-1;
cout<<vertex[v];
visited[v]=1;
Q[++rear]=v;
while(front!=rear)
{
w=Q[++front];
for(j=0;j<vertexNum;j++)
if(____①____&& visited[jl==0)
{
cout<<vertex[jl;
visited[j]=1;
____②____
}
}
}
①
edge[w][j]==1
②
Q[++rear]=j;
图的邻接表存储结构及实现
图采用邻接表存储,空间复杂度只与顶点个数有关,和边数无关。(×)
在图的邻接表存储中,存在两类结点:顶点表结点和边表结点。(√)
无向图有n个顶点e条边采用邻接表存储,查找某顶点的所有邻接点,平均情况下的时间复杂度是(C)。
A.
O
(
n
)
O(n)
O(n) B.
O
(
n
+
e
)
O(n+e)
O(n+e) C.
O
(
e
/
n
)
O(e/n)
O(e/n) D.
O
(
e
)
O(e)
O(e)
某个有向图采用邻接表存储,其存储结构是唯一的。(×)
基于邻接表存储的深度优先遍历图算法如下,请在横线处填写适当的语句或表达式。
void ALGraph<DataType>:: DFTraverse(int v)
{
int j;
EdgeNode *p=nullptr;
cout<<adjlist[v].vertex;
visited[v]=1;
____①____
while(p!=nullptr)
{
____②____
if(visited[j]==0)
DFTraverse(j);
____③____
}
}
①
p=adjlist[v].firstEdge;
②
j=p->adjvex;
③
p=p->next;
Prim算法
无向图的生成树是该图的一个极小连通子图。(√)
Prim算法采用(C)作为存储结构。
A.顺序存储 B.链接存储 C.邻接矩阵 D.邻接表
对于如下图所示无向连通图,从顶点d出发用Prim算法构造最小生成树,回答下列问题:
(1)最小生成树的代价是(B)。
A.15 B.17 C.19 D.20
(2)加入最小生成树的第4条边是(D)。
A.(f,e)2
B.(b,e)3
C.(f,b)3
D.(b,a)5
Prim算法如何存储候选最短边集?
答:设数组adjvex[n]和lowcost[n]分别存储候选最短边的邻接点和权值。
对于上图所示无向网图,从顶点d出发用Prim算法构造最小生成树,请填写下表。
Prim算法如下,请在横线处填写适当的语句或表达式。
void Prim(int v)
{
int i,j,k;
int adjvex[MaxSize],1owcost[MaxSize];
for(i=0;i< vertexlum;i++)//初始化辅助数组
{
____①____adjvex[il=v;
}
lowcost[v]=0;
for (k=1;k<vertexNum;i++)//迭代n-1次
{
j=MinEdge(lowcost,vertexNum)//寻找最短边的邻接点j
cout<<j<<adjvex[j]<<lowcost[j]<<endl;
lowcost[j]=0;
for(i=0;i<vertexNum;i++)//调整辅助数组
if(____②____)
{
lowcost[i]=edge[i][jl;
____③_____
}
}
}
①
lowcost[i]=edge[v][i];
②
edge[i]i]< lowcost[i]
③
adjvex[i]=j;
Kruskal算法
Kruskal算法采用(C)作为存储结构。
A.邻接矩阵 B.邻接表 C.边集数组 D.多重链表
并查集将集合中的元素组织成树的形式,并采用(A)存储。
A.双亲表示法 B.孩子表示法 C.二叉链表 D.列举法
有并查集{{a,b},{c,d,e},{f}},则双亲表示法的存储状态可能是(C)。
A.[-1,-1,2,2,2,3]
B.[-1,-1,2,2,2,5]
C.[-1,0,-1,2,3,-1]
D.[-1,0,-1,2,2,3]
对于如下图所示无向连通图,用Kruskal算法构造最小生成树,回答下列问题:
(1)加入最小生成树的第3条边是(D)。
A.(a,c)3
B.(b,e)3
C.(f,b)3
D.(d,f)4
(2)假设加入最小生成树的第2条边是(a,c)3,则当前的连通分量是(A)。
A.{a,c}{f,e}{d}{b}
B.{a,c}{f,e}{d,b}
C.{a,c,f,e}{d}{b}
D.{a,c,f,e}{d,b}
Kruskal 算法如下,请在横线处填写适当的语句或表达式。
void Kruskal()
{
int num=0,i,vex1,vex2;
int parent[vertexNum];//双亲表示法存储并查集
for(i=0;i<vertexNum;i++)
____①____//初始化n个连通分量
for(num=0,i=0; num<vertexNum;i++)
{
vex1=FindRoot(parent, edge[i]. from);
vex2=FindRoot(parent, edge[i]. to);
if(②)
{
cout<<edge[i].from<<","<<edge[i].to<< edge[i].weight;
parent[vex2]=vex1;
____③_____
}
}
}
①
parent[i]=-1;
②
vex1!=vex2
③
num++;
Dijkstra 算法
1.Dijkstra算法采用(C)作为存储结构。
A.边集数组
B.多重链表
C.邻接矩阵
D.邻接表
2.对于如下图所示有向图,用Dijkstra算法求最短路径,回答下列问题:
(1)从
v
0
v_0
v0到
v
2
v_2
v2的最短路径长度是(D)。
A.30 B.25 C.26 D.22
(2)求得的第3条最短路径是(B)。
A.
(
v
0
v
4
)
(v_0v_4)
(v0v4) 11
B.
(
v
0
v
6
v
3
)
(v_0v_6v_3)
(v0v6v3) 13
C.
(
v
0
v
4
v
3
)
(v_0v_4v_3)
(v0v4v3) 18
D.
(
v
0
v
6
v
3
v
5
)
(v_0v_6v_3v_5)
(v0v6v3v5) 16
Dijkstra算法如何保存迭代过程中当前的最短路径长度?
用数组dist[n]保存当前的最短路径长度。
对于上图所示有向图,用Dijkstra算法执行过程中数据结构的中间结果,请填写下表。
Dijkstra算法如下,请在每个横线处填写适当的语句或表达式。
void Dijkstra(int v)//从源点v出发
{
int i,k,num,dist[MaxSize];
for(i=0;i<vertexNum;i++)//初始化数组dist
____①____
for(num=1;num< vertexNum;num++)
{
k=Min(dist,vertexNum);
cout<<v<<"-->"<<k<<":"<<dist[k]);
for(i=0;i<vertexNum;i++)
if(_____②_____)
dist[i]=dist[k]+ edge[k][il;
dist[k]=0;//将顶点k加到集合S中
}
}
①
dist[i]=edge[v][i];
②
dist[i]>dist[k]+edge[k][i]
Floyd算法
1.Floyd算法采用(A)作为存储结构。
A.邻接矩阵 B.邻接表 C.边集数组 D.多重链表
2.Floyd算法的时间复杂度是(C)。
A.
O
(
n
)
O(n)
O(n) B.
O
(
n
2
)
O(n^2)
O(n2) C.
O
(
n
3
)
O(n^3)
O(n3) D.
O
(
n
4
)
O(n^4)
O(n4)
3.Floyd算法可以求任意两个顶点之间的最短路径。(√)
4.设有向图的邻接矩阵存储如下图左边所示,第2次迭代结果如下图右边所示,请在括号中填值。
①5
②9
③4
拓扑排序
1.在一个有向图的拓扑序列中,若顶点a在顶点b之前,则图中必有一条弧<a,b>。(×)
2.若一个有向图的邻接矩阵中对角线以下元素均为零,则该图的拓扑序列必定存在。(√)
3.拓扑排序算法可以用栈或者队列保存入度为0的顶点。(√)
4.在AOV网中不可能出现回路,因此一定存在拓扑序列。(×)
5.已知有向图带入度的邻接表存储如图6-12所示,画出拓扑排序算法在执行过程中,删除顶点
v
4
v_4
v4后邻接表的存储状态。
删除后:
拓扑排序算法如下,请在横线处填写适当的语句或表达式。
void TopSort()
{
int i,j,k;
int S[MaxSize],top=-1;
EdgeNode *p=nullptr;
for(i=0;i<vertexNum;i++)
if(____①____)
S[t+top]=i;//将入度为0的顶点压栈
while (top!=-1)
{
j=S[ top--];
cout <<adjlist[j].vertex;
_____②_____//工作指针p初始化
while(p!=nullptr)
{
k=p->adjvex;
____③____
if(adjlist[k].in==0)
{
S[++top]=k;
}
p=p->next;
}
}
}
①
adjlist[il.in=0
②
p=adjlist[i].firstEdge;
③
adjlist[k].in--;
关键路径
在AOE网中一定只有一条关键路径。(×)
加快关键活动的进度一定会缩短最短工期。(×)
关键活动的最早开始时间和最晚开始时间都不能推迟,否则会影响整个工期。(√)
求出所有事件的最早发生时间后,即可确定最短工期。(√)
如下图所示AOE网,回答下列问题:
(1)活动
a
2
a_2
a2的最早开始时间是(B),最晚开始时间是(B)。
A.3 B.4 C.5 D.6
(2)该AOE网的最短工期是(C)。
A.8 B.10 C.12 D.16
(3)该AOE网有(B)条关键路径。
A.1 B.2 C.3 D.4
(4)事件
v
2
v_2
v2的含义是什么?
活动
a
1
a_1
a1和
a
2
a_2
a2已经结束,活动
a
3
a_3
a3可以开始。
参考资料:《数据结构(从概念到C++实现)》清华大学出版社,王红梅