大话数据结构读书笔记系列(七)图

妖精的绣舞 提交于 2019-11-26 11:58:43

7.1 开场白

旅游几乎是每个年轻人的爱好,但没有钱或没时间也是困惑年轻人不能圆梦的直接原因。如果可以用最少的资金和最少的时间周游中国甚至是世界一定是非常棒的。假设你已经有了一笔不算很丰裕的闲钱,也有了约半年的时间。此时打算全国性的旅游,你将会如何安排这次行程呢?

我们假设旅游就是逐个省市进行,省市内的风景区不去细分,例如北京玩7天,天津玩3天,四川玩20天这样子。你现在需要做的就是制订一个规划方案,如何才能用最少的成本将图7-1-1中的所有省市都玩遍,这里所谓最少的成本是指交通成本与时间成本。

如果你不善于规划,很有可能就会出现如玩好新疆后到海南,然后再冲向黑龙江这样的荒唐决策。但是即使是紧挨着省市游完的方案也会存在很复杂的选择问题,比如游完湖北,周边有安徽,江西,湖南,重庆,陕西,河南等省市,你下一步怎么走最划算呢?

你一时解答不了这些问题是很正常的,计算的工作本来就非人脑应该是电脑去做的事情。我们今天要开始学习最有意思的一种数据结构--图。在图的应用中,就有相应的算法来解决这样的问题。学完这一章,即便不能马上获得最终答案,你也大概知道应该如何去做了。

7.2 图的定义

在线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继。在树形结构中,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。这和一对父母可以有多个孩子,但每个孩子却只能有一对父母是一个道理。可现实中,人与人之间关系就非常复杂,比如我认识的朋友,可能他们之间也互相认识,这就不是简单的一对一、一对多,研究人际关系很自然会考虑多对多的情况。那就是我们今天要研究的主题--图。图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。

前面同学可能觉得树的术语好多,可来到了图,你就知道,什么才叫做真正的术语多。不过术语再多也是有规律可循的,让我们开始"图"世界的旅程。如图7-2-1所示,先来看定义。

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

对于图的定义,我们需要明确几个注意的地方。

  • 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)
  • 线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。那么对于图呢?我记得有一个笑话说一个小朋友拿着一张空白纸给别人却说这是他画的一幅"牛吃草"的画,"那草呢?","草被牛吃光了。","那牛呢?","牛吃完草就走了呀。"。之所以好笑是因为我们根本不认为一张空白纸算作画的。同样,在图结构中,不允许没有顶点在定义中,若V是顶点的集合,则强调了顶点集合V有穷非空
  • 线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。

7.2.1 各种图定义

无向边:若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi,vj)来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)。图7-2-2就是一个无向图,由于是无方向的,连接顶点A与D的边,可以表示成无序对(A,D),也可以写成(D,A)。

对于图7-2-2中的无向图G1来说,G1=(V1,{E1}),其中顶点集合V1={A,B,C,D};边集合E1={(A,B),(B,C),(C,D),(D,A),(A,C)}

有向边:若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧(Arc)。用有序偶<vi,vj>来表示,vi称为弧尾(Tail),vj称为弧头(Head)。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed grapha)。图7-2-3就是一个有向图。连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A,D>表示弧,注意不能写成<D,A>。

对于图7-2-3中的有向图G2来说,G2=(V2,{E2}),其中顶点集合V2={A,B,C,D};弧集合E2={<A,D>,<B,A>,<C,A>,<B,C>}。

看清楚了,无向边用小括号"()"表示,而有向边则是用尖括号"<>"表示。

在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。我们课程里要讨论的都是简单图。显然图7-2-4中的两个图就不属于我们要讨论的范围。

在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。比如图7-2-5就是无向完全图,因为每个顶点都要与除它以外的顶点连线,顶点A与BCD三个顶点连线,共有四个顶点,自然是4x3,但由于顶点A与顶点B连线后,计算B与A连线就是重复,因此要整体除以2,共有6条边。

在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有nx(n-1)条边,如图7-2-6所示。

从这里也可以得到结论,对于具有n个顶点和e条边数的图,无向图0<=e<=n(n-1)/2,有向图0<=e<=n(n-1)。

