【数据结构入门精讲 | 第十七篇】一文讲清图及各类图算法

在上一篇中我们进行了的并查集相关练习,在这一篇中我们将学习图的知识点。

在这里插入图片描述

目录

    • 概念
    • 深度优先DFS
      • 伪代码
    • 广度优先BFS
      • 伪代码
    • 最短路径算法(Dijkstra)
      • 伪代码
    • Floyd算法
    • 拓扑排序
    • 逆拓扑排序

概念

下面介绍几种在对图操作时常用的算法。

深度优先DFS

深度优先搜索(DFS)是一种用于遍历或搜索树、图等数据结构的基本算法。该算法从给定的起点开始,沿着一条路径直到达到最深的节点,然后再回溯到上一个节点,继续探索下一条路径,直到遍历完所有节点或者找到目标节点为止。

具体步骤如下:

  1. 标记起始节点为已访问。

  2. 访问当前节点,并获取其所有邻居节点。

  3. 遍历所有邻居节点,如果该邻居节点未被访问过,则递归地对该邻居节点进行深度优先搜索。

  4. 重复步骤2和步骤3,直到所有能够到达的节点都被访问过。

DFS算法使用了递归或者栈的机制,在每一轮中尽可能深入地探索,并且只有在到达死胡同(无法继续深入)时才会回溯。DFS并不保证先访问距离起始节点近的节点,而是以深度为导向。

DFS算法可以用于寻找路径、生成拓扑排序、解决回溯问题等,但不保证找到最短路径。其时间复杂度为O(V+E),其中V表示节点数,E表示边数。在树或图的遍历中,DFS通常占用的空间较少,但在最坏情况下可能需要使用大量的栈空间。

简单来说,DFS遵循悬崖勒马回头是岸的原则

拿下图举例:从0一直完左走,走到3,发现没路可走后,回头,继续寻找。

在这里插入图片描述
所以:图的深度优先遍历类似于二叉树的先序遍历

伪代码

# 定义图的数据结构
graph = {'A': ['B', 'C'],'B': ['D', 'E'],'C': ['F'],'D': [],'E': ['F'],'F': []
}# 定义访问状态数组
visited = {}# 初始化访问状态
for node in graph:visited[node] = False# 定义DFS函数
def dfs(node):# 标记当前节点为已访问visited[node] = Trueprint(node, end=' ')# 遍历当前节点的邻接节点for neighbor in graph[node]:# 如果邻接节点未被访问,则递归调用DFS函数if not visited[neighbor]:dfs(neighbor)# 从起始节点开始进行DFS
start_node = 'A'
dfs(start_node)

广度优先BFS

广度优先搜索(BFS)是一种用于遍历或搜索树、图等数据结构的基本算法。该算法从给定的起点开始,按照距离递增的顺序依次访问其所有邻居节点,并将这些邻居节点加入到一个队列中进行遍历,直到访问到目标节点或者遍历完所有节点。

具体步骤如下:

  1. 创建一个队列,将起始节点加入队列中并标记为已访问。

  2. 循环执行以下步骤,直到队列为空:

    • 弹出队列头部的节点。
    • 访问当前节点,并获取其所有邻居节点。
    • 遍历所有邻居节点,如果该邻居节点未被访问过,则将其加入队列尾部,并标记为已访问。
  3. 循环结束后,所有能够从起始节点到达的节点都已经被访问过了。

BFS算法可以用于寻找最短路径或者解决迷宫等问题,其时间复杂度为O(V+E),其中V表示节点数,E表示边数。相对于深度优先搜索,BFS搜索更具有层次性,能够保证先访问距离起始节点近的节点,因此在寻找最短路径时更为有效。

如何对一个图进行广度优先遍历呢?

方法是:每一层从左到右进行遍历

在这里插入图片描述
比如下图的结果就是1、2、3、5、6、4、7

在这里插入图片描述
所以图的广度优先遍历类似于树的层次遍历

