数据结构,图

使用场景

我们假设旅游就是逐个省市进行, 省市内的风景区不去细分, 例如北京玩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. 某个顶点到其他顶点是否有边,有值即为1,没有则为0。

  2. 得到某个顶点的维度。如v1的维度就为:1+0+1+0=2。

  3. 得到某个顶点所有的相邻顶点,做法就是将这个矩阵扫描一遍,值为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。


从邻接表存储结构中,我们可以得到什么?

  1. 某个顶点的度,可以通过查找这个顶点中边表中节点的个数。以上面无向图的邻接表存储结构为例: 

  2. 要判断定点vi到vj(注意,这里的i和j是代表第几个顶点)是否存在边,只需要测试顶点vi的边表中adjvex中是否存在节点vj的下标j就型了。

  3. 若求顶点的所有邻接点, 其实就是对此顶点的边表进行遍历, 得到的adjvex域对应的顶点就是邻接点。

有向图:

        有向图, 邻接表结构是和无序图是类似的。但要注意的是有向图由于有方向, 我们是以顶点为弧尾来存储边表的, 这样很容易就可以得到每个顶点的出度。

 

        但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表, 即对每个顶点vi都建立一个链接为vi为弧头的表。

 

此时我们很容易就可以算出某个顶点的入度或出度是多少, 判断两顶点是否存在弧也很容易实现。对于带权值的网图, 可以在边表结点定义中再增加一个weight的数据域, 存储权值信息即可,如下图所示 。

 

图的其它存储结构,后续补充。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
/* * 基于邻接边表实现顶点结构 */ package dsa; public class Vertex_List implements Vertex { //变量 protected Object info;//当前顶点中存放的数据元素 protected Position vPosInV;//当前顶点在所属的顶点表V中的位置 protected List outEdges;//关联边表:存放以当前顶点为尾的所有边(的位置) protected List inEdges;//关联边表:存放以当前顶点为头的所有边(的位置) protected int status;//(在遍历等操作过程中)顶点的状态 protected int dStamp;//时间标签:DFS过程中该顶点被发现时的时刻 protected int fStamp;//时间标签:DFS过程中该顶点被访问结束时的时刻 protected int distance;//到指定起点的距离:BFS、Dijkstra等算法所确定该顶点到起点的距离 protected Vertex bfsParent;//在最短距离树(BFS或BestFS)中的父亲 //构造方法:在G中引入一个属性为x的新顶点 public Vertex_List(Graph G, Object x) { info = x;//数据元素 vPosInV = G.insert(this);//当前顶点在所属的顶点表V中的位置 outEdges = new List_DLNode();//出边表 inEdges = new List_DLNode();//入边表 status = UNDISCOVERED; dStamp = fStamp = Integer.MAX_VALUE; distance = Integer.MAX_VALUE; bfsParent = null; } //返回当前顶点的信息 public Object getInfo() { return info; } //将当前顶点的信息更新为x,并返回原先的信息 public Object setInfo(Object x) { Object e = info; info = x; return e; } //返回当前顶点的出、入度 public int outDeg() { return outEdges.getSize(); } public int inDeg() { return inEdges.getSize(); } //返回当前顶点所有关联边、关联边位置的迭代器 public Iterator inEdges() { return inEdges.elements(); } public Iterator inEdgePositions() { return inEdges.positions(); } public Iterator outEdges() { return outEdges.elements(); } public Iterator outEdgePositions() { return outEdges.positions(); } //取当前顶点在所属的顶点集V中的位置 public Position getVPosInV() { return vPosInV; } //读取、设置顶点的状态(DFS + BFS) public int getStatus() { return status; } public int setStatus(int s) { int ss = status; status = s; return ss; } //读取、设置顶点的时间标签(DFS) public int getDStamp() { return dStamp; } public int setDStamp(int s) { int ss = dStamp; dStamp = s; return ss; } public int getFStamp() { return fStamp; } public int setFStamp(int s) { int ss = fStamp; fStamp = s; return ss; } //读取、设置顶点至起点的最短距离(BFS) public int getDistance() { return distance; } public int setDistance(int s) { int ss = distance; distance = s; return ss; } //读取、设置顶点在的DFS、BFS、BestFS或MST树中的父亲 public Vertex getBFSParent() { return bfsParent; } public Vertex setBFSParent(Vertex s) { Vertex ss = bfsParent; bfsParent = s; return ss; } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值