有很少条边或弧的图称为稀疏图,反之称为稠密图。这里稀疏和稠密是模糊的概念,都是相对而言的。比如我去上海世博会那天,参观的人数差不多50万人,我个人感觉人数实在是太多,可以用稠密来形容。可后来听说,世博园里人数最多的一天达到了103万人,啊,50万人是多么的稀疏呀。

有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。这些权可以表示从一个顶点到领一个订单的距离或耗费。这种带权的图通常称为网(Network)。图7-2-7就是一张带权的图,即标识中国四大城市的直线距离的网,此图中的权就是两地的距离。

假设有两个图G=(V,{E})和G'=(V,{E'}),如果则称G'为G的子图(Subgraph)。例如图7-2-8带底纹的图均为左侧无向图和有向图的子图。

7.2.2 图的顶点与边间关系

对于无向图G=(V,{E}),如果边(v,v)E,则称顶点v和v'互为邻接点(Adjacent),即v和v'相邻接。边(v,v')依附(incident)于顶点v和v',或者说(v,v')与顶点v和v'相关联。顶点v的度(Degree)是和v相关联的边的数目,记为TD(v)。

例如图7-2-8左侧上方的无向图,顶点A与B互为邻接点,边(A,B)依附于顶点A与B上,顶点A的度为3。而此图的边数是5,各个顶点度的和=2+3+2=10,推敲后发现,边数其实就是各顶点度数和的一半,多出的一半是因为重复两次计数。简记之,

对于有向图G=(V,{E}),如果弧<v,v'>E,则称顶点v邻接到顶点v’,顶点v'邻接自顶点v。弧<v,v'>和顶点v,v'相关联。以顶点v为头的弧的数目称为v的入度(InDegree),记为ID(v);以v为为的弧的数目称为v的出度(OutDegree),记为OD(v);顶点v的度为TD(v)=ID(v)+OD(v)。例如图7-2-8左侧下方的有向图,顶点A的入度是2(从B到A的弧,从C到A的弧),出度是1(从A到D的弧),所以顶点A的度为2+1=3。此有向图的弧有四条,而各顶点的出度和=1+2+1+0=4,各顶点的入度和=2+0+1+1=4。所以得到

无向图G=(V,{E})中从顶点v到顶点v'的路径(Path)是一个顶点序列(v=v(i,0),v(i,1),...,v(i,m)=v'),其中(v(i,j-1),v(i,j))E,1<=j<=m。例如图7-2-9中就列举了顶点B到顶点D的四种不同的路径。

如果G是有向图,则路径也是有向的,顶点序列应满足<V(i,j-1),V(i,j)>i<=j<=m。例如图7-2-10,顶点B到D有两种路径。而顶点A到B,就不存在路径。

树中根结点到任意结点的路径是唯一的,但是图中顶点与顶点之间的路径却是不唯一的。

路径的长度是路径上的边或弧的数目。图7-2-9中的左侧两条路径长度为2,右侧两条路径长度为3.图7-2-10左侧路径长为2,右侧路径长度为3。

第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle)。序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。图7-2-11中两个图的粗线都构成环,左侧的环因第一个顶点和最后一个顶点都是B,且C,D,A没有重复出现,因此是一个简单的环。而右侧的环,由于顶点C的重复,它就不是简单环了。

7.2.3 连通图相关术语

在无向图G中,如果从顶点v到顶点v'有路径,则称v和v’是连通的。如果对于图中任意两个顶点vi,vjE,vi和vj都是连通的,则称G是连通图(Connected Graph)。图7-2-12的图1,它的顶点A到顶点B、C、D都是连通的,但显然顶点A与顶点E或F就无路径,因此不能算是连通图。而图7-2-12的图2,顶点A、B、C、D相互都是连通的,所以它本身是连通图。

无向图中的极大连通子图称为连通分量。注意连通分量的概念,它强调:

  • 要是子图;
  • 子图要是连通的;
  • 连通子图含有极大顶点数;
  • 具有极大顶点数的连通子图包含依附于这些顶点的所有边。

图7-2-12的图1是一个无向非连通图。但是它有两个连通分量,即图2和图3.而图4,尽管是图1的子图,但是它却不满足连通子图的极大顶点数(图2满足)。因此它不是图1的无向图的连通分量。

在有向图G中,如果对于每一对vi、vjV,vi!=vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量

现在我们再来看连通图的生成树定义。

