数据结构与算法 | 图(Graph)
在这之前已经写了数组、链表、二叉树、栈、队列等数据结构,本篇一起探究一个新的数据结构:图(Graphs )。在二叉树里面有着节点(node)的概念,每个节点里面包含左、右两个子节点指针;比对于图来说同样有着节点(node),在图里也称为顶点(vertex),顶点之间的关联不在局限于2个(左、右),一个顶点可以与任意(0-n个)个顶点进行链接,这称之为边(edge)。 一般会把一个图里面顶点的集合记作 V ,图里面边的集合记作 E,图也就用 G(V,E) 来表示。

对比二叉树可以看到图的约束更少,换一个角度看二叉树结构是图的特殊形式,所谓特殊形式指加上更多的限定条件。
图的分类(Types Of Graph)
可以看到图的基本的结构非常简单,约束也很少,如果在其中加上各种条件约束就可以定义各种类型的图。
- 约束边或者顶点个数来分类:
零图(Null graph):只有顶点没有边的图;平凡图(Trivial graph):只有一个顶点的图;
- 按照边是否有指向来分类:
有向图(Directed Graph):在每个边的定义中,节点都是有序的对。也就是(A,B)与(B,A)表示不同的边,一个代表从A到B方向的边,一个代表从B到A方向的边。无向图(Undirected Graph):边只是代表链接,没有指向性。(A,B)与(B,A)表示的同样的边。
- 根据是否在边上存储数据分类:
权重图(Weighted Graph):图中的边上附加了权重或值的图。这些权重表示连接两个节点之间的距离、代价、容量或其他度量。权重可以是任何数值,通常用于描述节点间的关系特性。
还有很多分类在此不一一罗列。每类图可能还会有其独特的一些特征描述,比如有向图(Directed Graph)里面,以某顶点作为开始的边的数量称为这个顶点的入度(Indegree),以某个顶点作为结束的边的数量称为这个顶点的出度(Outdegree)等等。
通过以上描述,可以感受到图其实是非常灵活的数据结构,同时它的衍生概念也非常多;初次探究大可不必一一记牢,有个基本的图结构知识体系即可,后续遇到的时候再扩充图的知识体系更为合适。

图的表达(Representation of Graphs)
图的表达其实也有多种形式,不过最基本的形式是:邻接矩阵(Adjacency Matrix) 与 邻接表(Adjacency List)
邻接矩阵(Adjacency Matrix)
邻接矩阵,所谓“矩阵”具体到代码其实就是二维数组,通过二维数组来表示图中顶点之间的边的关系。二维数组中的行和列分别代表图中的顶点,矩阵中的值表示顶点之间是否相连或连接的边的权重。
且用这种方式来表示先前示例的图结构,矩阵的值 0代表无相连边,1代表有相连边。如下:

邻接表(Adjacency List)
邻接表,所谓“表”指的就是列表 List ,图中的每个节点都有一个对应的列表,用于存储与该节点直接相连的其他节点的信息。邻接表中的每个节点列表包含了该节点相邻节点的标识符或指针等信息。对于无权图,通常使用数组或链表来存储相邻节点的标识符。而对于带权图,列表中可能还包含了边的权重信息。

