一、图的概念
1 .图的定义
图 1.3-1所示的 ⑴, ⑵, ⑶均为图 (Graph),它有若干个不同的点 v 1, v 2, …, v n,在其中一些点之间用直线或曲线连接。图中的这些点被称为顶点 (vertex)或结点,连接顶点的曲线或直线称为边 (edge)。通常将这种由若干个顶点以及连接某些顶点的边所组成的图形称为图,顶点通常被称作是图中的数据元素。
图1.3-1
在线性结构中每个元素只有一个前趋和一个后续,而图1.3-1中的各个图则与之不同,它是一种较为复杂的非线性数据结构,在图结构中的任意两个元素之间都可能相互联系,即每个元素都可能有多个前趋或多个后续。图作为一种数据结构,通常又可被定义为: graph=(V, E)或 G=(V,E),即一个图是由顶点的集合 V和边的集合 E组成。
在图 1.3-1中 ⑴图中的边没有方向,这类图称为无向图 (undirected graph)。在记录无向图时, (v 1, v 2 )等价于 (v 2, v 1)。
在图 1.3-1中 ⑵图中的边上有一个箭头,它表示边的方向,这类图称为有向图 (directed graph)。在记录有向图时, <v 1, v 2 >与 <v 2, v 1 >是两条不同的边。
图 1.3-1中 ⑴图的顶点集合为:
V ={ v 1, v 2, v 3, v 4}
边集合为:
E ={( v 1, v 2),( v 1, v 3),( v 2, v 3),( v 2, v 4),( v 3, v 4)}
图 1.3-1中 ⑵图的顶点集合为:
V ={ v 1, v 2, v 3, v 4}
边集合为:
E ={ <v 1, v 2 >, <v 1, v 3 >, <v 1, v 4 >, <v 2, v 1 >, <v 4, v 2 >}
2 .图的常用术语
环 (cycle):图 1.3-1中 ⑶图中的 v 1点本身也有边相连,这种边称为环。
有限图:顶点与边数均为有限的图,如图1.3-1中的三个图均属于有限图。
简单图:没有环且每两个顶点间最多只有一条边相连的图,如图 1.3-1中的 ⑴图。
邻接与关联:当( v 1, v 2) ∈E,或 <v 1, v 2 >∈E,即 v 1, v 2间有边相连时,则称 v1和 v 2是相邻的,它们互为邻接点( adjacent),同时称( v 1, v 2)或 <v 1, v 2 >是与顶点 v 1、 v 2相关联的边。
顶点的度数 (degree):从该顶点引出的边的条数,即与该顶点相关联的边的数目,简称度。图 1.3-1中 ⑴、 ⑵图的各顶点的度见下表:
入度( indegree):有向图中把以顶点 v为终点的边的条数称为是顶点 v的入度。
出度( outdegree):有向图中把以顶点 v为起点的边的条数称为是顶点 v的出度。图 1.3-1中、 ⑵图各顶点的入度和出度见下表(各顶点的入度与出度之和为该顶点的度 ):
终端顶点:有向图中把出度为 0的顶点称为终端顶点,如图 4.3-1中 ⑵图的 v 3。
道路与路长:道路也称路径 (path)。在图 G=( V, E)中,如果存在由不同的边 (v i0, v i1 ), (v i1, v i2 ), …, (v in-1, v in )或是 <v i0, v i1 >, <vi 1, v i 2>, …, < vin-1, v in >)组成的序列,则称顶点 v i0, v in是连通的,顶点序列( v i0, v i1, vi2, …, v in)是从顶点 v i0到顶点 v in的一条道路。路长是道路上边的数目, v i0到 v in的这条道路上的路长为n。
连通图:对于图中任意两个顶点 v i、 v j ∈V, v i、 v j之间有道路相连,则称该图为连通图 (connected graph),如 1.3-1中的 ⑴图。
强连通图(Strongly Connected Graph):是指一个有向图(DirectedGraph)中任意两点v1、v2间存在v1到v2的路径(path)及v2到v1的路径的图。
带权图:给图1.3-1中各图的边上附加一个代表性数据 (比如表示长度、流量或其他 ),则称其为带权图,如图 1.3-2。
网络:带权的连通图,如图 1.3-2所示。
图1.3-2
二、图的存储
图的最常见的存储方式是用邻接矩阵和邻接表。
无向图a 的邻接表b 和邻接矩阵c 表示如下所示:
有向图a 的邻接表b 和邻接矩阵c 表示如下所示:
1. 邻接矩阵存储
⑴ 邻接矩阵
邻接矩阵 (AdjacencyMatrix)是表示顶点间邻接关系的矩阵。在图的邻接矩阵表示法中通常用一个邻接矩阵表示顶点间的相邻关系,另外用一个顺序表来存储顶点信息。
具有 n个顶点的图 G=(V, E)的邻接矩阵可以定义为:
图 1.3-1中 ⑴图和 ⑵图的邻接矩阵表示为:
带权图的邻接矩阵可以定义为:
图 1.3-2邻接矩阵表示为:
⑵ 建立已知图的邻接矩阵
如要建立图 1.3-1中 ⑴、 ⑵的已知图,则可用常量数组直接说明如下:
const graph1:array[1..4,1..4] ofinteger=((0,1,1,0),(1,0,1,1),(1,1,0,1),(0,1,1,0));
graph2:array[1..4,1..4] ofinteger=((0,1,1,1),(1,0,0,0),(0,0,0,0),(0,1,0,0));
同样图 1.3-2中的图也可用常量数组直接说明如下:
const graph3:array[1..4,1..4] ofinteger=((0,3,4,0),(3,0,9,2),(4,9,0,6),(0,2,6,0));
⑶ 建立任意带权无向图的邻接矩阵
程序如下:
Program adjmatrix(input,output);
var vi,vj,vn,ei,en,wn:integer;
graph:array[1..20,1..20]of integer;
Begin
read(vn,en); { 读入顶点数和边数}
forvi:=1 to vn do
for vj:=1 to vn do
graph[vi,vj]:=0;
forei:=1 to en do
begin
read(vi,vj,wn); { 读入每条边的两个顶点及边上的权值 }
graph[vi,vj]:=wn;
graph[vj,vi]:=wn
end;
forvi:=1 to vn do
begin
for vj:=1 to vn dowrite(graph[vi,vj]:8); { 输出图 }
writeln
end
End.
2. 邻接表存储
⑴ 邻接表
邻接矩阵采用的是顺序存储的方式,而邻接表 (Adjacency List)采用的是图的一种链式存储方式。邻接表包含了一个顶点顺序表和每个顶点对应的单链表。
顶点顺序表中的顶点与图中的顶点一一对应,每个顶点的单链表中的每个顶点包含了邻接点域(用来指示与该顶点邻接的点在图中的位置),链域(用来指示相邻的另一条边的顶点),数据域(用来存储边的权值等信息)。
三、图的遍历
与树的遍历类似,从图中的某一顶点出发有序访问图中其余的所有顶点,并使每一个顶点恰好被访问一次,这个过程称为是图的遍历(traversinggraph)。在进行图的遍历时,由于图中顶点间是多对多的关系,图中的任一顶点都可能和其余的顶点相邻接。为避免重复访问,在遍历图的过程中,必须对访问过的顶点作上标记。如设置一个辅助的布尔型数组 visited[v1..vn],将该数组的初始值设为假,一旦顶点 vi被访问,便将其值visited[vi]设为真。
常用的图的遍历方法有两种:深度优先遍历和广度优先遍历,它们对有向图和无向图均适用。
1. 深度优先遍历
深度优先遍历 (Depth-firstTraversal)同树的先序遍历比较类似。先假设图中的所有顶点均未被访问,遍历时从图中的某个顶点 v0出发,访问该顶点,然后依次从v0的未被访问的邻接点出发深度优先遍历图,直到图中所有和v0有路径相通的顶点都被访问到。如果这时图中还有顶点未被访问,则另选图中一个未被访问的顶点作起始点,重复上述过程,直到图中所有顶点均被访问到为 止。
根据深度优先遍历的描述我们可以得到图 1.3-4中的 ⑴图遍历的顶点序列为V1→V2→V4→V8→V9→V5→V3→V6→V7, ⑵图遍历的顶点序列为V1→V2→V5→V3→V4→V6。
连通图的深度优先遍历的算法描述如下:(如果不是连通图则需多次使用遍历算法)
proceduredfs(vi:integer);
begin
write(graph[vi].v:4); {输出顶点数值}
visited[vi]:=true; { 设置已访问标记 }
last:=graph[vi].link; { 取边表指针}
while last<>nil do
begin
if not visited[last^.adjv] then dfs(last^.adjv);
last:=last^.next
end
end;
2. 广度优先遍历
广度优先遍历( Breadth-firstTraversal)也要先假设图中的所有顶点均未被访问,遍历时从某个顶点出发,访问该顶点后再依次访问与该顶点邻接的未被访问过的所有顶点。然后再分别从这些顶点出发进行广度优先遍历,直到图中所有未被访问过的顶点的相邻顶点均被访问到。如果这时图中仍有顶点为未被访问,则另选图中一个未被访问的顶点作为起点,再重复上述过程,直到图中所有顶点都被访问到为止。
根据广度优先遍历的描述我们可以得到图 1.3-4中的 ⑴图遍历的顶点序列为V1→V2→V3→V4→V5→V6→V7→V8→V9, ⑵图遍历的顶点序列为V1→V2→V3→V4→V5→V6。
在广度优先搜索过程中,设 Vi是将被访问的未访问过的顶点。它的邻接点记为 Vi1, Vi2, …, Vin。访问了Vi后,将对应的 visit[Vi]值设为真,并将 Vi保存在队列中。当 Vi出队时,搜索其邻接的 Vi1, Vi2, …,Vin顶点,然后将其中未被访问的顶点者作上访问标记后放入队列中。这种方法是将每个已被访问过的顶点入队,保证了每个顶点最多只有一次入队。
四、图的应用
1 .一笔画问题
数学家欧拉曾经解决过著名的七桥问题(七桥图见图 1.3-5 ⑴图)。下面写出七桥问题的描述:城市中有一条河,河中有 A、 D两个岛,河上有七座桥来连接两个岛及河的 B、 C两岸,问: ⑴能否刚好经过每座桥一次,既无重复也无遗漏? ⑵能否经过桥一次后又回到原来出发点上来?
图1.3-5
七桥问题可以画成图 1.3-5中的 ⑵图的形式,这样七桥问题的第一问就转化成了能否一笔画成一个图的问题。
一个图能否一笔画成需要满足以下条件:先根据图的邻接矩阵求出每个顶点的度数。如果没有度数为奇数的顶点,则可以从任一点开始一笔画成一个图。如果有两个度数为奇数的顶点,则可从这两个奇数顶点中的任一点开始一笔画成一个图。如果度数为奇数的顶点超过两个,则这个图不能够一笔画出。
图 1.3-6
对于图 1.3-5的 ⑵图或是 1.3-6所示的无向图,可以用数组 graph存储图的邻接矩阵,用数组 degree存储每个顶点的度数,用变量 Total_d存储总的度数,用变量 Odd_num存储度数为奇数的顶点个数,用变量 start存储一笔画的起始顶点。
一笔画程序如下:
programstroke(input,output);
vargraph:array[1..20,1..20] of 0..1;
degree:array[1..20] of integer;
odd_num,vn,vi,vj,start,total_d:integer;
begin
odd_num:=0;total_d:=0;start:=1;
write('please input the number of vertex:');
readln(vn);
writeln('please input the data:');
for vi:=1 to vn do
begin
degree[vi]:=0;
for vj:=1 to vn do
begin
read(graph[vi,vj]); {读入邻接矩阵}
degree[vi]:=degree[vi]+graph[vi,vj] {求每个顶点的度数}
end;
total_d:=total_d+degree[vi]; {求总的度数}
if odd(degree[vi]) then
begin
odd_num:=odd_num+1; {统计奇数顶点的个数}
start:=vi {确认从奇数顶点出发}
end
end;
if odd_num>2 then writeln('no solution') {奇数顶点超过两个显示无解}
else
begin
write('the road is: ',start);
vi:=0;
while total_d>2 do
begin
repeat vi:=vi+1 until graph[start,vi]<>0; {找连接的相邻点}
if degree[vi]>1 then {先画度数大于 1的顶点}
begin
write('->',vi);
graph[start,vi]:=0;
graph[vi,start]:=0;
degree[vi]:=degree[vi]-1;
degree[start]:=degree[start]-1;
total_d:=total_d-2;
start:=vi;
vi:=0
end
end;
repeat vi:=vi+1 until graph[start,vi]<>0; {确认最后一笔}
writeln('->',vi)
end
end.
输入图 1.3-6所示的无向图,程序运行结果如下:
please input the number of vertex:6
please input the data:
0 1 1 0 0 0
1 0 1 1 0 1
1 1 0 0 1 1
0 1 0 0 1 1
0 0 1 1 0 1
0 1 1 1 1 0
the road is:5->3->1->2->3->6->2->4->5->6->4
源文档 <http://www.ywhs.net/noi/ds/20081204/771.html>