图通常用来表示和存储具有“多对多”关系的数据,是数据结构中非常重要的一种结构。
6.1 图的定义与表示
图G可以理解为由集合 V 和 E 组成,记成G=(V, E);
V是节点的有限非空集合,E是节点的二元组集合,节点二元组称为边。V(G)和E(G)分别称为图G的节点集(顶点集)与边集,也可用G=(V,E)表示图。
图(graph),它表明了物件与物件之间的“多对多”的一种复杂关系。图包含了两个基本元素:顶点(vertex, 简称V)和边(edge,简称E)。
图中的圆圈叫作“顶点”(Vertex,也叫“结点”),连接顶点的线叫作“边”(Edge);由顶点和连接每对顶点的边所构成的图形就是图。
无向图
中描述两顶点(A 和 B)之间的关系可以用 (A,B)
来表示,而有向图
中描述从 V1 到 V2 的"单向"关系用<A,B>
来表示。
6.2 图的分类
- 有向图 VS. 无向图:边有无方向
- 完全图 VS. 非完全图:每个顶点和其他顶点是否有直接的路径(有向图中指的是有无双向的直接路径)相连。
- 带权图:某些实际场景中,
图中的每条边
(或弧)会赋予一个实数
来表示一定的含义,这种与边(或弧)相匹配的实数被称为"权"
,而带权的图通常称为网
。
- 连通图 vs. 非连通图
图中从一个顶点到达另一顶点,若存在至少一条路径,则称这两个顶点是连通着的;这样的图中没有独立的顶点或者子图;被称为连通图。否则被称为非连通图。
- 连通图 VS. 强连通图
连通图一定是无向图,而强连通图一定是有向图。
- 连通图是指在无向图中,任意两个顶点之间都存在路径相连,不存在孤立的顶点。由于无向图中的边没有方向性,所以连通图不涉及边的方向。
- 强连通图是指在有向图中,任意两个顶点之间都存在有向路径相连,不存在孤立的顶点。由于有向图中的边有方向性,所以强连通图要求路径是有向的。
也就是可以总结出:强连通图中每个顶点都必须至少有1个入度和1个出度;否则,肯定不会是强连通图。
6.3 图的表示(集合)
()
表示无向边;<>
表示有向边。
6.4 图的常用术语
- 邻接 若(V1,V2)是E(G)中的一条边,则称顶点V1和V2是相邻接(Adjacent)的顶点,称边(V1,V2)是依附于顶点V1和V2的边;
- 路径 顶点Vi到顶点Vj之间的连线称为路径(Path);
- 简单路径 在一条路径中,如果除了第一个顶点和最后一个顶点外,其余顶点各不相同,则称这样的路径为简单路径;
- 回路(环) 若一条路径的起点和终点相同(即Vi=Vj),则称此路径为回路或者环;
- 简单回路(简单环) 在一个图的序列中除过第一个顶点与最后一个顶点之外,其他顶点不重复出现的回路称为简单回路或者简单环;
- 顶点的度 顶点的度是指依附于某顶点Vi的边数,通常记为TD(Vi); 顶点的入度(InDegree)是指以Vi为终点的弧的而数目,记为ID(Vi); 顶点的出度(OutDegree)是指以Vi为始点的弧的数目,记为OD(Vi); 于是有:TD(Vi) = ID(Vi)+OD(Vi)
- 联通 若从顶点Vi到顶点Vj( i ≠ j )有路径,则Vi和Vj是联通的;
- 连通图 在无向图中,任意两个顶点Vi和Vj都是联通的则称这样的无向图为连通图;
- 强连通图 在有向图中,任意一对顶点Vi和Vj( i ≠ j )均有从Vi到Vj和从Vj到Vi的有向路径,则称为强连通图。
6.5 图的存储
常用的存储方式有两种:邻接矩阵和邻接表。
6.5.1 邻接矩阵
定义:设G=(V, E)是n个顶点的图,则G的邻接矩阵为下列n阶方阵:
无向图:
有向图:
带权图(网)的邻接矩阵:
总结:对于查找某一条边是否存在、权重多少非常快。但其比较浪费空间,对稠密图( E>>V )来说。
6.5.2 邻接表
图的邻接表存储结构是一种顺序分配和链式分配相结合的存储结构。它包括两个部分:一部分是数组,另一部分是链表,数组用来每条单链表的表头,有数组的特性可知定位每条单链表的时间都是O(1)。
也有的教材说:邻接表(Adjacency List)是图的一种链式存储结构。
邻接表存储图的核心思想是:将图中的所有顶点存储到顺序表中(也可以是链表),同时为各个顶点配备一个单链表,用来存储和当前顶点有直接关联的边或者弧(边的一端是该顶点或者弧的弧尾是该顶点)。
有向图和无向图的存储
用邻接表存储有向图(网),可以快速计算出某个顶点的出度,但计算入度的效率不高。反之,用逆邻接表存储有向图(网),可以快速计算出某个顶点的入度,但计算出度的效率不高。
那么有没有一种存储结构,可以快速计算出有向图(网)中某个顶点的入度和出度呢?答案是肯定的,十字链表就是这样的一种存储结构。
十字链表(Orthogonal List)是一种专门存储有向图(网)的结构,它的核心思想是:将图中的所有顶点存储到顺序表(也可以是链表)中,同时为每个顶点配备两个链表,一个链表记录以当前顶点为弧头的弧,另一个链表记录以当前顶点为弧尾的弧。
带权图的存储
6.6 图的遍历
图的遍历: 从图G中某一顶点v出发,顺序访问各顶点一次。 技巧: 为克服顶点的重复访问,设立辅助数组visited[n]。 visited[i]: 1:顶点i已被访问过 0:顶点i未被访问过
(连通图)遍历方法: 深度优先遍历法 - 类似于树的先序遍历 广度优先遍历法 - 类似于树的层次遍历
6.6.1 深度优先遍历法(DFS)
- 从图G(V, E)中任一顶点Vi开始,首先访问Vi,
- 然后访问Vi的任一未访问过的邻接点Vj,
- 再以Vj为新的出发点继续进行深度优先搜索(递归;需要在算法中使用到栈),直到所有顶点都被访问过。
为克服顶点的重复访问,设立一标志向量visited[n];
图的深度优先搜索遍历的复杂度:
邻接表 | 时间复杂度是O(n+e),其中n为图的顶点数,e为图的边数 |
邻接矩阵 | 时间复杂度是O(n2),其中,n为图的顶点数 |
6.6.2 广度优先遍历法(BFS)
- 从图G(V, E)中某一点Vi出发,
- 首先访问Vi的所有邻接点(w1, w2, …, wt),
- 然后再顺序访问w1,w2,…,wt的所有未被访问过的邻接点…,此过程直到所有顶点都被访问过。
为克服顶点的重复访问,设立一标志向量visited[n]; 顶点的处理次序–先进先出,故需用到一队列。
算法描述:
1.所有结点标记置为『未被访问』标志; 2.访问起始顶点,同时置起始顶点『已访问』标记; 3.将起始顶点进队列 4.当队列不为空时重复执行以下步骤:
- 取当前队头顶点
- 对与队头顶点相邻接的所有未被访问过的顶点依次做; (a)访问该顶点 (b)置该顶点为『已访问』标记,并将它进队列;
- 当前队头元素顶点出队
- 重复进行,直到队空时结束
6.7 生成树与最小生成树
生成树
含有该连通图的全部顶点的一个极小连通子图。
若连通图G的顶点个数为n,则G的生成树的边数为n-1。
G的子图G0边数大于n-1,则G0中一定有环路。
G的子图G0边数小于n-1,则G0中一定不连通。
最小生成树
给定一个带权图,构造带权图的一颗生成树,使得树中所有边的权值总和最小。
对于给定的连通网,求最小生成树常用的算法有两个,分别叫做:
- 普里姆(Prim)算法
- 克鲁斯卡尔(Kruskal)算法
Prim算法
适合于求边稠密的带权图的最小生成树。
假设G=(V,E)是一个无向带权图,生成的最小生成树为MinX=(V,T),其中V为顶点的集合,T为边的集合。
分析过程:
1.最小生成树MinX的顶点即V应该等于图G的顶点集V(G)。
2.再开始计算前要确定入口顶点,我们这里使用A作为入口。(实际上任意顶点均可)
3.定义两个空的集合U={};T={};它们分别存放最小生成树的顶点集和边集。再定义一个存放图中所有顶点的集合V = {A,B,C,D,E}
求解过程:
1.从V中取任一顶点假设是A出发;将A放入顶点集U;U={A}
2.选出顶点A到达集合B的所有边中权值最小的边【AB:5; AC:3; AD:8】;我们这里是(A,C)最小;将该边放入集合T;T={(A,C)}。同时将顶点C放入顶点集U;U={A,C}。去除V中的C;V={B,D,E}
若V≠∅则继续;否则,终止。
3.[重复1、2]找出集合U={A,C}到集合V={BDE}的全部被边【AB,AD,CB,CD】中权值最小的边(CB),将其放入集合T;T={(AC),(CB)}。同时将顶点B放入顶点集U;U={A,C,B}。去除V中的B;V={D,E}
4.[重复1、2]找出集合U={A,C,B}到 集合V={D,E}的全部变【AB,CD,BE】中权值最小的边(CD),将其放入集合T;T={(AC),(CB),(CD)}。同时将顶点D放入顶点集U;U={A,C,B,D}。去除V中的D;V={E}。
5.[重复1、2]找出集合U={A,C,B,D}到 集合V={E}的全部变【BE】中权值最小的边(BE),将其放入集合T;T={(AC),(CB),(CD),(BE)}。同时将顶点E放入顶点集U;U={A,C,B,D,E}。去除V中的D;V={}。此时V=∅;算法终止。
算法图解:
易错点:每次选最小权值时以某一个顶点为中心选取时不对的;而是应该以已经确认的所有结点为中心进行选取。
Kruskal克鲁斯卡尔算法
适合于求边稀疏的带权图的最小生成树。
克鲁斯卡尔算法查找最小生成树的方法是:将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。
基本思想: 1.设G=(V, E),令最小生成树初始状态为只有n个顶点而无边的非连通图T=(V, {}),每个顶点自成一个连通分量; 2.在E中选取权值最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则,舍去此边,选取下一条权值最小的边; 3.以此类推,重复2,直至T中所有顶点都在同一连通分量上为止。
原则: 按权值递增次序构造Tmin; 即每次选权最小且不构成回路的边,直至n-1条。
克鲁斯卡尔算法的难点:在于“如何判断一个新边是否会和已选择的边构成环路”
算法图解:
解决判定是否形成环路的办法:
上图【1】中将所有顶点添加标签(比如说0);
上图【2】中两个顶点的标签都是初始值0;则将已选择的边(B,C)的两个顶点标签修改为:1;
上图【3】中新选取的边(C,D)的两个顶点C标签是1,而D的标签是初始值0;他们不相同,则肯定不会形成回路;并将D的标签修改为1;
上图【4】中新选取的边(A,B)的两个顶点B标签是1,而A的标签是初始值0;他们不相同,则肯定不会形成回路;并将A的标签修改为1;
上图【5】中新选取的边(A,C)的两个顶点的标签相同都是1且不是默认值,则肯定会形成回路;放弃该边;顶点的标签保持不变;
上图【4】中新选取的边(B,E)的两个顶点B标签是1,而E的标签是初始值0;他们不相同,则肯定不会形成回路;并将E的标签修改为1;
若出现新选取的边的两个顶点的标签相同都是默认值;那么应该为这样的顶点分配新的标签(不能和之前分配的标签相同)。
最短路径的算法
最短路径不仅适用于无向加权图,也适用于有向加权图
Dijkstra算法(迪杰斯特拉算法)-要掌握
特点:较快,不允许负数权值。
迪杰斯特拉算法用于查找图中某个顶点到其它所有顶点的最短路径,该算法既适用于无向加权图,也适用于有向加权图。
Dijkstra松驰顺序,是依次松驰距离源点距离最短的未被处理过的点与之相连的顶点。是以一种贪心策略进行松驰的。这种特点导致,一旦某个顶点被处理过(即对与它相连的顶点进行松驰),那么后面该顶点自己被松驰,但是与它相连的顶点不能因为它的松驰而松驰,导致出现不准确的结果。(当边全为正,是不会出现这种情况,因为在松驰与该顶点相连的顶点时,这种算法已经保证了该点已经被松驰到极限)。
注意,使用迪杰斯特拉算法查找最短路径时,必须保证图中所有边的权值为非负数,否则查找过程很容易出错。
算法图解:
Floyd弗洛伊德算法(这可能是全网最能看懂的的Folyd算法图解了)-最好能懂
慢,允许负数权值。
在一个加权图中,如果想找到各个顶点之间的最短路径,可以考虑使用弗洛伊德算法。
弗洛伊德是另外一种求最短路径的方式,与迪杰斯特拉算法不同,弗洛伊德偏重于多源最短路径的求解,即能迪杰斯特拉能够求一个节点到其余所有节点的最短路径,但是弗洛伊德能够求出任意两个节点的最短路径,当然迪杰斯特拉重复N次也能达到目标。两种方式的时间复杂度均为O(n^3),但弗洛伊德形式上会更简易一些。
贝尔曼福特算法(Bellman-Ford)-了解
典型最短路径算法,用于计算一个节点到其他节点的最短路径。(Dijkstra算法也是);
再处理稀疏图时性能由于迪杰斯特拉算法;但是随着定点规模和稠密度增加性能弱于迪杰斯特拉算法。
基本原理:逐遍的对图中每一个边去迭代计算起始点到其余各点的最短路径,执行N-1遍(原因是避免负权环的情况下陷入死循环),最终得到起始点到其余各点的最短路径。(N为连通图结点数)
Bellman算法的核心就是松驰,没有贪心策略,也使它的时间复杂度比较高。因为它是单纯的松驰。首先我们要明白的是:如果处于第n层的节点,在它上一层的即n-1层所以节点的dist已经确定为最终真实值,那么通过一次遍历,第n层节点的dist也能被确定为最终真实值。第一次迭代,获得的信息是:与源点相邻点的真正dist(第二层节点),(其他点的可能仍为无穷大,或者为松驰一次状态);第二次循环,因为第二层的信息已经完全掌握,此次迭代是能确定第三层节点(从源点出发,经过2条边)的点的真实最短距离。(由于遍历的过程中,只完全掌握了第一层,其他节点的dist不能完全确定为最终的dist);如此循环,遍历n-1次,第n层的节点已经遍历完,至此,所有节点的dist都最终确定了。
对于贝尔曼福特和迪杰斯特拉算法的比较;这里有一份非常详尽的对比资料可以参考:
https://iopscience.iop.org/article/10.1088/1757-899X/917/1/012077/pdf
具体的算法图解见下图:
原文:https://www.geeksforgeeks.org/bellman-ford-algorithm-dp-23/
最短路径算法的比较:
比较项目 | floyd (弗洛伊德算法) | Dijkstra(迪杰斯特拉算法) | bellman-ford(贝尔曼夫德算法) | spfa |
空间复杂度 | O(N²) | O(M) | O(M) | O(M) |
时间复杂度 | O(N³) | O((m+n)logN) | O(MN) | 最坏也是O(NM) |
适用情况 | 稠密图和顶点关系密切 | 稠密图和顶点关系密切 | 稀疏图和边关系密切 | 稀疏图和边关系密切 |
负权 | 可以 | 不能 | 可以 | 可以 |
有负权边时可否处理 | 可以 | 不能 | 可以 | 可以 |
判断是否存在负权回路 | 不能 | 不能 | 可以 | 可以 |
其中N表示图中顶点数,M表示图中边数。
名词解释:
- 松弛操作:不断更新最短路径和前驱结点的操作。
- 负权回路:绕一圈绕回来发现到自己的距离从0变成了负数,到各结点的距离无限制的降低,停不下来。
6.8 拓扑排序
无环有向图称为无环图(DAG)
拓扑排序实质
上是对有向图的顶点排成一个线性序列。
什么叫拓扑排序?
定义:对AOV网构造顶点线性序列(…i,…,k,…j,…) i是j的前趋,则i在j之前,若i、k间无路径,则或i在k前,或k在i前都可以。这样的线性序列称为 拓扑有序序列 。
拓扑有序序列的构造过程称为 拓扑排序 。
为什么会有拓扑排序?
拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行。
为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。
AOV网中的弧表示活动之间存在的某种制约关系,AOV网不能存在回路;
规则:
- 图中每个顶点只出现
一次
。 - A在B前面,则不存在B在A前面的路径。(
不能成环!!!!
) - 顶点的顺序是保证所有指向它的下个节点在被指节点前面!(例如A—>B—>C那么A一定在B前面,B一定在C前面)。所以,这个核心规则下只要满足即可,所以拓扑排序序列不一定唯一!
对AOV网拓扑排序的步骤:
1,从AOV网中选择一个没有前驱的顶点(或者说没有入度的顶点),并输出它(顺序任意);
2,从网中删去该顶点,并删掉从该顶点出发的全部弧;
3,重复上述两步,直到剩余网中不存在没有前驱的顶点为止;
图解:
本人能力有限,文中内容难免有纰漏,真诚欢迎大家斧正~
喜欢本文的朋友请三连哦!!!
另外本文也参考了网络上其他优秀博主的观点和实例,这里虽不能一一列举但内心属实感谢无私分享知识的每一位原创作者。