数据结构 图论

本文介绍了图论的基础概念,包括图的定义、种类和基本术语,以及深度优先遍历(DFS)和广度优先遍历(BFS)两种图遍历方法。此外,文章还探讨了最小生成树的Prim和Kruskal算法,以及最短路径问题的Dijkstra和Floyd算法。图论在地图导航、互联网、任务调度和社会网络等领域有广泛应用。
摘要由CSDN通过智能技术生成


0.引入

0.1身边的图

在生活中,随处可见与图相关的应用:

  • 地图:在使用地图中,经常会想知道“从xx到xx的最短路线”这样的问题,要回答这些问题,需要把地图抽象成一个图(Graph),十字路口就是顶点,公路就是边。
  • 互联网: 整个互联网其实就是一张图,它的顶点为网页,边为超链接,图论可以帮我们在网络上定位信息。
  • 任务调度:当一些任务拥有优先级限制且需要满足前置条件时,如何在满足条件的情况下用最少的时间完成任务就需要用到图论。
  • 社交网络(小世界理论):在使用社交网站时,个人就是一个顶点,你和你的朋友建立的关系则是边,分析社交网络的性质,通过广度优先搜索算法,来实现好友推荐,给用户推荐二度或者三度好友。

0.2为什么用图

Q:为什么要有图?
A:数据结构有线性表和树等,存在即可合理,为什么仍要引入图?不妨考虑下线性表等和图的局限性及优势。线性表仅局限于一个直接前驱和一个直接后继的关系。树也仅有一个直接前驱也就是父节点。那多对多的关系怎么处理? 这里就用到了图。图由一组顶点和一组将两个顶点相连的边组成,顶点表示对象或事物,顶点间的边表示对象间或事物间的关联关系,图本质是为了研究事物间的联系。


1.图论概述

1.1图论起源:七桥问题(欧拉路径、一笔画问题)

在这里插入图片描述

图1 七桥问题示例图

Q:如何在一场散步中走过全城各地区,七座桥中的每一座都必须恰好走过一次?
A:1735年8月26日,欧拉向圣彼得堡科学院提交了一篇论文,发表了对该问题的看法。为了抓住问题本质,他按照经典的数学思想,只把必要的信息提取出来,从而解决了问题,该问题无解!同时也开辟出了一个新的数学分支—图论(graph theory)

  • 首先,欧拉抛开问题的具体背景,将城镇、河流、桥梁抽象出来,分别用大写字母A、B、C、D表示4个区域,
  • 再用小写字母a~g表示七座桥,
  • 把路线视为字符串,而不是行人的实际散步路线,A、B、C、D 4个区域的面积与论证完全无关,把它们都抽象成一点,而把a到g的桥梁抽象成连接两点的线,从而把题目形象化,画成一张更简单的图,如图2所示。
  • 所以,用11个字母可描述城里的任何一条路线,比如一条路线可以描述为:从A地经过c桥到达C地,再从C地经过g桥到达D地,从D地经过f桥到达B地,最后从B地经过b桥回到A地。这条路线也可以简记为AcCgDfBbA。
    在这里插入图片描述
图2 七桥抽象图

1.2图论知识点总览

  • 图论知识丰富,按基础知识和引申知识分类如下:
    在这里插入图片描述
图3 图论知识总览

补充说明:

  • 图可以是自然而成的图,如"城市交通网络"、互联网"、社交网络"、“社会关系”、"蛋白子分子结构"等
  • 也可以人为构造图来解决问题,将相关问题抽象成图论的问题,使用图论相关方法去解决,如使用相似度度量对象之间是否相似来构造对象之间的边,如商品名称归一,"立白洗衣粉"与"洗衣粉"相似,"雕牌洗衣粉"也与"洗衣粉"相似(基于字符串相似度量,如字串、最长公共子序列、最长公共子字符串)
  • 案例:“立白洗衣粉”、“雕牌洗衣粉”、"洗衣粉"作为点,它们之间是否相似作为它们之间是否存在边,"立白洗衣粉"与"洗衣粉"之间有边连接,"雕牌洗衣粉"与"洗衣粉"之间也有边连接,“立白洗衣粉"归集到"洗衣粉”,“雕牌洗衣粉"也归集到"洗衣粉”。这样同类的商品可以归集到一个粒度较为适中的粒度上(字符相似),可以大幅度降低商品个数,从而在某些任务上降低计算量,优化效率。

2.图基本概念

2.1图的定义

  • 图G是由点与边构成,为一个二元组(V(G),E(G)),
  • V(G)={v1,v2,…,vi,…,vj,…vn}是图G的顶点集合,它是一个非空集合。
  • E(G)={(vi,vj)}是图G的边集合,每条边<vi,vj>是一个有序或者无序的二元对,表示顶点vi与顶点vj之间的边,如果边上有权值,则使用<vi,vj,wij>表示顶点vi与vj之间的边,wij表示该条边的权值。
  • |V(G)|表示图G的顶点个数,即图的阶,|E(G)|表示图G的边的个数。

