首页 > 编程语言 >数据结构与算法 | 图(Graph)

数据结构与算法 | 图(Graph)

时间:2023-11-20 09:57:44浏览次数:38  
标签:node src distSet int Graph map 算法 顶点 数据结构

在这之前已经写了数组链表二叉树栈、队列等数据结构,本篇一起探究一个新的数据结构:图(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研究者 专栏、博客、公众号等。大伙儿的喜欢是创作最大的动力。

标签:node,src,distSet,int,Graph,map,算法,顶点,数据结构
From: https://www.cnblogs.com/jzhlin/p/Graph.html

相关文章

  • 数据结构和迭代器的使用方法
    Java数据结构和迭代器使用方法1. ArrayList(动态数组)创建ArrayList:ArrayList<String>list=newArrayList<>();添加元素:list.add("Element1");list.add("Element2");访问元素:Stringelement=list.get(0);迭代器遍历:Iterator<String>iter......
  • 代码随想录算法训练营第十一天 | ● 20. 有效的括号 ● 1047. 删除字符串中的所有相邻
    今日学习的内容●20.有效的括号varisValid=function(s){letstack=[];for(leti=0;i<s.length;i++){lettemp=s[i];if(temp=='('){stack.push(')')continue;}if(......
  • 【数据结构】数组和字符串(二):特殊矩阵的压缩存储:对角矩阵——一维数组
    4.2.1矩阵的数组表示【数据结构】数组和字符串(一):矩阵的数组表示4.2.2特殊矩阵的压缩存储  矩阵是以按行优先次序将所有矩阵元素存放在一个一维数组中。但是对于特殊矩阵,如对称矩阵、三角矩阵、对角矩阵和稀疏矩阵等,如果用这种方式存储,会出现大量存储空间存放重复信息或零......
  • 【数据结构】数组和字符串(三):特殊矩阵的压缩存储:三角矩阵、对称矩阵——一维数组
    4.2.1矩阵的数组表示【数据结构】数组和字符串(一):矩阵的数组表示4.2.2特殊矩阵的压缩存储  矩阵是以按行优先次序将所有矩阵元素存放在一个一维数组中。但是对于特殊矩阵,如对称矩阵、三角矩阵、对角矩阵和稀疏矩阵等,如果用这种方式存储,会出现大量存储空间存放重复信息或零......
  • GJK算法
    GJK(Gilbert-Johnson-Keerthi)算法背景知识凸多边形定义:对于平面上的一个多边形,如果延长它的任意一条边,使整个多边形都位于延长线的同侧,这样的多边形为凸多边形显然,人可以直观的判断一个多边形是否为凸多边形,那么在程序中,应该如何判断一个多边形是否为凸多边形利用向量的叉......
  • 代码随想录算法训练营第十天 | ● 理论基础 ● 232.用栈实现队列 ● 225. 用队列实现
    今日学习的文章链接和视频链接https://programmercarl.com/栈与队列理论基础.html●232.用栈实现队列varMyQueue=function(){this.stackIn=[];this.stackOut=[]};/***@param{number}x*@return{void}*/MyQueue.prototype.push=funct......
  • 在Spring Boot中实现GraphQL
    1什么是GraphQLGraphQL是一种API查询语言,由Facebook开发,用于提供灵活、高效的API接入。它允许客户端准确指定需要的数据,而不是获取预设的REST接口。2优势灵活的查询方式,请求特定字段,无过度获取强类型,类型安全单一端点,避免过多端点内置Documentation...3Spring......
  • 算法:约瑟夫环问题
    问题描述:n个人围成一圈,从编号为k的人开始报数,报到m的人出圈,剩下的人继续从1开始报数,报到m的人出圈;如此往复,求最后一个出圈的人 /**@paramarrarray值为range(1,总人数)*@parammint报号到m的人出圈*@paramcurrentint从第current+1个人开始喊1;值为k-1*@return......
  • 算法工程师的工作内容和岗位技能要求
    算法工程师是一种专注于设计、开发和实施算法的职位,他们的工作主要涉及到使用先进的数学和编程技术去解决复杂的问题。这个职位在许多行业中都有广泛的应用,包括但不限于人工智能(AI)、机器学习(ML)、数据科学、电信、金融、生物医学、物理等。在这篇文章中,我们将详细介绍算法工程师的......
  • RSA算法基础
    RSA算法的必要性密码学是一门保密通信技术,它将明文信息按双方约定的法则转换成只有特定人群才能看懂的密文以保证信息的安全传输。这样即使接收者之外的人得到传递的密文,也不知道信息的真正内容,从而达到安全传递信息的目的。古典密码学和近代密码学一般是通过转译和反转译的方法......