数据结构-图

     图的存储结构相对于线性表和树来说更为复杂,因为图中的顶点具有相对概念,没有固定的位置。那我们怎么存储图的数据结构呢?我们知道,图是由(V, E)来表示的,G=(V,E) ,其中 V表示图结构所有顶点的集合,顶点可以用不同的数字或者字母来表示。E是图结构中所有边的集合,每条边由所连接的两个顶点来表示。图结构中顶点集合V不能为空,必须包含一个顶点,而图结构边集合可以为空,表示没有边。

 例如:某数据结构的二元组形式表示为A=(D,R),D={1,2,3,4,5,6,7),R={r},r={<1,2>,<1,3>,<1,4>,<2,5>,<2,6>,<3,7>,<5,6>},这就是图形结构。

无向图

       如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。典型的无向图,如图二所示。由于无向图中的边没有方向性,这样我们在表示边的时候对两个顶点的顺序没有要求。例如顶点VI和顶点V5之间的边,可以表示为(V2, V6),也可以表示为(V6,V2)。

                                                

对于无向图,对应的顶点集合和边集合如下:

       V(G)= {V1,V2,V3,V4,V5,V6}

       E(G)= {(V1,V2),(V1,V3),(V2,V6),(V2,V5),(V2,V4),(V4,V3),(V3,V5),(V5,V6)}

有向图

      一个图结构中,边是有方向性的,那么这种图就称为有向图,如图三所示。由于图的边有方向性,我们在表示边的时候对两个顶点的顺序就有要求。我们采用尖括号表示有向边,例如<V2,V6>表示从顶点V2到顶点V6,而<V6,V2>表示顶点V6到顶点V2。

                                                      

 对于有向图,对应的顶点集合和边集合如下:

       V(G)= {V1,V2,V3,V4,V5,V6}

       E(G)= {<V2,V1>,<V3,V1>,<V4,V3>,<V4,V2>,<V3,V5>,<V5,V3>,<V2,V5>,<V6,V5>,<V2,V6>,<V6,V2>}

  注意:无向图也可以理解成一个特殊的有向图,就是边互相指向对方节点,A指向B,B又指向A。

为什么要使用图?

    如果你有一个编程问题可以通过顶点和边表示出来,那么你就可以将你的问题用图画出来,然后使用著名的图算法(比如广度优先搜索 或者 深度优先搜索)来找到解决方案。

例如,假设你有一系列任务需要完成,但是有的任务必须等待其他任务完成后才可以开始。你可以通过非循环有向图来建立模型:

                              

   每一个顶点代表一个任务。两个任务之间的边表示目的任务必须等到源任务完成后才可以开始。比如,在任务B和任务D都完成之前,任务C不可以开始。在任务A完成之前,任务A和D都不能开始。

    现在这个问题就通过图描述清楚了,你可以使用深度优先搜索算法来执行执行拓扑排序。这样就可以将所有的任务排入最优的执行顺序,保证等待任务完成的时间最小化。(这里可能的顺序之一是:A, B, D, E, C, F, G, H, I, J, K)

顶点和边

      理论上,图就是一堆顶点和边对象而已,描述有两种主要的方法:邻接列表和邻接矩阵。

     邻接列表:在邻接列表实现中,每一个顶点会存储一个从它这里开始的边的列表。比如,如果顶点A 有一条边到B、C和D,那么A的列表中会有3条边:

                    

      邻接列表只描述了指向外部的边。A 有一条边到B,但是B没有边到A,所以 A没有出现在B的邻接列表中。查找两个顶点之间的边或者权重会比较费时,因为遍历邻接列表直到找到为止。

     邻接矩阵:在邻接矩阵实现中,由行和列都表示顶点,由两个顶点所决定的矩阵对应元素表示这里两个顶点是否相连、如果相连这个值表示的是相连边的权重。例如,如果从顶点A到顶点B有一条权重为 5.6 的边,那么矩阵中第A行第B列的位置的元素值应该是5.6:

   往这个图中添加顶点的成本非常昂贵,因为新的矩阵结果必须重新按照新的行/列创建,然后将已有的数据复制到新的矩阵中。大多数时候,选择邻接列表是正确的

