题图:图数据库在数据治理中的应用(来源:百度安全-HugeGraph)
前言
前面的一些章节我们介绍了图数据的概念、应用场景和建模,主要还是偏向于业务和应用方面。后面这些章节,我们将从技术方面做一些展开,学识有限,有写的不对和不周到的地方请大家多批评指正,感谢。
今天,我们先来回顾一下图的主要存储方式。
常用的存储结构有两种,即顺序存储结构(顺序表)和链式存储结构(链表)。顺序表的特点是把逻辑上相邻的结点存储在物理位置上相邻的存储单元中,结点之间的逻辑关系由存储单元的邻接关系来体现。而在链表中,逻辑上相邻的数据元素,物理存储位置不一定相邻,元素之间的逻辑关系用指针实现。另外,顺序表的存储空间需要预先分配,链表的存储空间是动态分配的。
在线性表中,数据元素之间是被串起来的,仅有线性关系,每个元素只有一个直接前驱和直接后继。所以线性表既可以通过顺序表存储,也可以通过链表存储,而具体选择哪种,需要考虑很多因素,例如语言环境、存储空间、计算时间等等。
在树形结构中,数据元素之间有了层次关系,并且只能和上一层的一个元素相关,可能和下一层的多个元素相关。用简单的顺序表和链表都不能实现,但是将两者结合起来就能很好的实现了。常用的方法有三种,分别是双亲表示法(节点中保存双亲位置)、孩子表示法(多重链表)、还在兄弟表示法(两个指针分别指向第一个孩子和右兄弟)。
而图比线性表和树更加复杂,图中的任意两个元素都可能存在关联,所以图的存储结构也更加复杂。下面将分别介绍下图的几种存储结构。
1、邻接矩阵
1.1、定义
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图,一个一维数组存储顶点信息,一个二维数组(成为邻接矩阵)存储边的信息。
设图G=(V, E),其中V是图G中顶点的集合,E是图G中边的集合,通常用(vi, vj)表示无向边,用表示有向边。如果V中有n个顶点,则邻接矩阵是一个n * n 的方阵,定义如下:
图一:邻接矩阵的定义
1.2、存储表示
首先分别看下无向图和有向图的存储实例:
图二:无向图的邻接矩阵表示
图三:有向图的邻接矩阵表示
通过两张图的对比分析,想必大家已经有了一个比较清晰的认知。这里我简单做下说明和总结:无向图的邻接矩阵是一个对称矩阵
无向图中某个顶点的度,就是这个顶点vi在邻接矩阵第i行(或者第i列)上的元素之和
有向图中某个顶点vi的入度,就是邻接矩阵中第i列的元素之和;vi的出度就是第i行的元素之和
不论在无向图还是有向图中,求vi的所有邻接点都是将矩阵第i行的元素遍历一遍,查找arc[i][j]为1的顶点
1.3、优缺点优点:
直观、容易理解,可以很容易的判断出任意两个顶点是否有边,很容易计算出各个顶点的度。缺点:
对于边数较少,顶点比较多的稀疏矩阵,会造成很严重的空间浪费。
2、邻接表
2.1、定义
邻接表是一种数组与链表相结合的存储方法。邻接表的处理方法如下:图中的顶点用一个一维数组存储,也可以用单链表,只是用数组可以更容易的读取顶点信息。另外,在顶点数组中,每个数据元素需要存储一个指向第一个邻接点的指针。可以表示为 :| data | firstedge | 。
图中每个顶点vi的所有邻接点组成一个线性表,因为个数不定,用单链表存储。每个节点可以表示为:| adjvex | next |。
2.2、存储表示
我们再来看下,无向图和有向图的邻接表存储实例:
图四:无向图的邻接表表示
图五:有向图的邻接表表示
通过两张图片,对于邻接表的存储也比较清楚了。我同样做一些简单的总结。无向图中查询某个节点的度比较简单,直接计算次顶点对应的边表中节点的个数即可;但是在有向图(一般是把顶点作为弧尾来存储边的数据)中出度很容易计算,入度不好计算。
有向图中如果对于入度是强需求,可以使用逆邻接表来存储。
判断两个顶点vi和vj是否存在关联,只需要只需要到vi的边表中 adjvex域是否存在下标j即可。
2.3、优缺点优点: 对于,稀疏图,邻接表比邻接矩阵更节约空间。
缺点: 邻接表和逆邻接表分别擅长出度和入度的计算,对于入度和出度都需要遍历整个邻接表。
3、十字链表
3.1、定义
因为邻接表和逆邻接表的缺陷,我们很自然的就想到可以将两者结合起来,这就是十字链表。十字链表中,顶点表的结构为: | data | firstin | firstout |。其中firstin为入边表的头指针,firstout为出边表的头指针。
边表节点的结构为:| tailvex | headvex | headlink | taillink |。tailvex和headvex分别是指弧尾和弧头在顶点表的下标,headlink和taillink分别指终点相同和起点相同的下一条边。
3.2、存储表示
图六:有向图的十字链表表示
边表节点的定义中headlink和taillink容易让人疑惑。我们按照图中的示例来深化下理解。首先。headlink指终点相同的下一条边,看图中的V1,V1的边表中头指针是 | 1 | 0 | 圈2 | taillink |。其中圈2按照定义,是指向终点相同的下一条边,就是有向图中V2--> V0的边;而taillink指向起点相同(V1)的下一条边,起点相同的下一条边是 V1 --> V2,也就是 | 1 | 2 | -headlink | taillink |。
3.3、优缺点优点: 整合了邻接表和逆邻接表,顶点的出度和入度都很容易计算。
缺点: 数据结构相对复杂。
4、邻接多重表
4.1、定义
十字链表是针对有向图的存储优化,而针对无向图(对边的操作更多时)的存储优化就是邻接多重表。和邻接表一样,顶点表的结构可以表示为 :| data | firstedge | 。
边表节点的结构为:| ivex | ilink | jvex | jlink |。和十字链表类似,ivex和jvex是指某条边依附的两个顶点在顶点表的下标,headlink和taillink分别指终点相同和起点相同的下一条边。ilink和jlink分别指向依附顶点ivex和jvex的下一条边。
4.2、存储表示
图七:无向图的邻接多重表表示
5、边集数组
5.1、定义
边集数组用两个一维数组来存储图的,其中一个存储顶点,另外一个存储边。顶点数组不需要额外的指针,而边的数组中存储结构为 | begin | end | weight |,begin和end分别表示边的起点和终点,weight是权重。
5.2、存储表示
图八:有向图的边集数组表示
5.3、优缺点优点: 适合对边进行依次处理
缺点: 计算入度和出度需要扫描全表,效率不搞
6、预告
由于篇幅很大,分成两篇。
下一篇将主要介绍几个开源的图数据库项目,对应的存储方式。敬请期待,也鞭策我自己,好好看代码,认真做总结。
参考:
1、《大话数据结构》第七章,示例图直接截取自本书