数据结构与算法之 —— 图 Graph

1,698 阅读8分钟

0x01 基本概念

核心概念

图:一组由边连接的节点(或顶点)| 一些离散的节点和若干连接它们的边的集合,是网络结构的抽象模型。

**边:**连接两个节点的线段。

**权重:**边的权值

**路径:**若干个节点的连续序列,用 A-B-F 表示从节点 A 到节点 F 的路径。不包含重复的节点的路径称为「简单路径」,我们这一期所说的路径全都是简单路径

**节点的距离:**路径权重的和

节点和边,是图的最基本单元

其他概念

环:一条只有第一个和最后一个节点重复的非空路径。有环的图称为「有环图」,没有环的称为「无环图」

平行边:连接相同节点的边

相邻节点:同一条边连接的两个节点

节点的度:某个节点的相邻节点个数

连通图:任意两个节点都存在路径的图

有向图 & 无向图

根据边是否有方向分为有向图和无向图

一个图里,要么所有边都有方向,要么所有边的没有方向

0x02 存储结构

图可以有多种存储结构,不存在绝对正确的方式,实际使用哪种方式,只取决待解决的问题和图的类型

1、邻接矩阵

矩阵的行和列都表示节点,矩阵的值表示边的权重。未连接的边权重是 0。对于无向图来说是对称矩阵。对很多图而言,大量的数值都是 0,因此非常浪费存储空间

2、关联矩阵

矩阵的行表示节点,列表示边。适用于边比节点数量多的情况,大幅度地节约空间和内存。

3、邻接表

邻接表由图中每个节点的相邻节点列表组成。好处是不浪费存储空间。

对比矩阵的存储方式,邻接表可以用多种数据结构来存储,如数组、链表等,矩阵一般只能用二维数组来存储。而矩阵存储的优势在于针对特定种类的图可以做针对性的优化,无论是存储空间还是查找效率方面。

0x03 遍历 / 搜索

遍历和搜索的区别仅在于有无指定终点,算法核心没区别

1、广度优先搜索

优先遍历相邻节点的搜索算法

核心步骤只有两步:

  1. 访问节点,将该节点标记为已访问,从待访问节点中清除;

  2. 标记节点的相邻节点为待访问节点,如果已访问过或已经是待访问节点,则不标记;

具体从 A 开始的遍历过程如下:

  1. 选择 A 为起始节点;

  2. 访问 A,其相邻节点为 B、C。则新增待访问节点 B、C;【B、C】

  3. 访问 B,其相邻节点为 A、D、E。则新增待访问节点 D、E;【C、D、E】;

  4. 访问 C,其相邻节点为 A、D、E。则无新增待访问节点;【D、E】;

  5. 访问 D,其相邻节点为 B、C、F。则新增待访问节点 F;【E、F】;

  6. 访问 E,其相邻节点为 B、C、F。则无新增待访问节点;【F】;

  7. 访问 F,其相邻节点为 D、E。则无新增待访问节点;【 】;

  8. 待访问节点为空,遍历结束;

访问顺序为:A、B、C、D、E、F

2、深度优先搜索

优先遍历相邻节点的相邻节点的搜索算法

核心步骤也是两步:(和广度优先完全相同)

  1. 访问节点,将该节点标记为已访问,从待访问节点中清除;

  2. 标记节点的相邻节点为待访问节点,如果已访问过或已经是待访问节点,则不标记;

区别于广度优先搜索在于待访问节点的存储方式不同:
1. 广度优先:队列 - 先进先出
2. 深度优先:栈 - 先进后出

具体从 A 开始的遍历过程如下:

  1. 选择 A 为起始节点;

  2. 访问 A,其相邻节点为 B、C。则新增待访问节点 B、C;【C、B】

  3. 访问 C,其相邻节点为 A、D、E。则新增待访问节点 D、E;【E、D、B】;

  4. 访问 E,其相邻节点为 B、C、F。则新增待访问节点 F;【F、D、B】;

  5. 访问 F,其相邻节点为 D、E。则无新增待访问节点 ;【D、B】;

  6. 访问 D,其相邻节点为 B、C、F。则无新增待访问节点;【B】;

  7. 访问 B,其相邻节点为 A、D、F。则无新增待访问节点;【 】;

  8. 待访问节点为空,遍历结束;

访问顺序为:A、C、E、F、D、B

0x04 两个经典算法应用

1、两个节点的最短/长路径(最小/大距离)

两个节点之间的所有路径中,权重合最小的路径为最短路径

求最短路径的经典算法有很多,这里只介绍比较好理解的一种:

贝尔曼-福特(Bellman-ford) 算法(边为基本单元)

算法核心

  1. 将起点权重设为 0,其他节点设为无穷大。节点的权重表示:从起点到该节点的最短路径的暂定距离。

  2. 遍历所有的边,更新边两侧节点的权重

  3. 权重计算规则:取边的两个方向分别计算两个节点的权重,终点权重 = Math.min(终点权重原值, 起点权重 + 边的权重)

  4. 无向图中,实际上只需要重新计算两个节点中权重较大的节点即可

  5. 重复步骤 2,直到没有任何节点的权重再变化

算法 demo

A 为起点,F 为终点,寻找 A 到 F 的最短路径

一共有 8 条边,A-B、A-C、B-D、B-E、C-D、C-E、D-F、E-F

第一次遍历

  1. 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)算法(节点为基本单元)

算法核心

  1. 以起始节点开始,将其加入到最小生成树

  2. 遍历所有非最小生成树节点,找到距离生成树任意节点最近的邻接点,将该邻接点加入到最小生成树中。

  3. 此时最小生成树已添加新成员,则还需对剩余节点的最小价值进行更新

  4. 重复步骤2和3,直至最小生成树构造完成(即最小生成树节点数达成)

算法 demo

  1. 取节点 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、地图导航 - 路径规划、最短路径、地铁路径规划、出行航班选择等等

图更擅长研究节点的距离和关系

0x06 算法书籍推荐

入门

book.douban.com/subject/303…

book.douban.com/subject/258…

进阶

book.douban.com/subject/199…

book.douban.com/subject/266…

book.douban.com/subject/204…