图的遍历

深度优先遍历(Depth First Search)

深度优先遍历类似于二叉树的先序遍历,是树的先序遍历的推广。

     DFS的实现方式相比于BFS,只是把queue换成了stack而已,stack具有后进先出LIFO(Last Input First Output)的特性,DFS的操作步骤如下: 
1、把起始点放入stack; 
2、重复下述3步骤,直到stack为空为止:

from:https://blog.csdn.net/qq_38442065/article/details/81634282

  • 从stack中访问栈顶的点;
  • 找出与此点邻接的且尚未遍历的点,进行标记,然后全部放入stack中;
  • 如果此点没有尚未遍历的邻接点,则将此点从stack中弹出。
  1. 从图中某个顶点v出发,访问v。
  2. 对于v的所有邻接点w1、w2、w3…,找到第一个未被访问的邻接点,访问该顶点。以该顶点为新顶点,重复此步骤,直至刚访问的顶点没有未被访问的邻接点为止。
  3. 返回前一个访问过得且扔有未被访问的邻接点的顶点,找到该顶点的下一个未被访问的邻接点,访问该顶点。

重复步骤2,3,直至图中所有顶点都被访问过。

由于没有规定访问邻接点的顺序,所以深度优先序列不唯一。

时间复杂度:若图为邻接表表示为O(n+e),若为邻接矩阵表示为O(n+n^2)=O(n^2)。

                       

广度优先遍历(Breadth First Search)

图的广度优先遍历就类似于树的层序遍历。

  1. 从图中某个顶点v出发,访问v。
  2. 依次访问顶点v所有未被访问的邻接点、w1、w2…wn,并用栈或队列存储。
  3. 依次取出邻接点进行广度优先遍历,分别从这些邻接点出发依次访问他们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。重复步骤3,直至图中所有已被访问的顶点的邻接点都被访问到。

from:https://www.jianshu.com/p/5c676d76f3a3

深度优先遍历是回溯算法,广度优先遍历时一种分层的顺序搜索过程,不是递归。

遍历的应用

    求一条从顶点v到顶点s的简单路径:从v开始深度优先遍历,直到找到s。在对v深度优先遍历的过程中,首先将v添加到路径中,判断v的值是否为s,如果为s,返回真。如果不为s,则对v的邻接点进行递归,直到找到s为止。如果没找到,则将v从路径中删除,返回失败。

广度优先搜索解决是否存在从A到B的路径的问题,如果有,广度优先搜索将找出最短路径。

(2)寻找最短路径的问题,可以建立图关系,利用广度优先搜索算法求解https://blog.csdn.net/qq_36571422/article/details/80357138

(3)广度优先搜索,利用队列的结构,先从开始节点的邻居开始遍历,先检索一个节点是否满足要求,若满足要求,则结束搜索,若不满足则将该节点弹出队列,将该节点的邻居加入队列,最终完成遍历或找到满足要求的节点。

 

最小生成树

    假设有一个很实际的问题:我们要在n个城市中建立一个通信网络,则连通这n个城市需要布置n-1一条通信线路,这个时候我们需要考虑如何在成本最低的情况下建立这个通信网? 

     n个城市就是图上的n个顶点,然后,边表示两个城市的通信线路,每条边上的权重就是我们搭建这条线路所需要的成本,所以现在我们有n个顶点的连通网可以建立不同的生成树,每一颗生成树都可以作为一个通信网,当我们构造这个连通网所花的成本最小时,搭建该连通网的生成树,就称为最小生成树。

      构造最小生成树有很多算法,但是他们都是利用了最小生成树的同一种性质:MST性质(假设N=(V,{E})是一个连通网,U是顶点集V的一个非空子集,如果(u,v)是一条具有最小权值的边,其中u属于U,v属于V-U,则必定存在一颗包含边(u,v)的最小生成树),下面就介绍两种使用MST性质生成最小生成树的算法:普里姆算法和克鲁斯卡尔算法
 

  • 连通图:在无向图中,若任意两个顶点vi与vj都有路径相通,则称该无向图为连通图。
  • 强连通图:在有向图中,若任意两个顶点vi与vj都有路径相通,则称该有向图为强连通图。
  • 连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
  • 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
  • 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。 