所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。比如图7-2-14的图1是一普通图,但显然它不是生成树,当去掉两条构成环的边后,比如图2或图3,就满足n个顶点n-1条边且连通的定义了。他们都是一棵生成树。从这里也可知道,如果一个图有n分顶点和小于n-1条边,则是非连通图,如果它多于n-1条边,必定构成一个环,因为这条边使得它依附的那两个顶点之间有了第二条路径。比如图2和图3,随便加哪两顶点的边都将构成环。不过有n-1条边并不一定是生成树,比如图4。

如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树。对有向树的理解比较容易,所谓入度为0其实就相当于树中的根结点,其余顶点入度为1就是说树的非根结点的双亲只有一个。一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。如图7-2-15的图1是一棵有向图。去掉一些弧后,它可以分解为两棵有向树,如图2和图3,这两棵就是图1有向图的生成森林。

7.2.4 图的定义与术语总结

术语终于介绍得差不多了,可能有不少同学有些头晕,我们再来整理一下。

按照有无方向分为无向图有向图。无向图由顶点构成,有向图由顶点构成。弧有弧尾弧头之分。

图按照边或弧的多少分稀疏图稠密图。如果任意两个顶点之间都存在边叫完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图

图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做,有向图顶点分为入度出度

图上的边或弧上带则称为

图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为,当中不重复叫做简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量

无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林

7.3 图的抽象数据类型

图作为一种数据结构,它的抽象数据类型带有自己特点,正因为它的复杂,运用广泛,使得不同的应用需要不要的运算集合,构成不同的抽象数据操作。我们这里就来看看图的基本操作。

7.4 图的存储结构

图的存储结构相较线性表与树来说就更加复杂了。首先,我们口头上说的"顶点的位置"或"邻接点的位置"只是一个相对的概念。其实从图的逻辑结构定义来看,图上任何一个顶点都可被看成是第一个顶点,任一顶点的邻接点之间也不存在次序关系。比如图7-4-1中的四张图,仔细观察发现,它们其实是同一个图,只不过顶点的位置不同,就造成了表象上不太一样的感觉。

也正由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。而多重链表的方式,即以一个数据域和多个指针域组成的结点表示图中的一个顶点,尽管可以实现图结构,但其实在树中,我们也已经讨论过,这是有问题的。如果各个顶点的度数相差很大,按度数最大的顶点设计结点结构会造成很多存储单元的浪费,而若按每个顶点自己的度数设计不同的顶点结构,又带来操作的不便。因此,对于图来说,如何对它实现物理存储是个难题,不过我们的前辈们已经解决了,现在我们来看前辈们提供的五种不同的存储结构。

7.4.1 邻接矩阵

考虑到图是由顶点和边或弧两部分组成。合在一起比较困难,那就很自然地考虑到分两个结构来分别存储。顶点不分大小、主次,所以用一个一维数组来存储是很不错的选择。而边或弧由于是顶点与顶点之间的关系,一维搞不定,那就考虑用一个二维数组来存储。于是我们的邻接矩阵的方案就诞生了。

图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息

设图G有n个顶点,则邻接矩阵是一个nxn的方阵,定义为:

我们来看一个实例,图7-4-2的左图就是一个无向图。

我们可以设置两个数组,顶点数组为vertex[4]={v0,v1,v2,v3},边数组arc[4][4]为图7-4-2右图这样的一个矩阵。简单解释一下,对于矩阵的主对角线的值,即arc[0][0]、arc[1][1]、arc[2][2]、arc[3][3],全为0是因为不存在顶点到自身的边,比如v0到v0。arc[0][1]=1是因为v0到v1的边存在,而arc[1][3]=0是因为v1到v3的边不存在。并且由于是无向图,v1到v3的边不存在,意味着v3到v1的边也不存在。所以无向图的边数组是一个对称矩阵。

