1 图(Graph)
- 图由顶点(vertex)和边(edge)组成,通常表示为 G = (V, E)
- G表示一个图,V是顶点集,E是边集
- 顶点集V有穷且非空
- 任意两个顶点之间都可以用边来表示它们之间的关系,边集E可以是空的
2 有向图(Directed Graph)
- 有向图的边是有明确方向的
- 有向无环图(Directed Acyclic Graph,简称 DAG):如果一个有向图,从任意顶点出发无法经过若干条边回到该顶点,那么它就是一个有向无环图
- 出度、入度
- 出度、入度适用于有向图
- 出度(Out-degree)
- 一个顶点的出度为 x,是指有 x 条边以该顶点为起点。
- 顶点11的出度是3
- 入度(In-degree)
- 一个顶点的入度为 x,是指有 x 条边以该顶点为终点
- 顶点11的入度是2
3 无向图(Undirected Graph)
- 无向图的边是无方向的
- 效果类似于下面的有向图
4 混合图(Mixed Graph)
- 混合图的边可能是无向的,也可能是有向的
5 简单图、多重图
- 平行边
- 在无向图中,关联一对顶点的无向边如果多于1条,则称这些边为平行边
- 在有向图中,关联一对顶点的有向边如果多于1条,并且它们的的方向相同,则称这些边为平行边
- 多重图(Multigraph)
- 有平行边或者有自环的图
- 简单图(Simple Graph)
- 既没有平行边也不没有自环的图
- 课程中讨论的基本都是简单图
6 无向完全图(Undirected Complete Graph)
- 无向完全图的任意两个顶点之间都存在边
- n 个顶点的无向完全图有 n(n − 1)/2 条边
7 有向完全图(Directed Complete Graph)
- 有向完全图的任意两个顶点之间都存在方向相反的两条边
- n 个顶点的有向完全图有 n(n − 1) 条边
- 稠密图(Dense Graph):边数接近于或等于完全图
- 稀疏图(Sparse Graph):边数远远少于完全图
8 有权图(Weighted Graph)
- 有权图的边可以拥有权值(Weight)
9 连通图(Connected Graph)
- 如果顶点 x 和 y 之间存在可相互抵达的路径(直接或间接的路径),则称 x 和 y 是连通的
- 连通图是无向图的一种
- 如果无向图 G 中任意2个顶点都是连通的,则称G为连通图
- 连通分量(Connected Component)
- 连通分量指无向图的极大连通子图
- 连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量
- 下面的无向图有3个连通分量
10 强连通图(Strongly Connected Graph)
- 强连通图是有向图的一种
- 如果有向图 G 中任意2个顶点都是连通的,则称G为强连通图
- 强连通分量(Strongly Connected Component)指有向图的极大强连通子图
- 强连通图只有一个强连通分量,即其自身;非强连通的有向图有多个强连通分量
11 图的实现方案
11.1 邻接矩阵(Adjacency Matrix)
- 邻接矩阵的存储方式
- 一维数组存放顶点信息
- 二维数组存放边信息
- 邻接矩阵比较适合稠密图
- 不然会比较浪费内存
- 可以观察同一条边,例如v2到v1和v1到v2会存储两遍,同时不存在的边,例如v0到v0也会分配内存
- 邻接矩阵 – 有权图
11.2 邻接表(Adjacency List)
- 一维数组存放顶点信息
- 使用链表存储以该顶点为起点的边的信息,或以该顶点为终点的边的信息(逆邻接表)
- 邻接表 – 有权图
- 自己的实现类似邻接表,存放顶点、以该顶点为起点的边、以该顶点为终点的边
12 图的遍历
- 从图中某一顶点出发访问图中其余顶点,且每一个顶点仅被访问一次
12.1 广度优先搜索(Breadth First Search,BFS)
- 之前所学的二叉树层序遍历就是一种广度优先搜索,所以bfs方法和层序遍历一样,也可以使用队列实现
- 先找到距起点一步的所有节点,再找距离第一步节点一步的所有节点,直到所有节点遍历完毕
- 程序流程
- 将起点入队
- 将起点出队并访问,同时将以A为起点的所有边的终点入队
- 循环直到队列为空
- 期间注意,放入队列过(不管是否还在队列中)的顶点,就不要再次放入队列了
- 有向图使用广度优先搜索遍历时,要遵守方向
12.2 深度优先搜索(Depth First Search,DFS)
- 之前所学的二叉树前序遍历就是一种深度优先搜索,所以也可以通过递归或栈的方式实现
- 从起点开始,有多深走多深,到头之后往回退,看是否还有其他分支,有分支走分支,没分支继续回退
- 递归程序流程
- 先访问该顶点
- 将该顶点加入到访问过的顶点集合中
- 取出所有以该顶点为起点的边
- 如果这些边的终点已经访问过,那么跳过对该条边的处理
- 否则对该边的终点,使用第1、2、3步递归进行处理
- 使用栈程序流程
- 将起点入栈、访问、加入到已访问过的起点集合中
- 只要栈不为空,循环执行以下操作
- 出栈一个顶点,拿到该顶点为起点的所有边进行循环处理
- 如果该边终点已经访问过,跳过对该边的处理
- 否则将该边的起点入栈、终点入栈、访问终点,将终点加入到访问过的集合中
- 找到一条本次可以处理的边就退出当前循环,这样相当于可以对栈顶的那个更深处的顶点,继续往更深处处理
13 AOV网(Activity On Vertex Network)
- 一项大的工程常被分为多个小的子工程
- 子工程之间可能存在一定的先后顺序,即某些子工程必须在其他的一些子工程完成后才能开始
- 在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,子工程被称为活动(Activity)
- 以顶点表示活动、有向边表示活动之间的先后关系,这样的图简称为 AOV 网
- 标准的AOV网必须是一个有向无环图(Directed Acyclic Graph,简称 DAG)
- 前驱活动:有向边起点的活动称为终点的前驱活动。只有当一个活动的前驱全部都完成后,这个活动才能进行
- 后继活动:有向边终点的活动称为起点的后继活动
- 拓扑排序(Topological Sort):将 AOV 网中所有活动排成一个序列,使得每个活动的前驱活动都排在该活动的前面
- 比如上图的拓扑排序结果是:A、B、C、D、E、F 或者 A、B、D、C、E、F (结果并不一定是唯一的)
- 可以使用卡恩算法(Kahn于1962年提出)完成拓扑排序
- 假设 L 是存放拓扑排序结果的列表
- 把所有入度为 0 的顶点放入 L 中,然后把这些顶点从图中去掉
- 重复操作1,直到找不到入度为 0 的顶点
- 如果此时 L 中的元素个数和顶点总数相同,说明拓扑排序完成
- 如果此时 L 中的元素个数少于顶点总数,说明原图中存在环,无法进行拓扑排序
- 程序执行过程
- 为了不真正删除图中顶点,先建立一个顶点-入度对照表ins,存放入度不为0的所以顶点和入度对照关系
- 将初始入度为0的顶点,放入queue队列中
- 只要队列中元素不为空,执行如下循环
- 将队列中元素出队,并放入最终的拓扑排序结果list中
- 假想该顶点已经从整个图中去除,那么以改顶点为起点的所有边的终点的入度都应该减少1
- 如果减少1后,入度变为0,那么应该将该顶点放入queue队列中,保证循环继续
- 如果减少1后,入度仍不为0,那么应该更新顶点-入度对照表中的该顶点入度信息
14 生成树(Spanning Tree)
- 生成树(Spanning Tree):也称为支撑树(用最少的边,支撑了所有的顶点),是连通图的一种(所以是无向的)
- 极小连通子图:用最少的边构成的连通图
- 生成树是连通图的极小连通子图,如果连通图有n个顶点,那么生成树就恰好有n-1条边
- 最小生成树(Minimum Spanning Tree)
- 也称为最小权重生成树(Minimum Weight Spanning Tree)、最小支撑树
- 是所有生成树中,总权值最小的那棵
- 如果图的每一条边的权值都互不相同,那么最小生成树将只有一个,否则可能会有多个最小生成树
14.1 Prim(普里姆算法)
- 切分定理
- 给定任意切分,横切边中权值最小的边必然属于最小生成树
- 切分(Cut):把图中的节点分为两部分,称为一个切分
- 下图有个切分 C = (S, T),S = {A, B, D},T = {C, E}
- 横切边(Crossing Edge):如果一个边的两个顶点,分别属于切分的两部分,这个边称为横切边,比如上图的边 BC、BE、DE 就是横切边
- 由于横切边会将整个树分成两部分,也就是说,如果想让这两部分树连通,一定要选择其中一个横切边,对他们进行连接,横切边中权重最小的那条边,一定属于该图的最小生成树
- 以所有选好的边的所有顶点为起点的,不属于选好的边的,所有边,就是下一次切分的横切边
- Prim算法执行过程
- 程序执行过程
- 先找到任意一个顶点,将该顶点加入到已添加顶点集合addedVertices中
- 将以该顶点为起点的所有边放入一个小顶堆中
- 只要已添加的顶点数,小于整个图的顶点数,说明还没有将所有点连接起来,因此应该执行如下循环
- 从堆中找到这些边中权值最小的一条边,并从堆中删除
- 将该边加入到最后返回的,表示最小生成树所有边的edgeInfos集合中
- 将这条边的终点,加入到已添加顶点集合addedVertices中
- 将所有以该终点为起点的所有边,加入到堆中
- 循环结束后,得到的就是最后包含最小生成树的所有边信息的集合
14.2 Kruskal(克鲁斯克尔算法)
- Kruskal算法执行过程
- 按照边的权重顺序(从小到大)将边加入生成树中,直到生成树中含有 V – 1 条边为止( V 是顶点数量)
- 若加入该边会与生成树形成环,则不加入该边
- 从第3条边开始,可能会与生成树形成环
- 程序执行过程
- 将所有边加入到最小堆中
- 将所有顶点加入到并查集中
- 只要选出的边,还未达到顶点数-1,也就是还未选出最小生成树,就执行以下循环
- 从堆中找到权值最小的边,并从堆中删除该边
- 判断该边的起点和终点,在并查集中,是否属于同一个子集合,如果属于,说明这条边会导致形成环,因此不能选用该边
- 如果不属于同一个子集合,该边就是最小生成树的边,放入最终需要返回的最小生成树边集合edgeInfos中
- 在并查集中,对该边的终点和起点进行合并,让他们归属于同一个子集合
15 最短路径(Shortest Path)
- 最短路径是指两顶点之间权值之和最小的路径(有向图、无向图均适用,不能有负权环)
- 负权环:构成环的整个权重加起来是负的。如果有负权环,是根本无法算出最小路径的
- 负权边:边的权重为负数。Dijkstra算法,不能计算图中含有负权边的最短路径
15.1 Dijkstra算法
- Dijkstra 属于单源最短路径算法,用于计算一个顶点到其他所有顶点的最短路径
- 使用前提:不能有负权边
- 时间复杂度:可优化至 O (ElogV) ,E 是边数量,V 是节点数量
- Dijkstra的等价思考
- Dijkstra 的原理其实跟生活中的一些自然现象完全一样
- 把每1个顶点想象成是1块小石头
- 每1条边想象成是1条绳子,每一条绳子都连接着2块小石头,边的权值就是绳子的长度
- 将小石头和绳子平放在一张桌子上(下图是一张俯视图,图中黄颜色的是桌子)
- 接下来想象一下,手拽着小石头A,慢慢地向上提起来,远离桌面
- B、D、C、E会依次离开桌面
- 最后绷直的绳子就是A到其他小石头的最短路径
- 注:后离开桌面的小石头,都是被先离开桌面的小石头拉起来的,所以下图中,路径长度最短的那个终点一定是下一个离开地面的"石头"
- Dijkstra算法执行过程
- 找到以A为起点的所有边,其中路径长度最短的,就是到该终点的最短路径
- 对以该终点为起点的所有边进行松弛操作
- 对边进行松弛操作,其实就是指用源点到该边的起点的最短路径+该边的权值,重新计算源点到该边终点的距离
- 目的是看是否能得到比原来统计出的源点到该边终点的权值,更小的权值
- 重新找到路径长度中最短的,循环执行上述操作
- 程序执行过程
- 先只将A这个终点,以及A到A的最短路径,权值0,放入用于存放红色和蓝色记录的paths中
- 只要paths中不为空,也就是未找到所有最短路径,执行如下循环
- 获取paths中当前路径长度最短的那条路径
- 将这条路径信息放入到最终需要返回的存放所有最短路径的selectedPaths集合中
- 从paths中删除这条路径
- 将以这条路径的终点为起点的所有边,放入到paths中,循环执行如下操作
- 如果该边的终点的最短路径已经找到,那么不做处理
- 如果未被找到,对该边进行松弛操作
- 也就是使用已经选好的、该边起点的最小路径+该边权值,与之前该边起点的最小路径进行比较
- 如果前者更小,那么更新paths中最短路径、路径长度等信息
- 如果后者更小,不做处理
- 将selectedPaths中,作为辅助用的,以A为起点、A为终点、A-A为最短路径,0为路径长度的那条信息删除
15.2 Bellman-Ford算法
- Bellman-Ford 也属于单源最短路径算法,支持负权边,还能检测出是否有负权环
- 时间复杂度:O(EV) ,E 是边数量,V 是节点数量
- 最好情况对所有边仅需进行 1 次松弛操作就能计算出A到达其他所有顶点的最短路径
- 注:如果一个路径起点的最短路径都没有得到,那么改路径一定无法松弛成功
- Bellman-Ford算法执行过程
- 对所有的边进行 V – 1 次松弛操作( V 是节点数量),就能得到所有可能的最短路径
- 程序执行过程
- 先将A为终点、A->A为最短路径,0为路径长度这些信息放入最终要返回的记录着终点、最短路径、路径长度信息的selectedPaths集合中
- 循环执行下面过程顶点个数-1次
- 循环所有的边,对它们进行松弛操作
- 此时可以尝试对所有边再进行一次松弛操作,如果有一次松弛成功,就说明该图中有负权环
- 将selectedPaths中,作为辅助用的,以A为起点、A为终点、A-A为最短路径,0为路径长度的那条信息删除
15.3 Floyd算法
- Floyd 属于多源最短路径算法,能够求出任意2个顶点之间的最短路径,支持负权边
- 时间复杂度:O(V 3 ),效率比执行 V 次 Dijkstra 算法要好( V 是顶点数量)
- 算法原理:
- 从任意顶点 i 到任意顶点 j 的最短路径不外乎两种可能
- 直接从 i 到 j
- 从 i 经过若干个顶点到 j
- 假设 dist(i,j) 为顶点 i 到顶点 j 的最短路径的距离
- 对于每一个顶点 k,检查 dist(i,k) + dist(k,j)<dist(i,j) 是否成立
- 如果成立,证明从 i 到 k 再到 j 的路径比 i 直接到 j 的路径短,设置 dist(i,j) = dist(i,k) + dist(k,j)
- 当我们遍历完所有结点 k,dist(i,j) 中记录的便是 i 到 j 的最短路径的距离
- 程序执行过程
- 先准备一个最终需要返回的,存放从任意顶点到任意顶点最短路径信息的paths
- 对paths进行初始化,将当前所有边的起点、终点、路径信息,放入其中
- 对所有顶点,展开三层循环,最外层表示顶点k,变量设为v2,中间一层表示i,变量设为v1,最后一层表示j,变量设为v3,最内层循环执行如下步骤
- 判断如果三层循环中的顶点,有任意两层是相同的,直接退出本次循环
- 如果v1到v2或v2到v3目前为止还未被找到一条路径,就退出本次循环
- 如果上面都符合,且v1到v2+v2到v3,小于v1到v3,那么更新paths中v1到v3的路径
- 循环结束后,paths就是最终的结果
16 相关代码
- Data:用于构建指定的图
package com.mj;
public class Data {
public static final Object[][] BFS_01 = {
{
"A", "B"}, {
"A", "F"},
{
"B", "C"}, {
"B", "I"}, {
"B", "G"},
{
"C", "I"}, {
"C", "D"},
{
"D", "I"}, {
"D", "G"}, {
"D", "E"}, {
"D", "H"},
{
"E", "H"}, {
"E", "F"},
{
"F", "G"},
{
"G", "H"},
};
public static final Object[][] BFS_02 = {
{
0, 1}, {
0, 4},
{
1, 2},
{
2, 0}, {
2, 4}, {
2, 5},
{
3, 1},
{
4, 6}, {
4, 7},
{
5, 3}, {
5, 7},
{
6, 2}, {
6, 7}
};
public static final Object[][] BFS_03 = {
{
0, 2}, {
0, 3},
{
1, 2}, {
1, 3}, {
1, 6},
{
2, 4},
{
3, 7},
{
4, 6},
{
5, 6},
{
6, 7}
};
public static final Object[][] BFS_04 = {
{
1, 2}, {
1, 3}, {
1, 5},
{
2, 0},
{
3, 5},
{
5, 6}, {
5, 7},
{
6, 2},
{
7, 6}
};
public static final Object[][] DFS_01 = {
{
0, 1},
{
1, 3}, {
1, 5}, {
1, 6}, {
1, 2},
{
2, 4},
{
3, 7}
};
public static final Object[][] DFS_02 = {
{
"a", "b"}, {
"a", "e"},
{
"b", "e"},
{
"c", "b"},
{
"d", "a"},
{
"e", "c"}, {
"e", "f"},
{
"f", "c"}
};
public static final Object[][] TOPO = {
{
0, 2},
{
1, 0},
{
2, 5}, {
2, 6},
{
3, 1}, {
3, 5}, {
3, 7},
{
5, 7},
{
6, 4},
{
7, 6}
};
public static final Object[][] NO_WEIGHT2 = {
{
0, 3},
{
1, 3}, {
1, 6},
{
2, 1},
{
3, 5},
{
6, 2}, {
6, 5},
{
4, 7}
};
public static final Object[][] NO_WEIGHT3 = {
{
0, 1}, {
0, 2},
{
1, 2}, {
1, 5},
{
2, 4}, {
2, 5},
{
5, 6}, {
7, 6},
{
3}
};
public static final Object[][] MST_01 = {
{
0, 2, 2}, {
0, 4, 7},
{
1, 2, 3}, {
1, 5, 1}, {
1, 6, 7},
{
2, 4, 4}, {
2, 5, 3}, {
2, 6, 6},
{
3, 7, 9},
{
4, 6, 8},
{
5, 6, 4}, {
5, 7, 5}
};
public static final Object[][] MST_02 = {
{
"A", "B", 17}, {
"A", "F", 1}, {
"A", "E", 16},
{
"B", "C", 6}, {
"B", "D", 5}, {
"B", "F", 11},
{
"C", "D", 10},
{
"D", "E", 4}, {
"D", "F", 14},
{
"E", "F", 33}
};
public static final Object[][] WEIGHT3 = {
{
"广州", "佛山", 100}, {
"广州", "珠海", 200}, {
"广州", "肇庆", 200},
{
"佛山", "珠海", 50}, {
"佛山", "深圳", 150},
{
"肇庆",