两种求最小生成树算法

1.Kruskal算法

此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。 
1. 把图中的所有边按代价从小到大排序; 
2. 把图中的n个顶点看成独立的n棵树组成的森林; 
3. 按权值从小到大选择边,所选的边连接的两个顶点ui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。 
4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。

2.Prim算法

     普里姆算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。

  1. 图的所有顶点集合为V;初始令集合u={s},v=V−u;
  2. 在两个集合u,v能够组成的边中,选择一条代价最小的边(u0,v0),加入到最小生成树中,并把v0并入到集合u中。
  3. 重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。

from:https://blog.csdn.net/a2392008643/article/details/81781766 

有向无环图及其应用

     对于有向图中没有限定次序关系的顶点,则可以人为加上任意的次序关系,由此所得顶点的线型序列被称之为拓扑有序序列

     AOV网(Activity On Vertex network)是以顶点表示活动,弧表示活动之间的先后关系的有向图。AOV网中不允许有回路,不允许某项活动以自己为先决条件。

    检测AOV网是否存在环:对有向图构造顶点的拓扑有序序列,若图中所有顶点都在该序列中,则AOV网必定不存在环

    AOE网(Activity On Edge)用边表示活动的网。它是一个带权的有向无环图。网中只有一个入度为零的点(称为源点)和一个出度为零的点(称为汇点)。AOE网中重要概念:

顶点表示时间,弧表示活动
权值:活动持续的时间
路径长度:路径上各活动的持续时间之和
关键活动:该弧上的权值增加将使有向图上的最长路径的长度增加

                                

      那么,显然对上图AOE网而言,所谓关键路径:开始-->发动机完成-->部件集中到位-->组装完成。路径长度为5.5。如果我们试图缩短整个工期,去改进轮子的生产效率,哪怕改动0.1也是无益的。只有缩短关键路径上的关键活动时间才可以减少整个工期的长度。

AOE网和AOV网的区别:

  1. 一个用顶点表示活动,一个用边表示活动
  2. AOV网侧重表示活动的前后次序,AOE网除了表示活动的前后次序,还表示了活动的持续时间等。

关键路径

    关键路径:从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。

     AOV网络:有向图,用顶点表示活动,用弧表示活动的先后顺序
    AOE网络:有向图,用顶点表示事件,用弧表示活动,用权值表示活动消耗时间

                         

活动:业务逻辑中的行为,用边表示
事件:活动的结果或者触发条件

活动的两个属性e(i)最早开始时间,l(i)最晚开始时间
事件的两个属性ve(j)最早开始时间,vl(j)最晚开始时间

计算关键路径的过程

  1. 先求出每个顶点的ve和vl值
  2. 通过这两个值就可以求出每条边的e和l值。
  3. 取e(i)=l(i)的边就是关键路径上的边,关键路径不止一条

from:https://www.jianshu.com/p/676a1fd28676

拓扑排序

     所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程。由于拓扑排序过程中,需要删除顶点,显然用邻接表更加方便。

    AOV网或AOE网构造拓扑序列的方法:

  • 在有向图中选一个没有前驱(入度为0)的顶点并输出它。
  • 从图中删除该顶点和所有以它为尾的弧。(弧头顶点的入度减一)
  • 重复上述两步,直至全部顶点均已输出。

   如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序,也就是很多题目的无解的情况。 

最短路径

1、 Floyd

Floyd算法求每对顶点间最短路径,弗洛伊德算法:从vi到vj所有可能存在的路径中,选出一条长度最短的路径。方法:

  • 初始设置一个n阶方阵,令其对角线元素为0,若存在弧<vi,vj>,则对应元素为权值,否则为无穷。
  • 逐步试着在原直接路径中增加中间顶点,若加入中间点后路径变短,则修改。否则,维持原值。
  • 所有顶点试探完毕,算法结束。
//核心算法类似于动态规划
for(i = 0; i < G.vertex; i++){
    for(j = 0; j < G.vertex; j++){
        for(k = 0; k < G.vertex; k++){
            if(A[i][j] > A[i][k] + A[k][j]){
                A[i][j] = A[i][k] + A[k][j];
                path[i][j] = k;//记录分段点
            }
        }
    }
}

