图的存储,通俗详细

图,虽然在面试和实际应用中不是那么常见,但是也完全不可以忽视图的重要性哦!

什么是图呢?

图是一种比树更复杂的的数据结构。树描述的是一对多的关系,但是实际生活中的人际关系并不是都像父母与孩子的关系那样,一对父母可以有多个孩子,一个孩子只能有一对父母。所以要引入图来描述多对多的关系模式,比如说我的朋友,他们之间可能也相互认识。

直接上图吧比较直观

如图,把各个顶点看作是人,顶点之间的连线表示朋友关系。假如 B、D、E、F都是A的朋友,C又是B和D的朋友,像这样的许多人构成的一个多对多的关系网就是数据结构中的图(Graph)

图的术语

 图中,蓝色部分是图的顶点(vertex)是最基本的单元,相当于树中的节点。关联两个顶点之间的关系的是(edge)。每一条边并不是完全相同的,就像你的朋友在你心中的分量不一样,这就引出一个新的概念:边的权重(weight)。像上面这样涉及到权重的图就是带权图;如果几个朋友关系都一样好,那么权重可以忽略,这样的图就是无向图;还有可能你认识的人并不认识你,这种有方向的图就是有向图

有向图如下

图的表示

图的基本概念懂了,那如何存储一个图呢?

图的存储方式有很多种,比如邻接矩阵表示法、邻接表表示法、十字链表表示法、邻接多重表表示法,虽然这有四种方式,不过它们成立的原则都是一样的:存入电脑后能根据自身这一种存储方式就能复原图。不像树要根据先序、中序遍历或者中序、后序遍历才能复原。

下面一一讲解这四种存储方式

邻接矩阵

无向图的邻接矩阵

 

 如果一个图有n个顶点,那么这n个最多可以有n(n-1)条连接线。像这样的既要存储顶点,又要存储顶点之间的连线关系,二维数组再合适不过啦。如上图,右图是左图的邻接矩阵表现。先看顶点A和B,它们之间有边关联,那么矩阵中的元素[0][1]和[1][0]的值就是1;再看顶点A和C,它们之间没有边关联,所以[0][3]和[3][0]的值都是0。

需要注意的是,矩阵中左上角到右下角这条对角线上的值都是0。这点很好理解对角线上的点都是顶点自己连接自己,那么肯定就不需要连接线啦。并且矩阵是关于这条对角线对称的,因为这里的图是无向图,可以从A到B,反过来也是可行的。

有向图的邻接矩阵

无向图的邻接矩阵已经搞定了,那么可以举一反三得到有向图的邻接矩阵表示 

有向图的邻接矩阵跟无向图很相似,但是要知道行的含义是以这个顶点为起点的边,列的含义是以这个顶点为终点的边。如上图中A到B有边关联,但B到A没有,所以[0][1]的值是1,,1][0]的值是0,也正因此,矩阵不再具有对称性。

那么问题来啦,如果边上有权重的话,用邻接矩阵该如何表示呢?

带权图的邻接矩阵

 

带权图的邻接矩阵我们要解决的问题就是,权在矩阵矩阵终于任何表示,其他的还是沿用有向图的方法。如上图,A到B有边关联且权值为5,所以[0][1]的值是5;B到A没有边关联,这里用无穷的符号来表示。

三种图的邻接矩阵都已经实现,我们思考一下,如果一个图有n个顶点,那么就需要n*n个单元来存储边,占用的空间太多了。既然这样为什么还会使用这种方式呢?那是因为这种方式比较容易实现图的操作,比如:求某个顶点的度、判断顶点之间是否有边、找顶点的邻接顶点等等。

图的邻接矩阵的实现

