什么是图?

图(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)

优先广度遍历,一层遍历完成后再遍历下一层

优点

  1. 对于解决最短或最少问题特别有效,而且寻找深度小
  2. 每个结点只访问一遍,结点总是以最短路径被访问,所以第二次路径确定不会比第一次短

缺点

  1. 内存耗费量大(需要开大量的数组单元用来存储状态)

代码中使用队列+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);}}}}

基本过程如下:

  1. 访问出发点Vi
  2. 访问Vi所有未被访问过的邻接点Vi1、Vi2、Vi3…,并将它们标记为已访问过(添加到visitedNode集合)
  3. 然后再按照Vi1、Vi2、Vi3…的次序访问每一个顶点并获取此顶点未访问过的邻接点Vi11、Vi12、Vi13…,以此类推,直到遍历到最后一层

深度优先搜索算法 DFS(depth-first search)

深度优先遍历,走到最深的一层,先把深的遍历完成再一步步往上一层走进行遍历

优点

  1. 能找出所有解决方案
  2. 优先搜索一棵子树,然后是另一棵,所以和广搜对比,有着内存需要相对较少的优点

缺点

  1. 要多次遍历,搜索所有可能路径,标识做了之后还要取消。
  2. 在深度很大的情况下效率不高

代码中使用栈+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;}}}}

基本过程如下:

  1. 将访问的出发顶点Vi压入栈并标记以访问
  2. 遍历栈,并pop出顶点Vi,查找此Vi相邻的所有顶点,遍历所有相邻顶点
    • 将当前顶点Vi压入栈
    • 将当前顶点的相邻顶点Vi1压入栈
    • 标记Vi1已被访问过
  3. 继续遍历栈,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;}

基本过程如下:

  1. 根据graph参数获取所有顶点并加入对应集合和栈
  2. pop入度为0的结点Vi
    • 将顶点Vi加入结果集合result
    • 查找Vi所有相邻顶点Vix
    • 将相关联的顶点Vix入度减1,如果相关联的顶点入度为0则加入zeroInQueue队列
  3. 继续判断栈是否为空,不为空继续重复步骤2,直到zeroInQueue队列为空,也就是没有入度为0的顶点了
  4. 返回最终排序后的结果集合

最小生成树算法 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();}}

基本过程如下:

  1. 遍历图中所有的顶点
  2. 如果顶点Vi没有被访问过则获取Vi的所有边并加入优先队列
  3. pop优先队列元素(栈顶的是权重最小的边),获取此边对应的顶点Vi1且Vi1没有被访问过
    • 获取Vi1所有的边并加入优先队列、标记已被访问、加入结果集合操作
  4. 当下一次遍历栈时,pop出来的又是权重最小的,再重复2~3步骤…
  5. 最终都是按照权重最小的边且未被访问过的顶点的路径去走,走出来的路径就是最小生成树

总结

图的表示法:邻接表示法、矩阵表示法、边集数组表示法,这也是最通用的几种表示方法
图的几种算法:深度或广度优先遍历、拓扑排序、最小生成树,不同的算法作用也不同,例如深度优先遍历更加适合找出所有可能的解决方案;广度优先遍历更加适合查找顶点最短路径;