2、 Dijkstra算法(迪杰斯特拉)

    典型最短路径算法,用于计算一个节点到其他节点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。

                          

Dijkstra算法可以计算任意节点其他节点的最短路径,算法思路:

  1. 指定一个节点,例如我们要计算 'A' 到其他节点的最短路径
  2. 引入两个集合(S、U),S集合包含已求出的最短路径的点(以及相应的最短长度),U集合包含未求出最短路径的点(以及A到该点的路径,注意 如上图所示,A->C由于没有直接相连,初始时为∞
  3. 初始化两个集合,S集合初始时,只有当前要计算的节点,A->A = 0,U集合初始时为 A->B = 4, A->C = ∞, A->D = 2, A->E = ∞接下来要进行核心两步骤了
  4. 从U集合中找出路径最短的点,加入S集合,例如 A->D = 2
  5. 更新U集合路径,if ( 'D 到 B,C,E 的距离' + 'AD 距离' < 'A 到 B,C,E 的距离' ) 则更新U
  6. 循环执行 4、5 两步骤,直至遍历结束,得到A 到其他节点的最短路径

from:https://www.jianshu.com/p/ff6db00ad866

from:https://blog.csdn.net/LJFYYJ/article/details/80293263

基于图的图像分割:https://blog.csdn.net/qq_30815237/article/details/94444955


有环和无环

    无向图的一个特性就是其中一旦两个节点a和b是相连的,这就意味着有路径从a到b,同时也有从b到a的。它具体对应的矩阵表达方式对应着一个对称矩阵,对于无向图来说,有环无环很好判断,我们要检测一个图中间是否存在环,肯定也需要通过某种遍历的方式,然后对访问过的元素做标记。如果再次碰到前面访问过的元素,则说明可能存在环。这里,如何来检测环和如果这个环存在的话,我们要返回这个环。在无向图的时候,这个方法确实是很简单可行,我们可以通过广度或者深度优先遍历来解决。

    和无向图比起来,有向图更加多了一种出入度的概念。因为方向的有向性,很多以前在无向图里看起来比较简单的问题在这里会变得更加有意思。因为每个节点和节点之间对应着一定的方向关系,所以这里用一个箭头来表示从一个节点到另外一个节点的有向关系。

我们来看下面的一个示例:

 

该有向图不是有环。

  从环的构成来说。如果我们按照深度优先的顺序访问到了一个环,必然是在逐步递归推进的过程中能访问到自己前面访问过的节点。这里的差别就在于递归推进所重复访问的节点和前面图深度遍历所访问的节点还又所差别。我们以下图来说明一下它们的详细差别:

                                       

      假定我们从节点1出发,先访问2这边的边,一直到节点6,这个时候按照深度优先遍历是首先一步步递归进去到节点6。因为节点6没有别的出边,所以就要一步步的按照前面的过程返回。在前面2,6节点都返回后,这个时候就算后面的节点比如5访问到6了,它们是不构成环的。

     这个时候的节点2和6,它们和节点3,4, 5之间的差别是,2和6已经不在函数递归的栈里了,因为它们已经从前面的递归里返回了,而3,4,5节点还是在里面。所以到后面遍历到节点7,8之后,我们再次碰到了节点4,就可以确认它们是构成了一个环。如下图:

                                             

      所以,这里问题的关键点就是,我们再次碰到的节点4它还没有从前面向前递归的函数返回回来,结果又被遍历的时候给碰上了。这样,按照前面的分析,我们的环检测要点就是,找到一个还在遍历中的节点,同时在遍历的时候它如果再次被访问到了,则表示找到了环。而如果它被访问完了之后返回,则再次碰到它的时候就不是环了。

from:https://blog.csdn.net/KID_LWC/article/details/82391702

判定一个有向图上是否存在回路除了可以利用拓扑排序方法外,还可以用: C 

A、求关键路径的方法   (最长路径)

B、广度优先遍历算法    (最短路径)

C、深度优先遍历算法

D、求最短路径的Dijkstra方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值