伪代码

# 定义图的数据结构
graph = {'A': ['B', 'C'],'B': ['D', 'E'],'C': ['F'],'D': [],'E': ['F'],'F': []
}# 定义访问状态数组
visited = {}# 初始化访问状态
for node in graph:visited[node] = False# 定义BFS函数
def bfs(start_node):# 创建队列并将起始节点入队queue = []queue.append(start_node)visited[start_node] = Truewhile queue:# 取出队首节点current_node = queue.pop(0)print(current_node, end=' ')# 遍历当前节点的邻接节点for neighbor in graph[current_node]:# 如果邻接节点未被访问,则将其入队并标记为已访问if not visited[neighbor]:queue.append(neighbor)visited[neighbor] = True# 从起始节点开始进行BFS
start_node = 'A'
bfs(start_node)

最短路径算法(Dijkstra)

Dijkstra算法是一种用于解决带权重图中单源最短路径问题的经典算法。它能够找到从起始节点到其他所有节点的最短路径。

该算法的基本思想是通过逐步扩展已知最短路径来逐步确定起始节点到其他节点的最短路径。它维护一个距离字典,记录从起始节点到每个节点的当前最短距离,并使用一个优先队列按照距离的大小进行节点的选择和访问。

具体步骤如下:

  1. 创建一个距离字典,并将所有节点的距离初始化为无穷大,将起始节点的距离设置为0。

  2. 将起始节点加入优先队列。

  3. 循环执行以下步骤,直到优先队列为空:

    • 从优先队列中取出距离最小的节点,作为当前节点。
    • 遍历当前节点的所有邻居节点:
      • 计算从起始节点到当前邻居节点的新距离,即当前节点的距离加上当前节点到邻居节点的边的权重。
      • 如果新距离小于邻居节点的当前距离,则更新邻居节点的距离为新距离,并将邻居节点加入优先队列。
  4. 循环结束后,距离字典中记录了从起始节点到所有其他节点的最短距离。

Dijkstra算法适用于有向图或无向图,但要求图中的边权重必须为非负值。它是一种贪心算法,在每一步都选择当前距离最小的节点进行扩展,直到到达目标节点或遍历完所有节点。该算法的时间复杂度为O((|V|+|E|)log|V|),其中|V|是节点数,|E|是边数。

伪代码

# 定义图的数据结构
graph = {'A': {'B': 5, 'C': 3},'B': {'A': 5, 'C': 1, 'D': 6},'C': {'A': 3, 'B': 1, 'D': 2},'D': {'B': 6, 'C': 2}
}# 定义起始节点和终止节点
start_node = 'A'
end_node = 'D'# 定义距离字典和前驱节点字典
distances = {}
predecessors = {}# 初始化距离字典和前驱节点字典
for node in graph:distances[node] = float('inf')  # 将所有节点的距离初始化为无穷大predecessors[node] = None# 设置起始节点的距离为0
distances[start_node] = 0# 定义辅助函数:获取未访问节点中距离最小的节点
def get_min_distance_node(unvisited):min_distance = float('inf')min_node = Nonefor node in unvisited:if distances[node] < min_distance:min_distance = distances[node]min_node = nodereturn min_node# Dijkstra算法主体
unvisited = set(graph.keys())
while unvisited:current_node = get_min_distance_node(unvisited)unvisited.remove(current_node)if current_node == end_node:breakfor neighbor, weight in graph[current_node].items():distance = distances[current_node] + weightif distance < distances[neighbor]:distances[neighbor] = distancepredecessors[neighbor] = current_node# 重构最短路径
path = []
current_node = end_node
while current_node != start_node:path.insert(0, current_node)current_node = predecessors[current_node]
path.insert(0, start_node)# 输出结果
print("最短路径:", path)
print("最短距离:", distances[end_node])

Floyd算法

Floyd算法也称为插点法,是一种用于寻找图中所有节点对之间最短路径的算法,同时也可以用于检测图中是否存在负权回路。