数据结构与算法--图的表示与常用算法相关推荐

  1. 相似图像识别算法是什么,机器图像识别常用算法

    计算图像相似度的算法有哪些 SIM = Structural SIMilarity(结构相似性),这是一种用来评测图像质量的一种方法. 由于人类视觉很容易从图像中抽取出结构信息,因此计算两幅图像结构信 ...

  2. c语言二级常考算法大全,二级C语言 常用算法.doc

    二级C语言 常用算法 C语言常用算法 一.计数.求和.求阶乘等简单算法 此类问题都要使用循环,要注意根据问题确定循环变量的初值.终值或结束条件,更要注意用来表示计数.和.阶乘的变量的初值. 例:用随机 ...

  3. 人工智能的算法有哪些?AI常用算法

    人工智能(AI)是一个非常广泛的领域,其中包含许多不同的算法和技术.以下是一些常见的人工智能算法: 人工智能的算法有哪些? 机器学习(Machine Learning):机器学习是人工智能领域的一个重 ...

  4. 贪心算法适用条件_五大常用算法之三:贪心算法

    一.基本概念: 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解. 贪心算法没有固定的算法框架,算法设计的关键 ...

  5. 常用的数据结构_动态图展示 6 个常用的数据结构,一目了然

    数据结构的确很枯燥,尤其是初学时候,不知道到底有啥用.不过随着编码年限的增长,我们越会发现它真的很有用,巧妙的数据结构是算法高效实现的助推剂. 今天的文章不会用文字和静态图展现常用的数据结构,因为这种 ...

  6. 算法分支定界法C语言程序,常用算法大全-分枝定界

    任何美好的事情都有结束的时候.现在我们学习的是本书的最后一章.幸运的是,本章用到的大部分概念在前面各章中已作了介绍.类似于回溯法,分枝定界法在搜索解空间时,也经常使用树形结构来组织解空间(常用的树结构 ...

  7. c语言 迷宫深度遍历 算法,图的遍历迷宫生成算法浅析

    1. 引言 在平常的游戏中,我们常常会碰到随机生成的地图.这里我们就来看看一个简单的随机迷宫是如何生成. 2. 迷宫描述随机生成一个m * n的迷宫,可用一个矩阵maze[m][n]来表示,如图:   ...

  8. 机器学习算法学习---处理分类问题常用算法(一)

    logistic回归是一种广义线性回归(generalized linear model),因此与多重线性回归分析有很多相同之处.它们的模型形式基本上相同,都具有 w'x+b,其中w和b是待求参数,其 ...

  9. 蓄水池采样算法的python实现_常用算法-蓄水池抽样算法

    Leetcode上遇到一道题,题目是这样的: 这道题的关键是链表的长度不知道,但是要使随机返回每个元素的概率相等,这一下就难倒我了,如果知道链表的长度k,从0到k中随机选择一个整数就好了呀,可现在不知 ...

最新文章

  1. python set没有顺序_Python一题多解学思路:指定列前置
  2. 小米6自动重启android,小米6充电重启怎么办 小米6充电自动重启解决方法
  3. SAP UI5 bindProperty
  4. .net 技术类网址
  5. libgdx游戏引擎开发笔记(十三)SuperJumper游戏例子的讲解(篇七)----各个物体的创建及其碰撞检测...
  6. [svc]Linux中Swap与Memory内存简单介绍
  7. 一天搞懂深度学习—学习笔记2(CNN)
  8. 用C语言求解一元高次方程论文,一元高次方程C语言实现(最高五次
  9. 数字经济发展指标体系和测算(含互联网宽带、电话普及率等多指标 内附原始数据) 2011-2020年
  10. win10的windows聚焦不显示,灰屏解决方案
  11. 什么是Activity?Activity的生命周期!
  12. Android后台监听耳机(线控、蓝牙)按键事件
  13. ACM-ICPC Jiaozuo Onsite 2018 Resistors in Parallel (思维+java大数+找规律)
  14. Windows Installer:正在安装其他程序。请等待该安装完成,然后再次尝试安装此软件
  15. 谈B2B电商平台与大数据
  16. 升级Big Sur系统后指纹解锁出现问题怎么办
  17. 数据库-----JDBC技术
  18. java将m3u8转成视频文件
  19. 【python学习】数据预处理-如何归一化?
  20. Mac下载SQLServer

热门文章

  1. 怎么找到服务器的文档,服务器怎么找到数据库
  2. linux常用的文件操作命令大全,(办公)记事本_Linux常用的文件操作命令
  3. 如何通过js调用接口
  4. 捕获系统异常崩溃的方法
  5. 理解Python的With语句
  6. Mysql ERROR 2002 (HY000) Can't connect to local MySQL server through socket
  7. 脆弱的是生命 不脆弱的是精神 雅安 挺住!
  8. Windows server 2008 R2 个人使用修改==转载+原创
  9. 流程 - 发布【敏捷方法之Scrum v0.2.pdf】
  10. 中文分词算法工具hanlp源码解析