2.2图的种类

  • 边是否有方向
    • 有向图:边的两个端点是关系对称的,如QQ中的好友关系,A与B是好友,则A与B之间有边连接,并且A与B关于好友这个关系是对称的,或者说关系是无序的,A是B的好友,B也是A的好友,又如城市间的铁路网,一般A城市到B城市有直达列车,那么B也有到A的直达列车。
    • 无向图:边的二元对是有序二元对,即关系是有序的,往往表示该条边上两顶点在该边的关系上不对称。如微博中的关注,显然也是一个有向图,你关注大V,大V不一定关注你。
    • 混合图:不仅存在有向边,又存在无向边的图
      在这里插入图片描述
图2.1 有向图vs无向图
  • 边是否带权值

    • 无权图,边没有权重,如城市之间,如果只关注是否存在直达交通;
    • 有权图,边带有权重,关注直达交通的时间、花费、路径长度。
  • 图是否连通

    • 连通图,任意2个顶点之间都存在路径
    • 非连通图 ,存在顶点之间无路径,如图2.2,存在连通子图,{0,1,2,3,4} 为最大连通子图(节点数最多),也称连通分量
      在这里插入图片描述
    图2.2 非连通图
  • 图是否存在自环和平行边

    • 简单图,不存在自环和平行边
    • 多重图 ,存在自环和平行边

2.3图的基本术语

  • 顶点的度
    • 无向图,顶点的度d(v)入度等于出度,是其邻接点的数目,即与之相连的边的数目;
    • 有向图,每个顶点有入度d_in(v)与出度d_out(v)之分,入度便是以其为终点的边的数目,就是到达该点的边的数目(引入的),出度便是以其为起点的边的数目,该点出去的边的数目(引出的)。
    • 点的度之和为边的2倍,无向图:sum(d(vi))=2*|E(G)|;有向图:sum(d_in(vi)+d_out(vi))=2*|E(G)|
    • 一个图中度(有向图为入度与出度之和)为奇数的点的个数是偶数,因为2*|E(G)|是偶数,那么不存在奇数个奇数与偶数之和为偶数。

2.4图的表示

  • 邻接矩阵: 使用一个V * V的布尔矩阵。当顶点v和顶点w之间有相连接的边时,将v行w列的元素设为true,否则为false。当图的顶点非常多时,邻接矩阵所需的空间将会非常大。且它无法表示平行边。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图5 邻接矩阵
  • 边的数组:使用一个Edge类,它含有两个int成员变量来表示所依附的顶点。这种方法简单直接,但无法实现查询邻接点的函数需要检查图中的所有边。
  • 邻接表数组:使用一个顶点为索引的链表数组,其中的每个元素都是和该顶点相邻的顶点列表(邻接点)
    在这里插入图片描述
    在这里插入图片描述
图6 邻接表

邻接表实现如下:

问题:随机生成一个图(可以是有向图或是无向图),图的顶点大概100个左右,若是有向图则边大概2000条左右,若是无向图则边大概1000条左右!并计算出边的入度和出度

代码:
1、Graph类

public class GraphRandom {

    VertexRandom[] vertexArray=new VertexRandom[200];
    int verNum=0;
    int edgeNum=0;
}

2、Vertexl类

public class VertexRandom {

    int verName;
    int inRadius,outRadius;
    VertexRandom nextNode;
}

3、随机图实现类

public class CreateGraph2 {

    /**
     * 由顶点名称返回顶点集合中的该顶点
     * @param graph 图
     * @param name 顶点名称
     * @return返回顶点对象
     */
    public VertexRandom getVertex(GraphRandom graph,int name){
        for(int i=0;i<graph.verNum;i++){
            if(graph.vertexArray[i].verName==name){
                return graph.vertexArray[i];
            }
        }
        return null;
    }

    /**
     * 该顶点通过顶点和边构建图
     * @param graph 图
     */
    public void randomSpanning(GraphRandom graph){
        @SuppressWarnings("resource")
        Scanner scan=new Scanner(System.in);
        System.out.println("请输入随机图的顶点个数:");
        graph.verNum=scan.nextInt();

        for(int i=1;i<=graph.verNum;i++){
            VertexRandom vertex=new VertexRandom();
            vertex.verName=i;
            vertex.nextNode=null;
            graph.vertexArray[i-1]=vertex;
        }

        int number=(int)(Math.random()*200+1000);
        System.out.println("随机生成的边的数量为:"+number);
        graph.edgeNum=number;

        for(int i=0;i<graph.edgeNum;i++){
            int preV=(int)(Math.random()*100+1);
            int folV=(int)(Math.random()*100+1);
            while(folV==preV){
                folV=(int)(Math.random()*100+1);
            }

            VertexRandom vertex1=getVertex(graph,preV);
            if(vertex1==null)
                System.out.println("随机图中没有该顶点!");
            VertexRandom vertex2=new VertexRandom();
            vertex2.verName=folV;
            vertex2.nextNode=vertex1.nextNode;
            vertex1.nextNode=vertex2;

//          下面用于计算顶点的出度和入度的
            vertex1.outRadius++;
            VertexRandom v2=getVertex(graph,folV);
            if(v2==null)
                System.out.println("随机图中没有该顶点!");
            v2.inRadius++;

//          加上下面代码用于产生无向图
//          VertexRandom reVertex2=getVertex(graph,folV);
//          if(reVertex2==null)
//              System.out.println("随机图中没有该顶点!");
//          VertexRandom reVertex1=new VertexRandom();
//          reVertex1.verName=preV;
//          reVertex1.nextNode=reVertex2.nextNode;
//          reVertex2.nextNode=reVertex1;
        }
    }