Floyd算法采用动态规划的思想,通过不断更新两个节点之间经过其他节点的最短距离来求解任意两个节点之间的最短路径。具体而言,算法维护一个二维数组 dp,其中 dp[i][j] 表示从节点 i 到节点 j 的最短路径长度。初始化时,若存在一条边从节点 i 到节点 j,则 dp[i][j] 的初值为这条边的边权;否则,dp[i][j] 被赋值为一个足够大的数,表示节点 i 无法到达节点 j。

接下来,我们通过枚举一个中间节点 k,来更新所有节点对之间的最短路径长度。具体而言,如果 dp[i][j] > dp[i][k] + dp[k][j],则说明从节点 i 到节点 j 经过节点 k 的路径比当前的最短路径还要短,此时可以更新 dp[i][j] 的值为 dp[i][k] + dp[k][j]。

重复执行上述步骤,直到枚举完所有的中间节点 k,即可得到任意两个节点之间的最短路径长度。如果在更新过程中发现某些节点之间存在负权回路,则说明无法求解最短路径。

#define INF 99999
#define V 4void floydWarshall(int graph[V][V]) {int dist[V][V], i, j, k;// 初始化最短路径矩阵为图中的边权值for (i = 0; i < V; i++)for (j = 0; j < V; j++)dist[i][j] = graph[i][j];// 动态规划计算最短路径for (k = 0; k < V; k++) {for (i = 0; i < V; i++) {for (j = 0; j < V; j++) {// 如果经过顶点k的路径比直接路径更短,则更新最短路径if (dist[i][k] + dist[k][j] < dist[i][j])dist[i][j] = dist[i][k] + dist[k][j];}}}// 打印最终的最短路径矩阵for (i = 0; i < V; i++) {for (j = 0; j < V; j++) {// 如果路径为无穷大,则打印INF;否则打印最短路径值if (dist[i][j] == INF)printf("%7s", "INF");elseprintf("%7d", dist[i][j]);}printf("\n");}
}

拓扑排序

拓扑排序和逆拓扑排序都是用于对有向无环图进行排序的算法。

拓扑排序:对于一个有向无环图,拓扑排序可以得到一组节点的线性序列,使得对于任何一个有向边 (u, v),在序列中节点 u 都排在节点 v 的前面。以下是拓扑排序的伪代码:

# 定义图的数据结构
graph = {'A': ['B', 'C'],'B': ['D', 'E'],'C': ['F'],'D': [],'E': ['F'],'F': []
}# 定义入度字典
in_degree = {}# 初始化入度字典
for node in graph:in_degree[node] = 0for node in graph:for neighbor in graph[node]:in_degree[neighbor] += 1# 定义队列并将入度为0的节点加入队列
queue = []
for node in in_degree:if in_degree[node] == 0:queue.append(node)# 进行拓扑排序
result = []
while queue:current_node = queue.pop(0)result.append(current_node)for neighbor in graph[current_node]:in_degree[neighbor] -= 1if in_degree[neighbor] == 0:queue.append(neighbor)# 输出结果
print(result)

逆拓扑排序

逆拓扑排序:与拓扑排序相反,逆拓扑排序可以得到一组节点的线性序列,使得对于任何一个有向边 (u, v),在序列中节点 v 都排在节点 u 的前面。以下是逆拓扑排序的伪代码:

# 定义图的数据结构
graph = {'A': ['B', 'C'],'B': ['D', 'E'],'C': ['F'],'D': [],'E': ['F'],'F': []
}# 定义出度字典
out_degree = {}# 初始化出度字典
for node in graph:out_degree[node] = len(graph[node])# 定义队列并将出度为0的节点加入队列
queue = []
for node in out_degree:if out_degree[node] == 0:queue.append(node)# 进行逆拓扑排序
result = []
while queue:current_node = queue.pop(0)result.append(current_node)for neighbor in graph[current_node]:out_degree[neighbor] -= 1if out_degree[neighbor] == 0:queue.append(neighbor)# 输出结果
print(result)

