有向图、无向图相关数据结构
1. 概述
最近面试的公司是做地图业务相关的,可能在路径规划、路径搜索方面会涉及图相关的知识,因此趁着这个机会把图相关的知识补充整理一下。
如果对图进行大分类的话,主要可以分为有向图和无向图。
所谓有向,指的就是从A点到点B这条路径是成立的,而从点B到点A这条路径不一定成立。而无向图的话,从A可以到B的话,意味着也可以从B到A。
在图里面,使用的数据结构可以有四种:邻接矩阵、邻接表、十字链表、邻接多重表。
这里先给出四种数据结构的优缺点对比:
1. 邻接矩阵来表示图的话比较直观。可以根据每一行来判断每一个顶点出度对应的终点在哪,根据每一列可以判断每一个顶点的入度的起点是谁。如果矩阵里面有比较多的0,那么称为稀疏矩阵。稀疏矩阵里面蕴含的信息太少,需要压缩。
2. 邻接表类似于哈希链表。其中每一个顶点都可以作为一个bucket,然后这个bucket里面是一条链表,链接了以bucket为起点的所有终点的信息。对于有向图来说,这样的一个结构往往只含有一向的信息(出度信息),结点的入度信息需要遍历完整个邻接表才知道,效率太低。对于无向图,则存储的冗余信息太多。比如1->2那么必有2->1,其实只需要一个就行了。
3. 十字链表。十字链表主要是针对于有向图对邻接表的改进。十字链表的思想很简单:对于一个结点,不知道入度信息是吧?那入度信息也给你链接起来!
4. 多重邻接表。多重邻接表主要是针对于无向图对邻接表的改进。多重邻接表的思想很也简单:对于一个结点,重复表示了是吧,那行,结点从顶点变成了边。和这条边有关系的,维度一条链表,把这条边加进去。
1. 邻接矩阵
以上面的有向图构建的邻接矩阵如图所示。矩阵的构建比较简单,这里不在赘述。
需要注意的是,对于顶点2,对应的行全部是0,表示从顶点2出去的边一条都没有。此时如果把这一行进行删除,要用的时候发现2没有对应的行,就可以推断从结点2无法去到任何结点。因此,可以对邻接矩阵进一步进行压缩,节省空间。
2. 邻接表
以上面的有向图构建的邻接表如图所示。其实就是STL里面的链式哈希表。
邻接表的构建也比较简单。从图里面看出来的是对于结点2,相对于邻接矩阵,确实不需要额外的"0"来存储它的出度终点的信息。因为本来是没有,所以用0表示有点消耗空间。
但是,对于邻接表,需要思考的事情是:从顶点1出发,我能知道它能到达结点2,顺着链表,我又知道它可以到达结点3。但是我不知道从哪个结点出发可以到达顶点1。那怎么办,只能遍历全部链表,记录谁能到达结点1(时间复杂度高),又或者是加一条链表,存储全部那些终点是1的结点。但是这样消耗空间(空间复杂度高)。那怎么解决呢?使用十字链表!
3. 十字链表
十字链表可以看成是二维版本的邻接表。
相比于邻接表,它的实现就是给每一个结点加一条链表来存储 ’谁能到达这个结点‘。但是,这条链表的结点不是新生成的,而是利用了邻接表的原始结点(在结点里面加了少量信息)。因此,对于十字链表来寻找 ’能到达谁‘、’谁能到达我‘比较高效而且节省空间的。
要使用十字链表,首先要稍微更改一下邻接表中表(一个表有很多bucket)
和结点的存储信息:
这样看十分的抽象,直接上图:
上面的图显示了构建十字链表的第一步:对于每个节点,使用一条链表来存储以这个节点为起点,所对应终点的节点的信息。
其实就是一个和邻接表相似度达到百分之99相似的链式哈希表
。但是这个数据结构里面还是有些区别:
1. 灰色的表示表信息,但是相对于邻接表,这个灰色的格子多了一个空格出来。这个格子在第二步中是有用的:用来存储’哪些节点能到达这个节点‘。
2. 蓝色的表示节点的信息:但是相对于邻接表,这个蓝色的又多了两个格子,其中一个格子用来存储'起点'了。起点是什么意思? 比如,在上面的邻接表中,1->2的话。那么对于顶点1对应的链表,第一个格子只存了一个2,而现在需要存1和2。
上面的图显示了构建十字链表的第二步(全部画出来太乱,因此把第一步的线全部隐藏了,其实是存在的,而且这个图只画了和节点1有关的"谁能到达节点1"的链表):
在这一步中,第一步中没用到的第二个灰色格子用起来了,指向了一条链表。而且,这条链表的元素代表的都是“谁能到达这个节点”
。因此,在上图中,把全部的表中第二个灰色格子补充完整,就是第二步骤所作的事情。
再总结一下:
- 第一步完成了
我能到达哪个节点。
- 第二部完成了
哪个节点能到达我。
4. 邻接多重表
前面已经分析了邻接多重表主要是在邻接表上对于无向图的改进(存储双份信息,冗余)。因此,把链表中节点的信息换成下面的边信息可以减少这种冗余:
由左边无向图构建的邻接多重表如图所示:
上面的图其实有点像B树(或者B+树)。对于边,一般存储了两个节点的信息,还有两个指针域。
比如,上面1、2形成的边,节点1旁边的指针,其实是指向了从节点1出发可以到达节点几。
顺着这个指针,又可以找到1、3形成的边。这样,使用边信息来代替顶点信息,可以减少信息的冗余表示,节省空间。