基本应用示例(Basic Examples)
Leetcode 997. 找到小镇的法官【简单】
小镇里有 n 个人,按从 1 到 n 的顺序编号。传言称,这些人中有一个暗地里是小镇法官。
如果小镇法官真的存在,那么:
小镇法官不会信任任何人。
每个人(除了小镇法官)都信任这位小镇法官。
只有一个人同时满足属性 1 和属性 2 。
给你一个数组 trust ,其中 trusti = ai, bi 表示编号为 ai 的人信任编号为 bi 的人。
如果小镇法官存在并且可以确定他的身份,请返回该法官的编号;否则,返回 -1 。
示例
输入:n = 2, trust = [1,2]
输出:2
题目故事背景描述比较多,可以看到 信任的表述 可以用有向图的边来表示,每个人 用顶点 来表示,小镇法官的第1点 代表就是出度为 0,第2点 代表就是 入度为 n-1。 这样题目就转换为:判断一个n个顶点的有向图中 是否存在出度为0,入度为n-1的顶点 ;存在返回顶点编号,不存在返回 -1。
PS:关键点,将复杂描述的题目,建模成为图
public int findJudge(int n, int[][] trust) {
int[] outDegree = new int[n+1],inDegree = new int[n+1];
for(int i = 0; i < trust.length; i++){
outDegree[trust[i][0]] ++;
inDegree[trust[i][1]]++;
}
for(int i=1; i<= n; i++)
if(outDegree[i] == 0 && inDegree[i] == (n-1))
return i;
return -1;
}
Leetcode 787. K 站中转内最便宜的航班【中等】
有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flightsi = fromi, toi, pricei ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。
示例
输入: n = 3, edges = [0,1,100,1,2,100,0,2,500],src = 0, dst = 2, k = 1
输出: 200
备注:1 <= n <= 100,航班没有重复,且不存在自环
将城市看作是顶点,城市-城市之间的航班看作是 有向图边,航班的价格作为边的权重,也就完成了题意到图的建模。考虑到,城市数量 n < 100, 因此可以采用 邻接矩阵的方式来进行图的表达。
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
// 图 初始化建模
int[][] map = new int[n][n];
for(int i = 0; i < flights.length; i++){
map[flights[i][0]][flights[i][1]] = flights[i][2];
}
// 其他逻辑
}
以 src 作为 源顶点,通过以 src作为 起始顶点的边 链接到更多的顶点(此时经过 0个站中转);以这些链接到的顶点 为起始点,继续链接到更多的顶点(经过 1个站中转);继而可以推导到 经过 n 个站中转。这也就是典型的广度优先搜索(BFS),来遍历以src作为 源顶点的图,遍历代码如下:
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
// ...
// BFS
Deque<Integer> que = new ArrayDeque<>();
// src 作为起始点
que.offer(src);
// 经过 k 个中转站
for(int i = 0; i <= k && !que.isEmpty(); i++){
int size = que.size();
while( size-- > 0){
int node = que.poll();
for(int j = 0; j < map[node].length; j++){
// map[node][j] == 0 代表 node -> 不相连跳过
if( map[node][j] == 0) continue;
// ... 这里可以加入遍历过程中更多的逻辑
// 进入下一轮遍历
que.offer(j);
}
}
}
// ...
}
考虑题目需要的是 最多经过 k 站中转的 最便宜线路,不妨 广度优先遍历中 用 distSet[] 记录下 src 可到达站点的 最低价格;最后返回 distSet[ dst ] 即可, 这里注意下的是 如果没到达,按照题意应返回 -1。
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
// ...
int[] distSet = new int[n];
que.offer(src);
for(int i = 0; i <= k && !que.isEmpty(); i++){
// 判断当前最小的 标准 是基于上一轮的遍历结果
int[] pre = Arrays.copyOf(distSet,distSet.length);
int size = que.size();
while( size-- > 0){
int node = que.poll();
for(int j = 0; j < map[node].length; j++){
if( map[node][j] == 0) continue;
// distSet[j] == 0 代表之前没有到达过,因此需要 写入 distSet[j]
// 如果当前距离 不之前大,这个顶点不必进行下一轮遍历
if( distSet[j] != 0 &&
distSet[j] < pre[node] + map[node][j]) continue;
// 记录最小结果
distSet[j] = pre[node] + map[node][j] ;
que.offer(j);
}
}
}
// distSet[j] == 0 代表之前没有到达过,返回 -1
return distSet[dst] == 0 ? -1:distSet[dst];
}
这里其实是 使用 Bellman-Ford 算法的思想进行解题;在图算法领域还有着很多著名的算法,后续可以整理下更专业的解读,这里只是演示个简单的应用。
Bellman-Ford 算法,最初由Alfonso Shimbel 1955年提出,但以 Richard Bellman 和 Lester Ford Jr.的名字命名,他们分别于 1958年 和 1956年 发表了该算法,向前辈致敬。
最后附上完整代码:
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
int[][] map = new int[n][n];
for(int i = 0; i < flights.length; i++){
map[flights[i][0]][flights[i][1]] = flights[i][2];
}
int[] distSet = new int[n];
Deque<Integer> que = new ArrayDeque<>();
que.offer(src);
for(int i = 0; i <= k && !que.isEmpty(); i++){
int[] pre = Arrays.copyOf(distSet,distSet.length);
int size = que.size();
while( size-- > 0){
int node = que.poll();
for(int j = 0; j < map[node].length; j++){
if( map[node][j] == 0) continue;
if( distSet[j] != 0 &&
distSet[j] < pre[node] + map[node][j]) continue;
distSet[j] = pre[node] + map[node][j] ;
que.offer(j);
}
}
}
return distSet[dst] == 0 ? -1:distSet[dst];
}
欢迎关注 Java研究者 专栏、博客、公众号等。大伙儿的喜欢是创作最大的动力。
数据结构与算法 | 图(Graph)的更多相关文章
- python数据结构与算法——图的基本实现及迭代器
本文参考自<复杂性思考>一书的第二章,并给出这一章节里我的习题解答. (这书不到120页纸,要卖50块!!,一开始以为很厚的样子,拿回来一看,尼玛.....代码很少,给点提示,然后让读者自 ...
- python数据结构与算法——图的广度优先和深度优先的算法
根据维基百科的伪代码实现: 广度优先BFS: 使用队列,集合 标记初始结点已被发现,放入队列 每次循环从队列弹出一个结点 将该节点的所有相连结点放入队列,并标记已被发现 通过队列,将迷宫路口所有的门打 ...
- python数据结构与算法——图的最短路径(Floyd-Warshall算法)
使用Floyd-Warshall算法 求图两点之间的最短路径 不允许有负权边,时间复杂度高,思路简单 # 城市地图(字典的字典) # 字典的第1个键为起点城市,第2个键为目标城市其键值为两个城市间的直 ...
- python数据结构与算法——图的最短路径(Dijkstra算法)
# Dijkstra算法——通过边实现松弛 # 指定一个点到其他各顶点的路径——单源最短路径 # 初始化图参数 G = {1:{1:0, 2:1, 3:12}, 2:{2:0, 3:9, 4:3}, ...
- python数据结构与算法——图的最短路径(Bellman-Ford算法)解决负权边
# Bellman-Ford核心算法 # 对于一个包含n个顶点,m条边的图, 计算源点到任意点的最短距离 # 循环n-1轮,每轮对m条边进行一次松弛操作 # 定理: # 在一个含有n个顶点的图中,任意 ...
- 数据结构与算法-图的最短路径Dijkstra
一 无向图单源最短路径,Dijkstra算法 计算源点a到图中其他节点的最短距离,是一种贪心算法.利用局部最优,求解全局最优解. 设立一个visited访问和dist距离数组,在初始化后每一次收集一 ...
- 数据结构与算法——图(游戏中的自动寻路-A*算法)
在复杂的 3D 游戏环境中如何能使非玩家控制角色准确实现自动寻路功能成为了 3D 游戏开 发技术中一大研究热点.其中 A*算法得到了大量的运用,A*算法较之传统的路径规划算法,实时性更高.灵活性更强, ...
- python数据结构与算法
最近忙着准备各种笔试的东西,主要看什么数据结构啊,算法啦,balahbalah啊,以前一直就没看过这些,就挑了本简单的<啊哈算法>入门,不过里面的数据结构和算法都是用C语言写的,而自己对p ...
- 算法与数据结构基础 - 图(Graph)
图基础 图(Graph)应用广泛,程序中可用邻接表和邻接矩阵表示图.依据不同维度,图可以分为有向图/无向图.有权图/无权图.连通图/非连通图.循环图/非循环图,有向图中的顶点具有入度/出度的概念. 面 ...
- 数据结构与算法系列研究七——图、prim算法、dijkstra算法
图.prim算法.dijkstra算法 1. 图的定义 图(Graph)可以简单表示为G=<V, E>,其中V称为顶点(vertex)集合,E称为边(edge)集合.图论中的图(graph ...
随机推荐
- 【博客索引】Welcome!!
欢迎来到 Daniel_yzy 的博客园 个人简介 初二,男,就读于长沙市一中双语实验学校. 爱好 OI,一生讨厌文化课. 当然,也是唯物主义无神论者. 已有 npy,要问是谁的话可以私下问. 博客索 ...
- 痞子衡嵌入式:恩智浦i.MX RT1xxx系列MCU启动那些事(10.A)- FlexSPI NAND启动时间(RT1170)
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是恩智浦i.MX RT1170 FlexSPI NAND启动时间. 本篇是 i.MXRT1170 启动时间评测第四弹,前三篇分别给大家评测 ...
- 创建python虚拟环境并打包python文件
前言 当需要为一个离线环境部署python应用时,离线环境可能缺少各种python环境,有docker的话可以用docker,没有docker可以用pyinstaller打包成二进制文件.pyinst ...
- 【pandas小技巧】--列值的映射
映射列值是指将一个列中的某些特定值映射为另外一些值,常用于数据清洗和转换. 使用映射列值的场景有很多,以下是几种常见的场景: 将字符串类型的列中的某些值映射为数字.例如,将"男"和 ...
- 文心一言 VS 讯飞星火 VS chatgpt (74)-- 算法导论7.2 2题
二.如果用go语言,当数组A的所有元素都具有相同值时,QUICKSORT的时间复杂度是什么? 文心一言: 在Go语言中,对一个所有元素都相等的数组进行快速排序(QuickSort)的时间复杂度是O(n ...
- Mysql基础9-事务
一.事务简介 事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有操作作为一个整体一起向系统提交或者撤销操作请求,即这些操作要么同时成功,要么同时失败.mysql的事务默认是自动提交的,也就 ...
- 在编写API接口的技术文章时应注意的内容
编写API接口的技术文章时,建议包含以下内容: 1. 简介:介绍API接口的目的和作用,以及所属的项目或服务. 2. 接口描述:详细描述API接口的功能和使用方法,包括输入参数.输出结果和可能的错误码 ...
- 为何 Linus 一个人就能写出这么强的系统,中国却做不出来?
前言 知乎上有一个提问:为何 Linus 一个人就能写出这么强的系统,中国却做不出来? ↓↓↓ 今天,我们就这个话题,一起来做个讨论. 不知道大家是怎么看这个问题的?是美国人更聪明吗,所以才能写出这么 ...
- Appilot发布:打造面向DevOps场景的开源AI助手
今日,数澈软件Seal (以下简称"Seal")宣布推出面向 DevOps 场景的 AI 助手 Appilot,这款产品将充分利用 AI 大语言模型的能力为用户提供变革性的部署和应 ...
- vue中watch侦听器,deep和immediate的用法
1.deep深度监听的用法 当监听一个对象时,可能想监听整个对象的变化,而不仅仅是某个属性.但在默认情况下,如果你正在监听formData对象并且修改了formData.username,对应的侦听器 ...