使用场景
我们假设旅游就是逐个省市进行, 省市内的风景区不去细分, 例如北京玩7天, 天津玩3天, 四川玩20天这样子。 你现在需要做的就是制订一个规划方案, 如何才能用最少的成本将所有省市都玩遍, 这里所谓最少的成本是指交通成本与时间成本。如果你不善于规划, 很有可能就会出现如玩好新疆后到海南, 然后再冲向黑龙江这样的荒唐决策。 但是即使是紧挨着省市游玩的方案也会存在很复杂的选择问题, 比如游完湖北, 周边有安徽、 江西、 湖南、 重庆、 陕西、 河南等省市, 你下一步怎么走最划算呢?
利用图数据结构就能去解决这个问题。
相关概念
无序图、无向边
若顶点vi到vj之间的边没有方向, 则称这条边为无向边(Edge) , 用无序偶对(vi,vj)来表示。 如果图中任意两个顶点之间的边都是无向边, 则称该图为无向图(Undirected graphs) 。
有向图、有向边
若从顶点vi到vj的边有方向, 则称这条边为有向边, 也称为弧(Arc) 。 用有序偶<vi,vj>来表示, vi称为弧尾(Tail) , vj称为弧头(Head) 。 如果图中任意两个顶点之间的边都是有向边, 则称该图为有向图(Directed graphs) 。
有向边和无向边的表示方法
无向边用小括号“()”表示, 而有向边则是用尖括号“<>”表示。
度
对于无向图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)。边数其实就是各顶点度数和的一半, 多出的一半是因为重复两次记数。
强联通图、强连通分量
在有向图G中, 如果对于每一对vi、 vj∈ V、 vi≠vj, 从vi到vj和从vj到vi都存在路径(即:A可到D,D也可到A),), 则称G是强连通图。 有向图中的极大强连通子图称做有向图的强连通分量。
图的存储结构
邻接矩阵
此种方式是采用两个数组来表示图。为什么采用两个数组?由于图是由顶点的集合和边(或弧)的集合这两部分组成,一个一维数组用来存储顶点,一个二维数组来存储边的集合。其中这个二维数组就称为邻接矩阵。
假设图G有n个定点,则领接矩阵是一个n*n的方阵,定义为:
上面这个公式简单来理解就是,两个顶点之间有边存在,就记为1,没有边存在就记为0。
为什么是n*n的矩阵?
来看一个实例,下面的图是一个无向图:
这个矩阵对角线上的值都为0。
边数组或者4*4矩阵的由来:
顶点v0没有边到自身,所以(v0,v0)的值就是0;从顶点v0到顶点v1是有边的,所以(v0,v1)的值就是1;以此类推,即得到了一个4*4的矩阵,而其中顶点之间有边就是1,无边就是0,这也是上图矩阵(边数组)里的值,1和0的由来原因。
v1的度为2是怎样推算出来的?
实际上就是求的顶点v1到其他3个顶点的边的数量,这里根据上面这个矩阵得到v1的度计算过程为:1+0+1+0=2,这个v1的度为2就是这样推算出来的。
所以根据上面这个例子就可以得出无向图的边数组是一个n*n的对称矩阵。
所谓对称矩阵就是n阶矩阵的元满足aij=aji, (0≤i,j≤n) 。 即从矩阵的左上角到右下角的主对角线为轴, 右上角的元与左 下角相对应的元全都是相等的。
得到这个矩阵过后,可以很轻易得到的结论:
某个顶点到其他顶点是否有边,有值即为1,没有则为0。
得到某个顶点的维度。如v1的维度就为:1+0+1+0=2。
得到某个顶点所有的相邻顶点,做法就是将这个矩阵扫描一遍,值为1就是相邻顶点,为0则不是。
有向图的计算过程和无向图差不多,这里不再说明,只列出一个图供参考:
网如何在矩阵中体现?
所谓网就是每条边上带有权的图叫做网。如何理解权,打个比方,假如把重庆和成都两地作为两个顶点,这两地之间的距离就是边,而具体从成都到重庆或者是成都到重庆,距离的长度或者坐交通工具到达另一地的所付出的成本就叫做权。
通过上面的讲解我们知道在图数据结构中,顶点与顶点之间的边,是利用邻接矩阵来存储,那么权是如何在这个矩阵中体现呢?
假设图G是网图,则邻接矩阵是一个n×n的方阵, 定义为:这里wij表示(vi,vj) 或<vi,vj>上的权值。 ∞表示一个计算机允许的、 大于所有边上权值 的值, 也就是一个不可能的极限值。 有同学会问, 为什么不是0呢? 原因在于权值wij大多数情况下是正值, 但个别时候可能就是0, 甚至有可能是负值。 因此必须要用一个不可能的值来代表不存在。 如下图,左边图就是一个有向网图, 右图就是它的邻接矩阵。image-20220529100130030image-20220529100130030
邻接表
边数相对顶点较少的图如果采用邻接矩阵的存储结构,会造成存储空间的极大浪费。比如说下图这种稀疏有向图,邻接矩阵中除了arc[1][0](就是(v1,v0))有权值外,没有其他弧,其实这些存储空间都浪费掉了。针对这种情况,才诞生了 邻接表这种存储结构。
邻接表存储结构是这样的:
无向图:
图数据结构中的顶点采用一维数组存储(也可以采用链表来存储,不过数组在读取值方面更加方便,所以选择数组)。另外对于顶点数组中,每个数据元素还需要存储指向第一个相邻节点的指针,以便于查找该顶点的边信息。
图数据结构中每个顶点Vi(表示顶点v1到vi的每一个顶点,这里的i表示第几个顶点)的所有相邻顶点构成一个线性表,由于相邻顶点的个数不确定,所以用单链表存储,无向图称为顶点Vi的边表,有向图称为顶点vi作为弧尾的出边表。
下面是一个无向图的邻接表存储结构:
从上图中可以看出,顶点表的各个结点由data和firstedge两个域表示, data是数据域,存储顶点的信息, firstedge是指针域, 指向边表的第一个结点, 即此顶点的第一个相邻接点。 边表结点由adjvex和next两个域组成。 adjvex是邻接点域, 存储某顶点的相邻接点在顶点表中的下标, next则存储指向边表中下一个结点的指针。
看过上面这段文字描述可能还会有点蒙,没关系我们再详细进行说明。主要是上图中
指针指向的1、2、3等等这样的数据是如何得到的呢?
首先我们知道图的所有顶点存储在一个一维数组里面,那么就是上图中所有顶点在数组中的下标分别是:v1为0,v2为1,v3为2,v4为3。v0的相邻接v1在存储顶点的一维数组中的下标为1,而v1的下一个相邻接点v2在存储接点的一维数组中的下标为2,而v2的相邻接点v3在存储节点的一维数组中的下标为3,所以就得到了指针指向的数字分别为1、2、3。
从邻接表存储结构中,我们可以得到什么?
某个顶点的度,可以通过查找这个顶点中边表中节点的个数。以上面无向图的邻接表存储结构为例:
要判断定点vi到vj(注意,这里的i和j是代表第几个顶点)是否存在边,只需要测试顶点vi的边表中adjvex中是否存在节点vj的下标j就型了。
若求顶点的所有邻接点, 其实就是对此顶点的边表进行遍历, 得到的adjvex域对应的顶点就是邻接点。
有向图:
有向图, 邻接表结构是和无序图是类似的。但要注意的是有向图由于有方向, 我们是以顶点为弧尾来存储边表的, 这样很容易就可以得到每个顶点的出度。
但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表, 即对每个顶点vi都建立一个链接为vi为弧头的表。
此时我们很容易就可以算出某个顶点的入度或出度是多少, 判断两顶点是否存在弧也很容易实现。对于带权值的网图, 可以在边表结点定义中再增加一个weight的数据域, 存储权值信息即可,如下图所示 。
图的其它存储结构,后续补充。