嗯?对称矩阵是什么?忘记了不要紧,复习一下。所谓对称矩阵就是n阶矩阵的元满足a(ij)=a(ji),(0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的。

有了这个矩阵,我们就可以很容易地知道图中的信息。

  1. 我们要判定任意两顶点是否有边无边就非常容易了。
  2. 我们要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。比如顶点v1的度就是1+0+1+0=2。
  3. 求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点。

我们再来看一个有向图样例,如图7-4-3所示的左图。

顶点数组为vrtex[4]={v0,v1,v2,v3},弧数组arc[4][4]为图7-4-3右图这样的一个矩阵。主对角线上数值依然为0.单因为是有向图,所以此矩阵并不对称,比如有v1到v0有弧,得到arc[1][0]=1,而v0到v1没有弧,因此arc[0][1]=0。

有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之和。顶点v1的出度为2,即第v1行的各数之和。

与无向图同样的办法,判断顶点vi到vj是否存在弧,只需要查找矩阵中arc[i][j]是否为1即可。要求vi的所有邻接点就是将矩阵第i行元素扫描一遍,查找arc[i][j]为1的顶点。

在图的术语中,我们提到了网的概念,也就是每条边上带有权的图叫做网。那么这些权值就需要存下来,如何处理这个矩阵来适应这个需求呢?我们有办法。

设图G是网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:

这里W(ij)表示(Vi,Vj)或<Vi,Vj>上的权值。∞表示一个计算机允许的,大于所有边上权值的值,也就是一个不可能的极限值。有同学会问,为什么不是0呢?原因在于权值W(i,j)大多数情况下是正值,但个别时候可能就是0,甚至有可能是负值。因此必须要用一个不可能的值来代表不存在。如图7-4-4左图就是一个有向网图,右图就是它的邻接矩阵。

那么邻接矩阵是如何实现图的创建的呢?我们先来看看图的邻接矩阵存储的结构是,代码如下。

有了这个结构定义,我们构造一个图,其实就是给顶点表和边表输入数据的过程。我们来看看无向网图的创建代码。

7.4.2 邻接表

邻接矩阵是不错的一种图存储结构,但是我们也发现,对于边数相对顶点较少的图,这种结构是存在对存储空间的极大浪费的。比如说,如果我们要处理图7-4-5这样的稀疏有向图,邻接矩阵中除了arc[1][0]有权值外,没有其他弧,其实这些存储空间都浪费掉了。

因此我们考虑另外一种存储结构方式。回忆我们在线性表时谈到,顺序存储结构就存在预先分配内存可能造成存储空间浪费的问题,于是引出了链式存储的结构。同样的,我们也可以考虑对边或弧使用链式存储的方式来避免空间浪费的问题。

再回忆我们在树中谈存储结构时,讲到了一种孩子表示法,将结点存入数组,并对结点的孩子进行链式存储,不管有多少孩子,也不会存在空间浪费问题。这个思路同样适用于图的存储。我们把这种数组与链表相结合的存储方法称为邻接表(Adjacency List)。

邻接表的处理办法是这样。

  1. 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
  2. 图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。

例如图7-4-6所示的就是一个无向图的邻接表结构。

从图中我们知道,顶点表的各个结点由data和firstege两个域表示,data是数据域,存储顶点的信息,firstege是指针域。指向边表的第一个结点,即此顶点的第一个邻接点。边表结点有adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。比如v1顶点与v0、v2互为邻接点,则在v1的边表中,adjvex分别为v0的0和v2的2。

这样的结构,对于我们要获得图的相关信息也是很方便的。比如我们要想知道某个顶点的度,就去查找这个钉钉的边表中结点的个数。若要判断顶点vi到vj是否存在边,只需要测试顶点vi的边表中adjvex是否存在结点vj的下标j就行了。若求顶点的所有邻接点,其实就是对此顶点的边表进行遍历,得到的adjvex域对应的顶点就是邻接点。

若是有向图,邻接表结构是类似的,比如图7-4-7中第一幅图的邻接表就是第二幅图。但要注意的是有向图由于有方向,我们是以顶点为弧尾来存储边表的,这样很容易就可以得到每个顶点的出度。但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个链接为vi为弧头的表。如图7-4-7的第三幅图所示。

此时我们很统一就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。

对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可,如图7-4-8所示。

有了这些结构图,下面关于结点定义的代码就很好理解了。

对于邻接表的创建,也就是顺理成章之事。无向图的邻接表创建代码如下。

这里加粗代码,是应用了我们在单链表创建中讲解到的头插法,由于对于无向图,一条边对应都是两个顶点,所以在循环中,一次就针对i和j分别进行了插入。本算法的时间复杂度,对于n个顶点e条边来说,很容易得出是O(n+e)。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!