至此,图的知识点就介绍完了,在下一篇中我们将进行图的专项练习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/248689.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

安装一个插件、回馈给你一款属于猿的绚丽浏览器

不知道大家有没有了解&#xff0c;由CSDN推出的一款CSDN浏览器助手&#xff0c;其实哈就我个人而言对一些助手、插件还是很有兴趣的&#xff0c;毕竟他能够让我们快速高效的去完成我们的操作。但是一直没有找到一款称心如意的&#xff0c;知道遇见了这款CSDN浏览器助手让我爱不…

《看完它面试必solo | 寻找C站宝藏》

今天给大家搂点干货&#xff0c;2020 年 9 月 18 日晚 11 点半发布了 Vue 3.0 版本。到目前已经很多公司开始鼓励大家去学习Vue3了&#xff0c;在这里小编就把自己所了解到的‘皮毛’贡献给大家 Vue3.0 的突出亮点 Performance&#xff1a;性能比Vue2快1.2~2倍Tree shaking s…

KVC/KVO 本质

KVO 的实现原理 KVO是关于runtime机制实现的当某个类的对象属性第一次被观察时&#xff0c;系统就会在运行期动态地创建该类的一个派生类&#xff0c;在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制如果原类为Person&am…

攻破 程序员35岁 “瓶颈” 那都不是事!

我正在参与CSDN《新程序员》有奖征文&#xff0c;点击和我一起参与吧 下面呢&#xff0c;我就在这里给大家讲述一下我的经历吧。 初衷 作为程序猿的我们&#xff0c;初衷在哪里&#xff0c;高薪&#xff1f;体面&#xff1f;热爱&#xff1f;曾经有一个同学聊天中说到这个话…

Vue项目中引用‘阿里巴巴字体图标库iconfont’

1.前言 在实际开发中&#xff0c;作为前端开发人员的我们经常会遇到下面这种ui图 我们看到在上面两个平台设计图中的出现了大量的图标&#xff0c;在某种情况下&#xff0c;这种图标是需要我们自己去找的&#xff0c;不要喷我们的 ui 哈&#xff0c;主要是我喜欢麻烦 哈哈&…

项目流程

转载于:https://www.cnblogs.com/Koma-vv/p/10243286.html

最详细的讲解 JS 原型与原型链

文章目录一. 普通对象与函数对象二. 构造函数三. 原型对象四. proto五. 构造器六. 原型链七. Prototype总结一. 普通对象与函数对象 JavaScript 中&#xff0c;万物皆对象&#xff01;但对象也是有区别的。分为普通对象和函数对象&#xff0c;Object 、Function 是 JS 自带的函…

jmeter分布式压测原理简介1

1、什么叫分布式压测&#xff1f; 分布式压测&#xff1a;模拟多台机器向目标机器产生压力&#xff0c;模拟几万用户并发访问 2、分布式压测原理&#xff1a;如下 3、更多补充.....待添加 转载于:https://www.cnblogs.com/yoyoblogs/p/11071774.html

十三 re模块

一&#xff1a;什么是正则&#xff1f; 正则就是用一些具有特殊含义的符号组合到一起&#xff08;称为正则表达式&#xff09;来描述字符或者字符串的方法。或者说&#xff1a;正则就是用来描述一类事物的规则。&#xff08;在Python中&#xff09;它内嵌在Python中&#xff0c…

带你玩转 ui 框架 ——scoped及样式穿透问题详解

前言 在我们前端的开发中经常会使用到各种 ui 框架 下面这两个是比较火的&#xff0c;也是我常用的两个ui框架。 问题描述 但是在使用框架的时候难免会遇到需要改变组件中的一些样式&#xff0c;当然如果我们所有页面的组件样式都是统一的话&#xff0c;我们可以进行全局设置…

