610 likes | 679 Views
第 10 章 图. 本章前面介绍了线性表、树,本章将介绍另一种重要的数据结构 -- 图结构。图结构与表结构和树结构的不同表现在结点之间的关系上,线性表中结点之间的关系是一对一的,除头结点和尾结点外,每个结点仅有一个前驱和一个后继;树是按分层关系组织的结构,树结构中结点之间关系是一对多,一个双亲可以有多个孩子,每个孩子结点仅有一个双亲;而图结构中,顶点之间的关联是任意的,图中每个顶点都可以和其它顶点相关联,即数据元素之间存在多对多的关系。相应地,图的存储及其运算也较为复杂。本章将介绍图的概念,如何在计算机上表示和处理图,以及如何利用图来解决一些实际问题。. 图的应用举例
E N D
本章前面介绍了线性表、树,本章将介绍另一种重要的数据结构--图结构。图结构与表结构和树结构的不同表现在结点之间的关系上,线性表中结点之间的关系是一对一的,除头结点和尾结点外,每个结点仅有一个前驱和一个后继;树是按分层关系组织的结构,树结构中结点之间关系是一对多,一个双亲可以有多个孩子,每个孩子结点仅有一个双亲;而图结构中,顶点之间的关联是任意的,图中每个顶点都可以和其它顶点相关联,即数据元素之间存在多对多的关系。相应地,图的存储及其运算也较为复杂。本章将介绍图的概念,如何在计算机上表示和处理图,以及如何利用图来解决一些实际问题。本章前面介绍了线性表、树,本章将介绍另一种重要的数据结构--图结构。图结构与表结构和树结构的不同表现在结点之间的关系上,线性表中结点之间的关系是一对一的,除头结点和尾结点外,每个结点仅有一个前驱和一个后继;树是按分层关系组织的结构,树结构中结点之间关系是一对多,一个双亲可以有多个孩子,每个孩子结点仅有一个双亲;而图结构中,顶点之间的关联是任意的,图中每个顶点都可以和其它顶点相关联,即数据元素之间存在多对多的关系。相应地,图的存储及其运算也较为复杂。本章将介绍图的概念,如何在计算机上表示和处理图,以及如何利用图来解决一些实际问题。
图的应用举例 例1、交通图(公路、铁路) 顶点:地点 边:连接地点的路 例2、电路图 顶点:元件 边:连接元件之间的线路 例3、通讯线路图 顶点:地点 边:地点间的连线 例4、各种流程图 如产品的生产流程图。 顶点:工序 边:各道工序之间的顺序关系
图的基本概念 • 图的基本概念 • 图的遍历 • 图的连通性与最小生成树
图的基本概念 • 图(Graph)——图G是由两个集合V(G)和E(G)组成的,记为G=(V,E) 其中:V(G)是顶点的非空有限集 E(G)是边的有限集合,边是顶点的无序对或有序对。 • 图的分类 • 有向图 • 无向图
图的基本概念 • 有向图——有向图G是由两个集合V(G)和E(G)组成的。 其中:V(G)是顶点的非空有限集。 E(G)是有向边(也称弧)的有限集合,弧是顶点的有序对,记为<v,w>,v,w是顶点,v为弧尾,w为弧头。
图的基本概念 • 例如: G1 = <V2,E2> V1 = { A, B, C, D, E } E1 = {<A,B>, <A,E>, <B,C>, <C,D>, <D,B>, <D,A>, <E,C> } A B E C D
图的基本概念 • 无向图——无向图G是由两个集合V(G)和E(G)组成的。 其中:V(G)是顶点的非空有限集。 E(G)是边的有限集合,边是顶点的无序对,记为(v,w) 或(w,v),并且(v,w)=(w,v)。
图的基本概念 • 例如: G2 = <V1,E1> V2 = { v0 ,v1,v2,v3,v4 } E2 = { (v0,v1), (v0,v3), (v1,v2), (v1,v4), (v2,v3), (v2,v4) } V1 V0 V2 V3 V4
图的基本概念 V0 V4 V3 V2 V0 V1 V1 V2 V3 1 邻接点及关联边 邻接点:边的两个顶点 关联边:若边e= (v, u), 则称顶点v、u 关连边e 2 顶点的度、入度、出度顶点V的度= 与V相关联的边的数目 在有向图中: 顶点V的出度= 以V为起点有向边数 顶点V的入度= 以V为终点有向边数 顶点V的度= V的出度+V的入度 设图G 的顶点数为 n,边数为 e 图的所有顶点的度数和 = 2*e (每条边对图的所有顶点的度数和“贡献”2度) e
图的基本概念 • 路径、回路 无向图 G =(V,E)中的顶点序列v1, v2, … ,vk,若 (vi,vi+1)E ( i=1, 2 ,… k-1), v=v1, u=vk,则称该序列是从顶点v到顶点u的路径;若v=u,则称该序列为回路。 有向图 D =(V,E)中的顶点序列 v1, v2, …, vk,若 <vi,vi+1>E (i=1,2,…k-1),v=v1,u=vk,则称该序列是从顶点v到顶点u的路径;若v=u,则称该序列为回路。
图的基本概念 V0 V4 V3 V2 V0 V1 V1 V2 V3 • 例如 在图G1中,V0, V1, V2, V3 是 V0 到 V3 的路径;V0, V1, V2, V3, V0 是回路。 在图G2中,V0, V2, V3 是 V0 到 V3 的路径; V0, V2, V3, V0 是回路。 无向图G1 有向图G2
图的基本概念 V4 V0 V0 V1 V3 V3 V4 V2 V0 V1 V2 V3 V0 V2 V3 V5 V2 V1 V1 • 连通图(强连通图) 在无(有)向图 G=< V, E > 中,若对任何两个顶点 v、u 都存在从 v 到 u 的路径,则称G是连通图(强连通图) 非连通图 连通图 非强连通图 强连通图
图的基本概念 V4 V3 V3 V4 V0 V0 V4 V3 V0 V2 V2 V2 V1 V1 V1 • 子图 设有两个图 G=(V,E),G1=(V1,E1),若V1 V,E1 E,E1关联的顶点都在 V1 中,则称 G1 是 G 的子图; • 例 (b)、(c)是 (a)的子图 (c) (a) (b)
图的基本概念 V0 V2 V3 V5 V4 V1 • 连通分量(强连通分量) 无向图G的极大连通子图称为G的连通分量。 极大连通子图意思是:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通。 连通分图 非连通图
图的基本概念 V0 V1 V2 V3 V0 V2 V3 V1 强连通分量 • 连通分量(强连通分量) 有向图D的极大强连通子图称为D的强连通分量。 极大强连通子图意思是:该子图是D强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通的。
图的基本概念 V0 V4 V3 V0 V4 V3 V2 V2 V1 V1 • 生成树 包含无向图 G 所有顶点的极小连通子图称为G生成树。 极小连通子图意思是:该子图是G的连通子图,在该子图中删除任何一条边,子图不再连通,若T是G的生成树当且仅当T满足如下条件: T是G的连通子图 T包含G的所有顶点 T中无回路 G1的生成树 连通图G1
图的存储结构 1 若(vi,vj)E 或 <vi,vj >E 0 否则 A[i][j]= V0 V1 V2 V3 V0 V4 V3 V2 • 0 1 0 1 0 • 0 1 0 1 • 0 1 0 1 1 • 0 1 0 0 • 0 1 1 0 0 • 0 1 1 0 • 0 0 0 0 • 0 0 0 1 • 0 0 0 V1 一、数组表示法(邻接矩阵表示) 邻接矩阵:G的邻接矩阵是满足如下条件的n阶矩阵: 在数组表示法中,用邻接 矩阵表示顶点间的关系
图的存储结构 4 4 0 V0 V4 V3 1 3 V0 V1 V2 V3 V4 0 1 2 3 4 m-1 0 V2 1 3 2 2 1 2 V1 二、邻接表 邻接表是图的链式存储结构 1、无向图的邻接表 顶点:通常按编号顺序将顶点数据存储在一维数组中; 关联同一顶点的边:用线性链表存储。 该结点表示边 (V2,V4),其中的4是V4在一 维数组中的位置
图的存储结构 • 无向图的邻接表的特点 1)在G邻接表中,同一条边对应两个结点; 2)顶点v的度:等于v对应线性链表的长度; 3)判定两顶点v ,u是否邻接:要看v对应线性链表中有无对应的结点。 4)在G中增减边:要在两个单链表插入、删除结点; 5)设存储顶点的一维数组大小为 m(m图的顶点数n),图的边数为e,G占用存储空间为:m+2*e。G占用存储空间与G的顶点数、边数均有关;适用于边稀疏的图。
图的存储结构 v0 v1 v2 vexdata firstarc v3 0 0 3 1 2 1 2 V0 V1 3 V2 V3 二、邻接表 2、有向图的邻接表 顶点:用一维数组存储(按编号顺序) 以同一顶点为起点的弧:用线性链表存储 adjvex next ^ ^ ^ ^ 类似于无向图的邻接表,所不同的是: 以同一顶点为起点的弧:用线性链表存储
图的存储结构 v0 v1 v2 vexdata firstarc v3 2 0 0 3 0 1 2 V0 V1 3 V2 V3 二、邻接表 3、有向图的逆邻接表 顶点:用一维数组存储(按编号顺序) 以同一顶点为终点的弧:用线性链表存储。 ^ ^ ^ ^ 类似于有向图的邻接表,所不同的是: 以同一顶点为终点弧:用线性链表存储
图的存储结构 弧结点: typedef struct ArcBox { int tailvex, headvex; //弧尾、弧头在表头数组中位置 struct arcnode *hlink;//指向弧头相同的下一条弧 struct arcnode *tlink; //指向弧尾相同的下一条弧 } ArcBox; tailvex headvex hlink tlink 顶点结点: typedef struct VexNode { VertexType data; //存与顶点有关信息 ArcBox *firstin;//指向以该顶点为弧头的第一个弧结点 ArcBox *firstout; //指向以该顶点为弧尾的第一个弧结点 } VexNode; VexNode OLGraph[M]; data firstin firstout 三、有向图的十字链表表示法
图的存储结构 例 1 1 2 1 3 a 2 b a b 3 c c d 3 1 3 4 4 d 4 3 4 1 4 2 三、有向图的十字链表表示法 相同弧尾 相同弧头 ^ ^ ^ ^ ^ ^ ^ ^
图的存储结构 边结点: typedef struct node { VisitIf mark; //标志域,记录是否已经搜索过 int ivex, jvex; //该边依附的两个顶点在表头数组中位置 struct EBox * ilink, * jlink; //分别指向依附于ivex和jvex的下一条边 }EBox; mark ivex ilink jvex jlink 顶点结点: typedef struct VexBox { VertexType data; //存与顶点有关的信息 EBox * firstedge; //指向第一条依附于该顶点的边 }VexBox; VexBox AMLGraph[M]; data firstedge 四、无向图的邻接多重表表示法
图的存储结构 1 3 5 3 2 1 4 3 4 5 2 1 2 a 例 b 2 a b 3 c c 4 d d e 5 e 四、无向图的邻接多重表表示法 ^ ^ ^ ^ ^
小结 本小节介绍了图的基本概念和图的存储结构。这些基本概念包括有向图、无向图、度、入度、出度、子图、路径、连通图与强连通图等。 图的存储结构至少要保存两类信息: 1)顶点的数据 2)顶点间的关系 常用的存储结构包括邻接矩阵、邻接表、邻接多重表和十字链表。 无向图的邻接矩阵是对称的,有向图的邻接矩阵可能是不对称的。 用邻接矩阵表示图,很容易判定任意两个顶点之间是否有边相连,并求得各个顶点的度数。无向图,顶点vi的度数是邻接矩阵中第i行或第i列值为1的元素个数, 有向图,邻接矩阵中第i行值为1的元素个数为顶点vi的出度,第i列值为1的元素的个数为顶点vi的入度, 用邻接矩阵表示法存储图,占用的存储单元只与图中顶点的个数有关,而与边的数目无关。 而用邻接表存储图,占用的存储单元为n+2e 在邻接多重表中, 所有依附于同一个顶点的边都链接在同一个单链表中。 从顶点 i 出发, 可以循链找到所有依附于该顶点的边,也可以找到它的所有邻接顶点。
图的遍历 V6 V7 V3 V4 V2 V1 V5 V8 V4 V1 V8 V7 V3 V5 V2 V6 • 连通图的深度遍历(DFS) 从图中某顶点v出发: 1)访问顶点v;2)对v的每个未被访问的邻接点wj,从wj出发,继续对图进行深度优先遍历。 例 深度遍历:V1,V2,V4,V5,V8,V3,V6,V7 深度遍历:V1,V3,V6,V7,V2,V5,V8,V4
图的遍历 • 图的深度遍历(DFS) V 从图解可见: 1. 从深度优先搜索遍历连通图的过程类似于树的先根遍历; w1 w3 w2 w2 SG3 SG1 SG2 2. 如何判别V的邻接点是否被访问? 解决的办法是:为每个顶点设立一个 “访问标志 visited[w]”。
图的遍历 Boolean visited[MAX]; // 访问标志数组 Status (*VisitFunc)(int v); // 访问函数 void DFS ( Graph G, int v) { // 从顶点v出发,深度优先搜索遍历连通图 G visited[v] = TRUE; VisitFunc(v); for ( w=FirstAdjVex(G, v); w!=0; w=NextAdjVex(G,v,w) ) if ( !visited[w] ) DFS(G, w); // 对v的尚未访问的邻接顶点w // 递归调用DFS } // DFS
图的遍历 • 非连通图的深度优先搜索遍历 首先将图中每个顶点的访问标志设为 FALSE,之后搜索图中每个顶点,如果未被访问,则以该顶点为起始点,进行深度优先搜索遍历,否则继续检查下一顶点。
图的遍历 void DFSTraverse ( Graph G, Status (*Visit) (int v)) { // 对图 G 作深度优先遍历。 VisitFunc = Visit; for ( v=0; v<G.vexnum; ++v ) visited[v] = FALSE; // 访问标志数组初始化 for ( v=0; v<G.vexnum; ++v ) if (!visited[v]) DFS(G, v); // 对尚未访问的顶点调用DFS }
图的遍历 例如: b a b a g g f c d e f c d e k h k h 0 1 2 3 4 5 6 7 8 访问标志: F F F F F F F F F T T T T T T T T T 访问次序: a c h d k f e b g
图的遍历 • 图的深度遍历(DFS) 开始 开始 访问V,置标志 标志数组初始化 求V邻接点 V=0 V访问过? 有邻接点w N N Y Y DFS(g,v) 结束 Y W访问过 V=V+1 N Y 求下一邻接点 V<VexNum DFS(G,w) N 结束
图的遍历 例 vexdata firstarc adjvex next V1 1 1 ^ 2 2 ^ V2 V3 3 3 ^ 4 4 ^ V4 V5 V6 V7 7 7 8 3 6 2 2 4 8 5 5 ^ 6 6 ^ V8 7 7 ^ 8 8 ^ • 图的深度遍历(DFS)——递归算法 深度遍历:V1 V3 V7 V6 V2 V4 V8 V5
图的遍历 • 图的广度遍历(BFS) 从图中的某个顶点V0出发,并在访问此顶点之后依次访问V0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和V0有路径相通的顶点都被访问到。 若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
图的遍历 V5 V6 V7 V8 V3 V4 V2 V1 V1 V8 V7 V6 V2 V4 V3 V5 • 图的广度遍历(BFS) 从图中某顶点v出发: 1) 访问顶点v 2) 访问v的所有未被访问的邻接点w1,w2,…wk 3) 依次从这些邻接点出发,访问它们的所有未被访问的邻接点,直到图中所有访问过的顶点的邻接点都被访问到。 V1,V2,V3,V4,V8,V5,V6,V7 V1,V3,V2,V6,V7,V4,V5,V8
图的遍历 void BFSTraverse ( Graph G, Status (*Visit) (int v) ) { for ( v=0; v<G.vexnum; ++v ) visited[v] = FALSE; // 初始化访问标志 InitQueue(Q); // 置空的辅助队列Q for ( v=0; v<G.vexnum; ++v ) if ( !visited[v]) { // v 尚未访问 … //next page } } // BFSTraverse
图的遍历 visited[v] = TRUE; Visit(v); // 访问v EnQueue(Q, v); // v入队列 while (!QueueEmpty(Q)) { DeQueue(Q, u); // 队头元素出队并置为u for ( w=FirstAdjVex(G, u); w!=0; w=NextAdjVex(G,u,w) ) if ( ! visited[w] ) { visited[w]=TRUE; Visit(w); EnQueue(Q, w); // 访问的顶点w入队列 } // if } // while
图的遍历 例 1 firstarc adjvex next vexdata ^ 1 1 2 3 4 2 2 ^ 3 3 ^ 5 4 4 ^ f f f 3 5 5 5 4 1 2 2 1 4 3 1 5 5 ^ 1 4 4 3 0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5 r r r 遍历序列:1 遍历序列:1 4 遍历序列:1 4 3 • 图的广度遍历(BFS)
图的遍历 例 1 firstarc adjvex next vexdata 4 3 2 3 2 ^ 1 1 2 3 4 2 2 ^ 3 3 ^ 5 遍历序列:1 4 3 2 遍历序列:1 4 3 2 4 4 ^ f f f 5 5 4 2 2 3 5 1 1 4 1 3 5 5 ^ 3 2 5 0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5 r r r 遍历序列:1 4 3 2 5 • 图的广度遍历(BFS)
图的遍历 例 1 firstarc adjvex next vexdata ^ 1 1 2 3 4 2 2 ^ 3 3 ^ 5 4 4 ^ f f f 2 3 4 1 1 5 2 4 3 5 1 5 5 5 ^ 0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5 5 2 5 r r r 遍历序列:1 4 3 2 5 遍历序列:1 4 3 2 5 遍历序列:1 4 3 2 5 • 图的广度遍历(BFS)
小结 有两种遍历方法(它们对无向图,有向图都适用) 深度优先遍历 从图中某顶点v出发: 1)访问顶点v; 2)依次从v的未被访问的邻接点出发,继续对图进行深度优先遍历; 广度优先遍历 图中某未访问过的顶点vi出发: 1)访问顶点vi; 2)访问 vi 的所有未被访问的邻接点w1 ,w2 , …wk ; 3)依次从这些邻接点出发,访问它们的所有未被访问的邻接点; 依此类推,直到图中所有访问过的顶点的邻接点都被访问; 由于没有规定访问邻接点的顺序,这两种遍历序列的结果都不是唯一的。
图的最小生成树 V0 V4 V3 V0 V4 V3 V2 V2 V1 V1 • 生成树 • 包含无向图 G 所有顶点的极小连通子图称为G生成树。 • 极小连通子图意思是:该子图是G的连通子图,在该子图中删除任何一条边,子图不再连通,若T是G的生成树当且仅当T满足如下条件: • T是G的连通子图 • T包含G的所有顶点 • T中无回路 G1的生成树 连通图G1
图的最小生成树 • 问题提出 要在n个城市间建立通信联络网, 顶点——表示城市 权——城市间建立通信线路所需花费代价 希望找到一棵生成树,它的每条边上的权值之和(即建立该通信网所需花费的总代价)最小——最小代价生成树 • 问题分析 n个城市间,最多可设置n(n-1)/2条线路。 n个城市间建立通信网,只需n-1条线路。 问题转化为:如何在可能的线路中选择 n-1 条,能把所有城市(顶点)均连起来,且总耗费(各边权值之和)最小。
图的最小生成树 • 普里姆算法的基本思想 取图中任意一个顶点 v 作为生成树的根,之后往生成树上添加新的顶点 w。 在添加的顶点 w 和已经在生成树上的顶点 v 之间必定存在一条边,并且该边的权值在所有连通顶点 v 和 w 之间的边中取值最小。 之后继续往生成树上添加顶点,直至生成树上含有 n-1 个顶点为止。
图的最小生成树 • 普里姆算法(Prim) 设 G=(V,GE)为一个具有 n 个顶点的连通网络,T=(U,TE)为构造的生成树。 (1)初始时,U = {u0},TE = ; (2)在所有 uU 、vV-U 的边(u,v)中选择一条权值最小的边,不妨设为(u,v); (3)(u,v) 加入TE,同时将 v 加入U; (4)重复(2)(3),直到 U=V 为止; u u v v V-U缩小 U扩大 U V-U
图的最小生成树 3 6 5 1 5 5 6 4 3 V4 V1 V6 V4 V3 V1 V3 V2 V3 V2 V3 V2 V2 V4 V2 V1 V4 V2 V1 V1 V4 V6 V3 V3 V5 V6 V5 V5 V5 V6 V6 V4 V1 V5 V6 V5 2 6 5 5 1 1 1 1 1 2 2 2 4 4 4 4 • 普里姆算法(Prim) U={ V1,V3,V6} U={ V1 } U={ V1,V3 } U={ V1,V3,V6,V4 } U={ V1,V3,V6,V4,V2 } U={ V1,V3,V6,V4,V2,V5 }
图的最小生成树 • 普里姆算法的基本思想 设置一个辅助数组,对当前 V-U 集中的每个顶点,记录和顶点集U中顶点相连接的代价最小的边: struct { VertexType adjvex; // U集中的顶点序号 VRType lowcost; // 边的权值 } closedge [ MAX_VERTEX_NUM ];
图的最小生成树 • 普里姆算法的基本思想 • 有关数据的存储结构 • 无向连通网络: MGraph G 选择权值最小的边: 设置一个一维数组closedge[ ],对 vV-U closedge[v].lowcost = min{ cost[u’,v]| u’U } //保存连接v到U中各顶点权最小边的权值 • closedge[v].adjvex = u • //用来记录该边在U中的顶点