需要注意的一点:用来两个数组分别存储顶点表和邻接矩阵 

 #define INFINITY  INT_MAX             //最大值∞#define MAX_VERTEX_NUM    20  //假设的最大顶点数typedef  enum {DG, DN, AG, AN } GraphKind; //有向/无向图,有向/无向网typedef   struct   ArcCell{                VRType     adj; //顶点间关系,无权图取1或0;有权图取权值类型     InfoType   *info;    //该边相关信息的指针} AdjMatrix [ MAX_VERTEX_NUM ] [MAX_VERTEX_NUM ];struct Mgraph{                             //图的定义    VertexType vexs [MAX_VERTEX_NUM ] ; //顶点表,用一维向量即可(n)    AdjMatrix arcs;               //邻接矩阵n*n    int  vernum, arcnum;   //顶点总数n,边总数e    GraphKind kind;    //图的种类标志};       //无向网的构造算法,用数组表示法表示Status CreateUDN(Mgraph &G){    scanf(&G.vexnum, &G.arcnum, &IncInfo);        for(i=0;i<G.vexnum,;++i)      scanf(&G.vexs[i] ); //输入n个顶点值,存入一维向量    for(i=0; i<G.vexnum; ++i) //给邻接矩阵赋初值       for(j=0;j<G.vexnum;++j)           G.arcs[i][j]={INFINITY, NULL};    for(k=0;k<G.arcnum;++k){         scanf(&v1, &v2, &w); //输入边的两顶点以及对应权值     i=LocateVex(G,v1);     j=LocateVex(G,v2); //找到两顶点在矩阵中的位置(n次)     G.arcs[i][j].adj=w; //输入对应权值     if(IncInfo)         Input(*G.arcs[i][j].info); //如果边有信息则填入      G.arcs[i][j] = G.arcs [j] [i];//无向网是对称矩阵  }     return OK;   } // CreateUDN

对于有n个顶点e条边的网,创建网的时间效率是O(n+n2+e*n)

邻接表

为了解决空间效率的问题,所以诞生了邻接表。邻接表是用单链表的形式实现的,就是对每一个顶点创建一个单链表,这个顶点作为链表的头结点,后面链接着这个顶点能够直接到达的相邻接点。

把头结点分成两个域,分别是数据域和链域(指向单链表的第一个结点);头结点后面链接的相邻接点也分成两个域,分别是邻接点域(邻接点是位置)和链域(指向下一个结点)。 

链表左边的数字“0,1,2,3”是顶点的链人顺序,各个边的节点的链入顺序是任意的,所以邻接表不是唯一的!

使用邻接表很容易就能查找到A的邻接点或者A到C的路径。但是如果要查找哪些顶点能够直接到达A,对于上面的有向图只有4个顶点,挨个遍历很快就能够找到,如果要是有100,1000甚至更过的顶点的话效率会很低。

像这样逆向查找,我们可以通过逆邻接表来解决。

逆邻接表实际上就是把邻接表反过来,逆邻接表每一个顶点作为链表的头节点,后面链接的是能够直达到这个顶点的相邻接点。

虽然逆向查找的问题解决啦,但是在实际应用当中,要使用邻接表,还要使用逆邻接表,实现过程很麻烦。这个问题后面的十字链表会帮我们解决,我们先看下图的邻接表用代码如何表示。

图的邻接表的表示

#define MAX_VERTEX_NUM 20  //假设的最大顶点数struct  ArcNode {     //边结构      int  adjvex;         //该边所指向的顶点位置      struct  ArcNode *nextarcs; //指向下一条边的指针      InfoArc    *info;           //该边相关信息的指针      };typedef   struct  VNode{      //顶点结构      VertexType   data;          //顶点信息      ArcNode   * firstarc;   //指向依附该顶点的第一条边的指针}AdjList[  MAX_VERTEX_NUM ];  struct ALGraph{                 //图结构      AdjList   vertics ;         //应包含邻接表      int  vexnum, arcnum; //应包含顶点总数和边总数       int  kind;                   //说明图的种类(用标志)};

十字链表

它是有向图的另一种链式结构,将邻接矩阵用链表存储,是邻接表和逆邻接表的结合

 

 看到这张图第一感觉就是懵,这种东西确实有点麻烦,不过看懂它的结构就很好理解啦。

大致思路就是开设两个结点:顶点结点和边结点,结点结构如下

起点顶点位置

终点顶点位置

终点相同的下一边位置

起点相同的下一边位置