三分钟带你掌握 CSS3 的新属性

文章目录1. css3简介2. css3边框2.1 边框圆角2.2 边框阴影3. css3背景3.1背景图大小3.2背景图起始点4. css3文本效果4.1 文本阴影4.2 文本换行5. css3字体图标6. css32D转换7. css3 3D转换8. css3 transition8.1 单项改变8.2 单项改多项改变9. css3 动画1. css3简介 CSS 用于控…

用 div 仿写 input 和 textarea 功能

div仿写input和textarea input不能换行&#xff0c;textarea也不能跟随内容多少而增加高度。 contenteditable true; <div class"msg_content" contenteditable"true" placeholder在这里输入您的留言或建议></div> .msg_content {box-sizing:…

Vue项目中如何设置动态的TDK

TDK是什么 TDK就是网站的标题&#xff08;title&#xff09;、描述&#xff08;description&#xff09;和关键词&#xff08;keyword&#xff09; TDK在哪里 上面大佬对TDK的概念解释的很全面&#xff0c;但是在网页中的TDK在哪里呢&#xff0c;作为开发人员打开F12我们就…

PHP从零开始--基础篇

一、 变量 1.1概念 变量是存储数据的用的容器。 1.2定义变量 变量名的语法规则&#xff1a; 可以是数字、字母、下划线&#xff0c;但是不能以数字开头不能出现空格变量名是区分大小写变量名不能是系统中的关键字行业约定的语法规范 驼峰命名法 比如 myname 定义成 myNam…

PHP从零开始--循环数组

一、循环 1.1单层for循环 1.1.1基础语法 for(初识变量;结束范围;累加/累减){ 重复执行的代码 } 1、 先初识化变量$i 2、 $i<100表达式进行判断 3、 跳入循环&#xff0c;执行重复代码 4、 累加或者累加 5、 再进行$i<100表达式判断 6、 再跳入循环&#xff0c;执行重复…

Spring Cloud(F版)搭建高可用服务注册中心

上一篇文章【Spring Cloud搭建注册中心】成功搭建了一个Eureka Server服务注册中心&#xff0c;不过相信细心的朋友都会发现&#xff0c;这个服务注册中心是一个单节点服务注册中心&#xff0c;万一发生故障或者服务器宕机&#xff0c;那所有的服务可就不能使用了&#xff0c;这…

Python(60)_闭包

1 、闭包的概念 #-*-coding:utf-8-*- 1、闭包&#xff1a;内部函数调用外部函数的变量def outer():a 1def inner():print(a)print(inner.__closure__) outer() print(outer.__closure__) 2 闭包的使用 #-*-coding:utf-8-*- 1、闭包&#xff1a;内部函数调用外部函数的变量 …

PHP从零开始--错误处理函数

一、错误处理 1.1错误种类 1.1.1Notices 比如没有定义变量确使用了会报notice错误&#xff0c;只是提醒注意&#xff0c;不影响后续代码执行 1.1.2Warnings 这是警告错误&#xff0c;比如include引入一个并不存在的文件&#xff0c;不影响后续代码执行 1.1.3Fatal Erro…

第四单元博客总结——暨OO课程总结

第四单元博客总结——暨OO课程总结 第四单元架构设计 第一次UML作业 简单陈述 第一次作业较为简单&#xff0c;只需要实现查询功能&#xff0c;并在查询的同时考虑到性能问题&#xff0c;即我简单的将每一次查询的结果以及递归的上层结果都存储下来&#xff0c;使用一个Boolean…

PHP从零开始--数据库

文章目录一、 数据库简介1.1概念1.2命令行操作1.3连接数据库1.4配置环境变量二、 数据库的相关操作2.1显示所有仓库2.2创建仓库2.3删除仓库2.4切换仓库三、 数据表的相关操作3.1概念3.2显示所有的数据表3.3创建数据表3.2修改字段名3.3查看表结构3.4添加字段3.5删除字段3.6更改数…