数据结构与算法--图的表示与常用算法
什么是图?
图(Graph)形结构中,是一种非线性结构,在图中每一个元素都可以有0或多个前驱,也可以有多个后驱。节点之间的关系是任意的,即图中任意两个数据元素之间都有可能相关。
图的术语
- 顶点:带有数字的圈圈都叫做顶点
- 边:连接两个顶点之间的线叫做边
- 度数:分为入度和出度,入度表示被指向的数,出度表示出发的数
- 路径:如1到5的路径
- 有向图、无向图:边是否带有箭头,图中表示有向图
为什么需要图?
假设一个项目中有多个任务,这些任务之间部分是存在现后顺序的,那么如何去正确的描述这种关系呢?
如图所示,通过图的方式,可以很方便的表示任务的现后顺序,每一个顶点表示一个任务。在B和D任务都完成之前,任务C不可以开始;在任务A开始之前,任务B和D都不能开始。
通过图的方式可以把问题描述清楚,就可以通过不同的图算法来进行求解,比如使用深度优先搜索算法来执行拓扑排序,保证等待任务完成时间的最小化。
图的表示
邻接矩阵表示法
在邻接矩阵中,使用行和列来表示顶点,每个单元格中表示的是两个顶点之间的权重。例如顶点A到顶点B有一条权重为5.6的边,在矩阵中A行B列位置的元素值就应该是5.6
优点:
- 便于判断两个顶点是否有边,只需看元素是否有值即可
- 适用于密集图,空间复杂度为:
O(顶点个数+边的个数)
缺点:
- 不便于插入和删除节点,在图中插入顶点后矩阵需要重新按照新的行/列创建,然后将老的矩阵已有数据复制到新的矩阵中
- 不便于统计与某个顶点相连的边的数目,每次都需要遍历整个表
- 不适用于稀疏图,效率不如邻接表
邻接表表示法
在邻接表实现中,每一个顶点会存储一个从它这里出发的列表。
比如顶点A出发可以到B、C、D,那么A的列表中会有3条边。
优点:
- 便于插入和删除节点,只需要修改一下单链表即可
- 便于统计与这个顶点相连的边的数目,只需要看单链表的大小即可
- 空间效率高,空间复杂度为
O(顶点个数+边的个数)
,更适用于表示稀疏图
缺点:
- 不利于判断两个顶点之间是否有边,需要花费
O(顶点个数)
的时间复杂度扫描邻接表 - 不利于统计有向图顶点的入度,对于无向图来说,顶点对应的链表长度就是该顶点的度,但是在有向图中,链表的大小只能表示出度,而求入度较困难。
边集数组
边集数组是由两个一维数组构成。
- 一个数组存储顶点的信息
- 一个数组存储边的信息,每个元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
优点:
- 更适合对边依次进行处理的操作场景,而不适合对顶点相关的操作
缺点:
- 在边集数组中要查找一个顶点的度需要扫描整个边数组,效率不高
- 插入删除节点需要遍历整个边数组进行删除,效率不高
几种表示方法比较(V代表顶点,E代表边):
操作 | 邻接列表 | 邻接矩阵 | 边集数组 |
---|---|---|---|
存储空间 | O(V + E) | O(V^2) | |
添加顶点 | O(1) | O(V^2) | |
添加边 | O(1) | O(1) | |
检查相邻性 | O(V) | O(1) |
图有哪些算法?
广度优先搜索算法 BFS(breadth-first search)
优先广度遍历,一层遍历完成后再遍历下一层
优点
- 对于解决最短或最少问题特别有效,而且寻找深度小
- 每个结点只访问一遍,结点总是以最短路径被访问,所以第二次路径确定不会比第一次短
缺点
- 内存耗费量大(需要开大量的数组单元用来存储状态)
代码中使用队列+Set实现:
/*** 广度优先搜索算法 BFS(breadth-first search)** @param node 访问出发点*/private void bfs(Node node) {Queue<Node> queue = new LinkedList<>();Set<Node> visitedNode = new HashSet<>();// 访问首节点queue.add(node);visitedNode.add(node);// 广度优先遍历while (!queue.isEmpty()) {Node curNode = queue.poll();System.out.println(node.getValue());// 获取下一层的所有结点List<Node> nextNodes = curNode.getNextNodes();for (Node nextNode : nextNodes) {// 未被访问过则加入队列if (!visitedNode.contains(nextNode)) {queue.add(nextNode);visitedNode.add(nextNode);}}}}
基本过程如下:
- 访问出发点Vi
- 访问Vi所有未被访问过的邻接点Vi1、Vi2、Vi3…,并将它们标记为已访问过(添加到visitedNode集合)
- 然后再按照Vi1、Vi2、Vi3…的次序访问每一个顶点并获取此顶点未访问过的邻接点Vi11、Vi12、Vi13…,以此类推,直到遍历到最后一层
深度优先搜索算法 DFS(depth-first search)
深度优先遍历,走到最深的一层,先把深的遍历完成再一步步往上一层走进行遍历
优点
- 能找出所有解决方案
- 优先搜索一棵子树,然后是另一棵,所以和广搜对比,有着内存需要相对较少的优点
缺点
- 要多次遍历,搜索所有可能路径,标识做了之后还要取消。
- 在深度很大的情况下效率不高
代码中使用栈+Set实现:
/*** 深度优先搜索算法 DFS(depth-first search)** @param node 访问出发点*/private void dfs(Node node) {Stack<Node> stack = new Stack<>();Set<Node> visitedNode = new HashSet<>();// 访问首顶点stack.push(node);visitedNode.add(node);// 深度优先遍历while (!stack.empty()) {Node cur = stack.pop();for (Node nextNode : cur.getNextNodes()) {if (!visitedNode.contains(nextNode)) {// 有相邻顶点,则将当前顶点压入栈stack.push(cur);// 相邻顶点压入栈stack.push(nextNode);// 标记已被访问过visitedNode.add(nextNode);System.out.println(nextNode.getValue());break;}}}}
基本过程如下:
- 将访问的出发顶点Vi压入栈并标记以访问
- 遍历栈,并pop出顶点Vi,查找此Vi相邻的所有顶点,遍历所有相邻顶点
- 将当前顶点Vi压入栈
- 将当前顶点的相邻顶点Vi1压入栈
- 标记Vi1已被访问过
- 继续遍历栈,pop出顶点Vi1,继续按照以上步骤2循环,直至所有顶点都被访问过
拓扑排序算法(Topological Sorting)
拓扑排序是一个有向无环图(DAG,Directed Acyclic Graph)的所有顶点的线性序列
约束条件:每个顶点出现且只出现一次
将入度为0的顶点进行遍历,将相邻结点的入度减1,并断开当前顶点,然后继续查找,找到下一个入度为0的顶点,以下一个入度为0顶点带入重复如上步骤
通过队列+Map实现:
/*** 拓扑排序(Topological Sorting)** @param graph 有向无环图(DAG,Directed Acyclic Graph)* @return 排序后的集合*/private static List<Node> topologySort(Graph graph) {Queue<Node> zeroInQueue = new LinkedList<>();Map<Node, Integer> inMap = new HashMap<>();// 遍历图的所有顶点,其实就是初始化集合for (Node node : graph.getNodeMap().values()) {// 找出入度为0的顶点并加入zeroInQueue队列if (node.getIn() == 0) {zeroInQueue.add(node);}// 加入入度数map,key为结点,value为入度数inMap.put(node, node.getIn());}// 保存排序结果的集合List<Node> result = new ArrayList<>();// 遍历入度为0的栈while (!zeroInQueue.isEmpty()) {Node cur = zeroInQueue.poll();result.add(cur);// 获取相邻顶点并再次遍历并加入对应集合for (Node nextNode : cur.getNextNodes()) {// 入度减1并加入集合inMap.put(nextNode, inMap.get(nextNode) - 1);// 入度为0的if (inMap.get(nextNode) == 0) {zeroInQueue.add(nextNode);}}}return result;}
基本过程如下:
- 根据graph参数获取所有顶点并加入对应集合和栈
- pop入度为0的结点Vi
- 将顶点Vi加入结果集合result
- 查找Vi所有相邻顶点Vix
- 将相关联的顶点Vix入度减1,如果相关联的顶点入度为0则加入zeroInQueue队列
- 继续判断栈是否为空,不为空继续重复步骤2,直到zeroInQueue队列为空,也就是没有入度为0的顶点了
- 返回最终排序后的结果集合
最小生成树算法 MST(minimum spanning tree)–Prim(普里姆)算法
图的生成树是它的一棵含有其所有顶点的无环连通子图。
其中一幅"加权图的最小生成树"是它的一颗总权值和最小(树中所有边的权值之和)的生成树
基本过程:从某个顶点出发找到该点所有的边,再从中找到最小的边,然后最小边的对应顶点再重复如上步骤循环,直到找到所有顶点
通过优先优先队列+Set实现:
/*** 最小生成树 MST(minimum spanning tree)* 采用的是贪心算法** @param graph 有向无环图(DAG,Directed Acyclic Graph)* @return 最小生成树*/private Set<Edge> prim(Graph graph) {// 排序后的集合Set<Edge> result = new HashSet<>();// 优先队列PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());// 顶点集合Set<Node> nodeSet = new HashSet<>();// 遍历图中的所有顶点for (Node node : graph.getNodeMap().values()) {if (!nodeSet.contains(node)) {// 获取此顶点的所有边并加入优先队列List<Edge> edges = graph.getEdges(node);edges.forEach(e -> priorityQueue.add(e));// 遍历优先队列,权重最小的总是在栈顶while (!priorityQueue.isEmpty()) {Edge edge = priorityQueue.poll();Node to = edge.getTo();// 没有被访问过的顶点if (!nodeSet.contains(to)) {// 再去找所有边并加入优先队列List<Edge> edgeList = graph.getEdges(to);edgeList.forEach(e -> priorityQueue.add(e));nodeSet.add(to);// 加入结果集合result.add(edge);}}}}return result;}private static class EdgeComparator implements Comparator<Edge> {@Overridepublic int compare(Edge o1, Edge o2) {return o1.getWeight() - o2.getWeight();}}
基本过程如下:
- 遍历图中所有的顶点
- 如果顶点Vi没有被访问过则获取Vi的所有边并加入优先队列
- pop优先队列元素(栈顶的是权重最小的边),获取此边对应的顶点Vi1且Vi1没有被访问过
- 获取Vi1所有的边并加入优先队列、标记已被访问、加入结果集合操作
- 当下一次遍历栈时,pop出来的又是权重最小的,再重复2~3步骤…
- 最终都是按照权重最小的边且未被访问过的顶点的路径去走,走出来的路径就是最小生成树
总结
图的表示法:邻接表示法、矩阵表示法、边集数组表示法,这也是最通用的几种表示方法
图的几种算法:深度或广度优先遍历、拓扑排序、最小生成树,不同的算法作用也不同,例如深度优先遍历更加适合找出所有可能的解决方案;广度优先遍历更加适合查找顶点最短路径;
数据结构与算法--图的表示与常用算法相关推荐
- 相似图像识别算法是什么,机器图像识别常用算法
计算图像相似度的算法有哪些 SIM = Structural SIMilarity(结构相似性),这是一种用来评测图像质量的一种方法. 由于人类视觉很容易从图像中抽取出结构信息,因此计算两幅图像结构信 ...
- c语言二级常考算法大全,二级C语言 常用算法.doc
二级C语言 常用算法 C语言常用算法 一.计数.求和.求阶乘等简单算法 此类问题都要使用循环,要注意根据问题确定循环变量的初值.终值或结束条件,更要注意用来表示计数.和.阶乘的变量的初值. 例:用随机 ...
- 人工智能的算法有哪些?AI常用算法
人工智能(AI)是一个非常广泛的领域,其中包含许多不同的算法和技术.以下是一些常见的人工智能算法: 人工智能的算法有哪些? 机器学习(Machine Learning):机器学习是人工智能领域的一个重 ...
- 贪心算法适用条件_五大常用算法之三:贪心算法
一.基本概念: 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解. 贪心算法没有固定的算法框架,算法设计的关键 ...
- 常用的数据结构_动态图展示 6 个常用的数据结构,一目了然
数据结构的确很枯燥,尤其是初学时候,不知道到底有啥用.不过随着编码年限的增长,我们越会发现它真的很有用,巧妙的数据结构是算法高效实现的助推剂. 今天的文章不会用文字和静态图展现常用的数据结构,因为这种 ...
- 算法分支定界法C语言程序,常用算法大全-分枝定界
任何美好的事情都有结束的时候.现在我们学习的是本书的最后一章.幸运的是,本章用到的大部分概念在前面各章中已作了介绍.类似于回溯法,分枝定界法在搜索解空间时,也经常使用树形结构来组织解空间(常用的树结构 ...
- c语言 迷宫深度遍历 算法,图的遍历迷宫生成算法浅析
1. 引言 在平常的游戏中,我们常常会碰到随机生成的地图.这里我们就来看看一个简单的随机迷宫是如何生成. 2. 迷宫描述随机生成一个m * n的迷宫,可用一个矩阵maze[m][n]来表示,如图: ...
- 机器学习算法学习---处理分类问题常用算法(一)
logistic回归是一种广义线性回归(generalized linear model),因此与多重线性回归分析有很多相同之处.它们的模型形式基本上相同,都具有 w'x+b,其中w和b是待求参数,其 ...
- 蓄水池采样算法的python实现_常用算法-蓄水池抽样算法
Leetcode上遇到一道题,题目是这样的: 这道题的关键是链表的长度不知道,但是要使随机返回每个元素的概率相等,这一下就难倒我了,如果知道链表的长度k,从0到k中随机选择一个整数就好了呀,可现在不知 ...
最新文章
- python set没有顺序_Python一题多解学思路:指定列前置
- 小米6自动重启android,小米6充电重启怎么办 小米6充电自动重启解决方法
- SAP UI5 bindProperty
- .net 技术类网址
- libgdx游戏引擎开发笔记(十三)SuperJumper游戏例子的讲解(篇七)----各个物体的创建及其碰撞检测...
- [svc]Linux中Swap与Memory内存简单介绍
- 一天搞懂深度学习—学习笔记2(CNN)
- 用C语言求解一元高次方程论文,一元高次方程C语言实现(最高五次
- 数字经济发展指标体系和测算(含互联网宽带、电话普及率等多指标 内附原始数据) 2011-2020年
- win10的windows聚焦不显示,灰屏解决方案
- 什么是Activity?Activity的生命周期!
- Android后台监听耳机(线控、蓝牙)按键事件
- ACM-ICPC Jiaozuo Onsite 2018 Resistors in Parallel (思维+java大数+找规律)
- Windows Installer:正在安装其他程序。请等待该安装完成,然后再次尝试安装此软件
- 谈B2B电商平台与大数据
- 升级Big Sur系统后指纹解锁出现问题怎么办
- 数据库-----JDBC技术
- java将m3u8转成视频文件
- 【python学习】数据预处理-如何归一化?
- Mac下载SQLServer
热门文章
- 怎么找到服务器的文档,服务器怎么找到数据库
- linux常用的文件操作命令大全,(办公)记事本_Linux常用的文件操作命令
- 如何通过js调用接口
- 捕获系统异常崩溃的方法
- 理解Python的With语句
- Mysql ERROR 2002 (HY000) Can't connect to local MySQL server through socket
- 脆弱的是生命 不脆弱的是精神 雅安 挺住!
- Windows server 2008 R2 个人使用修改==转载+原创
- 流程 - 发布【敏捷方法之Scrum v0.2.pdf】
- 中文分词算法工具hanlp源码解析