这个是边结点,是用来存储边的信息的结点。把它分成4个域,下面一一解释各个域的存储信息

例如顶点A,以A作为起点的边有两条,一条从A指向B(下文简称边AB),一条从A指向C。既然有两条边就需要有两个边结点来存储边信息,就拿A的第一个边结点来说,它代表的是由A指向B的这条边。这个边结点的第一个域存放的是边的起点A的位置,第二个域存放的是边的终点B的位置,第三个和第四个域存放的都是地址分别指向与边AB终点相同的下一条边的边结点和与边AB起点相同的下一条边的边结点。这样看也不是很难,捋顺了就懂了。

顶点信息

以该顶点为终点的第一条边结点

以该顶点为起点的第一条边结点

这个是顶点结点,存放的是顶点的信息。分为3个域,分别是……上面有就不废话啦,顶点结点与边结点大致相同,不懂的话再捋捋前面的边结点。

上面的例子讲的是有向图的十字链表,那带权图的该怎么办呢?

很简单,给边结点再增加一个域,用来存放权值,问题就解决啦。

十字链表集合了邻接表和逆邻接表,空间复杂度和建表的时间复杂度也与邻接表相同,也很容易操作。既然说到操作啦就来看看十字链表存储结构怎样构建吧。

#define MAX_VERTEX_NUM 20struct  ArcBox {     //边结点结构,5个域    int  tailvex , headvex ;    struct  ArcBox * hlink , tlink;    InfoType    *info;};struct  VexNode{  //顶点结构, 3个域      VertexType   data;      ArcBox   * firstin,*firstout;};struct OLGraph{              //图结构,整体概念      VexNode xlist[  MAX_VERTEX_NUM ];   //表头向量       int  vexnum, arcnum;};

邻接多重表

这是无向图的另一种存储结构,当对边操作是一般采用这种结构存储。前面的十字链表能够理解的话,邻接多重表也没什么问题,他们道理是一样的。

 看到这张图又要怀疑人生啦,那么多的线!不慌,听我娓娓道来,很明显还是开设两个结点:头结点和边结点。

结构如下表:

边依附的顶点i的位置

指向下一条依附顶点i的边位置

边依附的顶点j位置

指向下一条依附顶点j的边位置

 

顶点信息

依附顶点的第一条边结点

举个例子你就明白啦。还是顶点A,A的顶点结点的第二个域存放的是与顶点A相连的第一条边的边结点的地址,也就存储边AB的边结点。边结点有分为4个域,第一个域存储的是“0”也就是A的位置;第三个域存储的是“1”是B的位置;第二个域存放的是与A相关的下一条边的边结点的地址;第四个域存放的是与B相关的下一条边的地址。很绕,结合上面的图多捋几遍还是可以理解的。

邻接多重表把所有的与某一个顶点有关的边都串联在同一个链表中,因为每条边上有两个顶点,所以每个边结点同时链接在两个链表中。

邻接多重表的表示

#define MAX_VERTEX_NUM 20typedef int Status;typedef int InfoType;  //定义弧相关信息为整形typedef char VertexType;    //定义顶点类型为字符型typedef enum {unvisited,visited} VisitIF;    //定义{未访问,已访问}等枚举常量,同时定义VisitIF为枚举变量类型typedef struct ArcNode{     VisitIF mark;             //访问标记     int ivex,jvex;            //该弧所依附的两个顶点在顺序结构中的位置     struct ArcNode *ilink,*jlink;  //分别指向依附这两个顶点的下一条边     InfoType *info;           //该弧相关信息的指针}ArcNode;                     //弧结构typedef struct VNode{     VertexType data;            //顶点     ArcNode *firstarc;          //指向第一条依附该顶点的弧的指针}VNode,AMLList[MAX_VERTEX_NUM]; //顶点结构typedef struct{     AMLList vertices;     //顶点数组     int vexnum,arcnum;    //顶点数和弧数}AMLGraph;                 //多重邻接表表示的图结构

好啦!小伙伴们,今天的分享就到这,喜欢的话可以关注收藏。有什么问题可以在微信公众号私信给我。

明天见喽!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值