0x01 基本概念
核心概念
图:一组由边连接的节点(或顶点)| 一些离散的节点和若干连接它们的边的集合,是网络结构的抽象模型。
**边:**连接两个节点的线段。
**权重:**边的权值
**路径:**若干个节点的连续序列,用 A-B-F 表示从节点 A 到节点 F 的路径。不包含重复的节点的路径称为「简单路径」,我们这一期所说的路径全都是简单路径
**节点的距离:**路径权重的和
节点和边,是图的最基本单元
其他概念
环:一条只有第一个和最后一个节点重复的非空路径。有环的图称为「有环图」,没有环的称为「无环图」
平行边:连接相同节点的边
相邻节点:同一条边连接的两个节点
节点的度:某个节点的相邻节点个数
连通图:任意两个节点都存在路径的图
有向图 & 无向图
根据边是否有方向分为有向图和无向图
一个图里,要么所有边都有方向,要么所有边的没有方向
0x02 存储结构
图可以有多种存储结构,不存在绝对正确的方式,实际使用哪种方式,只取决待解决的问题和图的类型
1、邻接矩阵
矩阵的行和列都表示节点,矩阵的值表示边的权重。未连接的边权重是 0。对于无向图来说是对称矩阵。对很多图而言,大量的数值都是 0,因此非常浪费存储空间
2、关联矩阵
矩阵的行表示节点,列表示边。适用于边比节点数量多的情况,大幅度地节约空间和内存。
3、邻接表
邻接表由图中每个节点的相邻节点列表组成。好处是不浪费存储空间。
对比矩阵的存储方式,邻接表可以用多种数据结构来存储,如数组、链表等,矩阵一般只能用二维数组来存储。而矩阵存储的优势在于针对特定种类的图可以做针对性的优化,无论是存储空间还是查找效率方面。
0x03 遍历 / 搜索
遍历和搜索的区别仅在于有无指定终点,算法核心没区别
1、广度优先搜索
优先遍历相邻节点的搜索算法
核心步骤只有两步:
-
访问节点,将该节点标记为已访问,从待访问节点中清除;
-
标记节点的相邻节点为待访问节点,如果已访问过或已经是待访问节点,则不标记;
具体从 A 开始的遍历过程如下:
-
选择 A 为起始节点;
-
访问 A,其相邻节点为 B、C。则新增待访问节点 B、C;【B、C】
-
访问 B,其相邻节点为 A、D、E。则新增待访问节点 D、E;【C、D、E】;
-
访问 C,其相邻节点为 A、D、E。则无新增待访问节点;【D、E】;
-
访问 D,其相邻节点为 B、C、F。则新增待访问节点 F;【E、F】;
-
访问 E,其相邻节点为 B、C、F。则无新增待访问节点;【F】;
-
访问 F,其相邻节点为 D、E。则无新增待访问节点;【 】;
-
待访问节点为空,遍历结束;
2、深度优先搜索
优先遍历相邻节点的相邻节点的搜索算法
核心步骤也是两步:(和广度优先完全相同)
-
访问节点,将该节点标记为已访问,从待访问节点中清除;
-
标记节点的相邻节点为待访问节点,如果已访问过或已经是待访问节点,则不标记;
区别于广度优先搜索在于待访问节点的存储方式不同:
1. 广度优先:队列 - 先进先出
2. 深度优先:栈 - 先进后出
具体从 A 开始的遍历过程如下:
-
选择 A 为起始节点;
-
访问 A,其相邻节点为 B、C。则新增待访问节点 B、C;【C、B】
-
访问 C,其相邻节点为 A、D、E。则新增待访问节点 D、E;【E、D、B】;
-
访问 E,其相邻节点为 B、C、F。则新增待访问节点 F;【F、D、B】;
-
访问 F,其相邻节点为 D、E。则无新增待访问节点 ;【D、B】;
-
访问 D,其相邻节点为 B、C、F。则无新增待访问节点;【B】;
-
访问 B,其相邻节点为 A、D、F。则无新增待访问节点;【 】;
-
待访问节点为空,遍历结束;
访问顺序为:A、C、E、F、D、B
0x04 两个经典算法应用
1、两个节点的最短/长路径(最小/大距离)
两个节点之间的所有路径中,权重合最小的路径为最短路径
求最短路径的经典算法有很多,这里只介绍比较好理解的一种:
贝尔曼-福特(Bellman-ford) 算法(边为基本单元)
算法核心
-
将起点权重设为 0,其他节点设为无穷大。节点的权重表示:从起点到该节点的最短路径的暂定距离。
-
遍历所有的边,更新边两侧节点的权重
-
权重计算规则:取边的两个方向分别计算两个节点的权重,终点权重 = Math.min(终点权重原值, 起点权重 + 边的权重)
-
无向图中,实际上只需要重新计算两个节点中权重较大的节点即可
-
重复步骤 2,直到没有任何节点的权重再变化
算法 demo
A 为起点,F 为终点,寻找 A 到 F 的最短路径
一共有 8 条边,A-B、A-C、B-D、B-E、C-D、C-E、D-F、E-F
第一次遍历
-
A-B:B 的权重 = 0 + 9 = 9,原本权重 ∞,最终权重为 9
2. A-C:C 的权重 = 0 + 1 = 1,原本权重 ∞,最终权重为 1
3. B-D:D 的权重 = 9 + 2 = 11,原本权重 ∞,最终权重为 11
4. B-E:E 的权重 = 9 + 5 = 14,原本权重 ∞,最终权重为 14
5. C-D:D 的权重 = 1 + 3 = 4,原本权重 11,最终权重为 4
6. C-E:E 的权重 = 1 + 7 = 8,原本权重 14,最终权重为 8
7. D-F:C 的权重 = 4 + 3 = 7,原本权重 ∞,最终权重为 7
8. E-F:E 的权重 = 7 + 5 = 12,原本权重 8,最终权重为 8
第一次遍历之后,消除了所有的 ∞
第二次遍历,最后结果如下:
对比第一次遍历,只有节点 B 的权重从 9 变成了 6。说明从 A 到 B 的最短路径并不是 A-B,至少目前 A-C-D-B 就更短一些。
第三次遍历,最后结果如下:
对比第二次遍历,没有节点权重变化,算法结束。
最终结论,A 到 F 的最短路径为:A-C-D-F,权重为 7。下图「得到色 #ff6b00」所示
其他算法
狄克斯特拉(Dijkstra)算法、A* (A-star)算法、SPFA(Shortest Path Faster Algorithm)算法、弗洛伊德(Floyd)算法等(详细见推荐书籍)
2、最小生成树
构造连通网的最小代价生成树。把连通图中的多个或全部节点连接起来且路径之和最小。简单来说,就是用 n-1 条边连接 n 个节点,并且使路径最短
普利姆(Prim)算法(节点为基本单元)
算法核心
-
以起始节点开始,将其加入到最小生成树
-
遍历所有非最小生成树节点,找到距离生成树任意节点最近的邻接点,将该邻接点加入到最小生成树中。
-
此时最小生成树已添加新成员,则还需对剩余节点的最小价值进行更新
-
重复步骤2和3,直至最小生成树构造完成(即最小生成树节点数达成)
算法 demo
- 取节点 A 加入到最小生成树中
2. 遍历剩余节点,找到 C 距离 A 最近,加入到最小生成树中
3. 遍历剩余节点,找到 D 距离 C 最近,加入到最小生成树中
4. 遍历剩余节点,找到 B 距离 D 最近,加入到最小生成树中
5. 遍历剩余节点,找到 F 距离 D 最近,加入到最小生成树中
6. 遍历剩余节点,找到 E 距离 B 或者 F 最近,加入到最小生成树中
7. 无剩余节点,由于在 步骤 6 中存在 2 个相等距离的最近路径,因此该图有两颗最小生成树
其他算法
克鲁斯卡尔(kruskal)算法 - 以边为单位来生成最小子树。(详细见推荐书籍)
0x05 图的实际应用例子
1、基于图存的数据库 Neo4j
Neo4j Graph Platform – The Leader in Graph Databases
neo4j.com
2、六度分割理论
世界上任何互不相识的两人,只需要很少的中间人就能够建立起联系。哈佛大学心理学教授斯坦利·米尔格拉姆于1967年根据这个概念做过一次连锁实验,尝试证明平均只需要6步就可以联系任何两个互不相识的人
3、地图导航 - 路径规划、最短路径、地铁路径规划、出行航班选择等等
图更擅长研究节点的距离和关系