    /**
     * 输出图的邻接链表
     * @param graph 图
     */
    public void outputGraph(GraphRandom graph){
        System.out.println("输出图的邻接链表为:");
        for(int i=0;i<graph.verNum;i++){
            VertexRandom vertex=graph.vertexArray[i];
            System.out.print(vertex.verName);

            VertexRandom current=vertex.nextNode;
            while(current!=null){
                System.out.print("-->"+current.verName);
                current=current.nextNode;
            }
            System.out.println();
        }
    }

    /**
     * 输出图的入度和出度
     * @param graph 图
     */
    public void IORadius(GraphRandom graph){
        for(int i=0;i<graph.verNum;i++){
            System.out.print("顶点"+(i+1)+"的出度为:"+graph.vertexArray[i].outRadius);
            System.out.println(",入度为:"+graph.vertexArray[i].inRadius);
        }
    }

    public static void main(String[] args) {
        GraphRandom graph=new GraphRandom();
        CreateGraph2 createGraph2=new CreateGraph2();
        createGraph2.randomSpanning(graph);
        createGraph2.outputGraph(graph);
        createGraph2.IORadius(graph);
    }
}

每种图实现的性能复杂度如下表:

DS 空间 添加边 检查两节点是否相邻 遍历所有节点
边数组 E 1 E E
邻接矩阵 V^2 1 1 V
邻接表 E+V 1 deg(V) deg(V)
  • 分析空间复杂度
    • 邻接矩阵显然需要 O(|V|^2),邻接表需要 O(|V| + |E|)。
    • 当T = 1,000,000时,即使邻接矩阵的每一个单元格仅占1字节,那么也需要1TB才能将数组存下。
    • 考虑极限情况,当图的边变密到极限的时候有: ∣ V ∣ ( ∣ V ∣ − 1 ) / 2 = ∣ E |V|(|V|-1)/2=|E V(V1)/2=E,此时邻接表的空间开销与邻接矩阵相当,均为 O(|V|^2)。满足此种情况下的图被称为完全图,而当上述关系大致成立时,图被称为致密图,空间复杂度也大致相当。当|E|远不能达到致密状态时,我们称之为稀疏图。
  • 分析时间开销
    • 确认某两个顶点是否有边相连;

      • 对于邻接表,我们需要访问目标节点下的整个链表来确认它们是否相连,时间复杂度O(|V|);
      • 对于邻接矩阵,则直接询问二维数组即可,时间开销为常数O(1);
    • 访问一个顶点的所有边(搜索);

      • 对于邻接表,只需访问链表的长度;
      • 而对于邻接矩阵,需要完全访问某一行/某一列才可以。
      • 做一次广搜遍历,邻接表的复杂度为O(|V| + |E|),
      • 而邻接矩阵为O(|V|^2)。

从上述计算我们可以看出,邻接表和邻接矩阵只有在特定条件下才有高低之分。大家一定要结合实际情况选择表达方式。

总结:

  • 如果你的图是一张庞大的图,并且是稀疏的(|E(G)|<<|V(G)|*|V(G)|),如微博的关注网络,毕竟大v只是少数,大部分人的关注量很少(长尾)。这样邻接矩阵对于很多不存在边的顶点之间也使用了存储空间,邻接矩阵有大量的0元素,即邻接矩阵M是稀疏矩阵,这样会浪费很大的空间,甚至造成爆内存问题。而使用邻接表只需要存储节点与存在的边,存储空间降低了很多。
  • 如果你需要快速访问两个顶点之间是否存在边,则邻接矩阵更好,如需要判断上图中v1与v3之间是否存在边,则邻接矩阵直接取第1行第3列的元素M13是否为0进行判断。而对于邻接表,则需要遍历v1的邻居链表来进行判断。其实,为了便于边存在判断,对邻接表进行改造也可以加快速度,那便是基于Hash表,每个节点的邻居链表使用Hash来存储,Hash表访问的理论时间复杂度是O(1)。
  • 如果你的问如果你的问题是对图进行遍历(遍历需求),那么邻接表则更好,因为它不会存储无用的信息,存储方式符合遍历的需求。
  • 在平时使用中,如果你的图很小(顶点个数少),或者很稠密,甚至是完全图,那么可以优先考虑邻接矩阵。但是大部分真实场景下,优先使用邻接表或者改造的邻接表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值