数据结构与算法之图的精简要点总结
图与树和并查集相近,主要解决的是网络问题,比如人际网络。
实现方式
图有两种实现方法包括邻接矩阵和邻接表。邻接矩阵(Adjacency Matrix)适合存储稠密图(Dense Graph),邻接表(Adjacency List)适合存储稀疏图 (Sparse Graph),稠密图的代表是完全图,即每个顶点都与其他任意顶点相连。
邻接矩阵则是使用 array[i][j]表示第i个节点是否指向第j个节点。邻接表则是使用将当前节点指向的节点则加入以当前元素作为表头的链表存储指向关系。
遍历方法
图的遍历方法跟树一样有两种,分别是深度优先搜索和广度优先搜索。深度优先搜索是遍历当前节点的全部相邻节点,与树不同只有一种遍历顺序就是存储顺序。而广度优先搜索与树一样,只不过入队的是全部相邻节点。其中深度优先搜索的每一次遍历路径便是一个连通路径,并且该搜索过程便可以获得当前节点到其他节点路径。值得一提的是无权图的最短路径可以通过广度优先遍历获取。
性能: 稀疏图(邻接表)的时间复杂度为O(V+E),稠密图(邻接矩阵)的时间复杂度为O(V2),其中V表示节点个数,E表示边的个数。
带权图
对于带权图相较上述的功能外,每个边都有了权值,对于邻接矩阵的直接将array[i][j]表示权值即可,而邻接表则不能直接指向邻近节点了,需要重新定义Edge类型包含指向和权值,在链表中加入全部的Edge。
最小生成树
该树所有边之和最小则成为最小生成树。现阶段的算法基于切分定理,
如果一个边的两个端点,属于切分(Cut)不同的两边,这个边称为横切边(Crossing Edge).
切分定理(cut property):给定任意切分,横切边中全值最小的边必然属于最小生成树。
经典的方法为lazy prim, prim 和 kruskal。
Lazy Prim和Prim 的原理一致: 每次均选择当前部分与未搜索部分连接的全部横切边中的最短边,但是实现上有所不同。
Lazy Prim实现
- 将该节点的全部邻接横切边均插入最小堆中
- 取出堆顶节点,判断是否为横切边,如果是的话保留存储至数组中,并遍历当前节点指向节点,执行步骤一。
Prim实现
- 将该节点的全部邻接横切边均用于修改最小索引堆中的元素数组,以保证当前元素数组中存储该元素对应链接到节点的最短横切边权值
- 取出堆顶节点,判断是否为横切边,如果是的话保留存储至数组中,并遍历当前节点指向节点,执行步骤一。
Kruskal实现
- 将所有的横切边插入最小堆,初始化并查集
- 每次取出堆顶元素,并且查看堆顶元素连接的两个节点是否在一个集合,如果不在一个集合则不构成环,将该路径记录,否则不予记录;直到路径中边的个数为节点个数-1。
性能对比
时间复杂度 | |
---|---|
Lazy Prim | \(O(E \log E)\) |
Prim | \(O(E \log V)\) |
Kruskal | \(O(E \log E)\) |
虽然算法复杂度如此,但是由于每次都是选择一个边,当有相同权值的边时,便会有多个解。
最短路径树
最短路径树(单源最短路径)指的是该图中所有节点距离其实节点路径最小。而松弛操作是求最短路径树的核心。
Dijkstra 单源最短路径算法
注意:该算法不允许图中有负权边,时间复杂度为:\(O(E \log V)\)。
- 将所有的边插入最小索引堆中
- 每次从最小索引堆中取出堆顶元素(权值最小边),通过该路径进行下一步的搜索和松弛操作,直到堆中元素为空。
Bellman-Ford单源最短路径算法
注意:有负权环的图中没有最短路径,时间复杂度为:\(O(VE)\)
如果一个图没有负权环,从一点到另外一点的最短路径,最多经过所有的V个顶点,有V-1条边;否则,存在顶点经过了两次,即存在负权环。对一个点的一次松弛操作,就是找到经过这个点的另外一条路径,多一条边,权值更小。如果一个图没有负权环,从一点到另外一点的最短路径,最多经过所有的V个顶点,有V-1条边,对所有的点进行V-1次松弛操作。对所有的点进行V-1次松弛操作,理论上就找到了从源点到其他所有点的最短路径。如果还可以继续松弛,所说原图中有负权环。
该算法通过判断通过该节点之后到其所有相邻节点的路径是否会更短(松弛操作)来实现的,并且在执行完成之后再对所有节点进行一次松弛操作判断是否存在负权环。
性能对比
算法 | 要求 | 应用图类型 | 算法复杂度 |
---|---|---|---|
Dijkstra | 无负权边 | 有向无向图均可 | O( ElogV) |
Bellman-Ford | 无负权环 | 有向图 | O(VE) |
利用拓扑排序 | 有向无环图 | 有向图 DAG | O(V+E) |