大家好,很高兴又和大家见面啦!!!
经过前面的内容,我们已经学习了图的三种存储结构:
在今天的内容中我们将会介绍图的第四种存储结构以及图的一些基本操作。下面我们直接进入今天的内容;
在有向图中,我们可以通过3种存储结构来存储有向图的顶点与弧的信息,并且这三种存储结构各有其优缺点:
相较于邻接矩阵与邻接表,十字链表不仅提高了弧的查找效率,还节省了存储空间。
但是十字链表法并不能像邻接矩阵和邻接表一样不仅可以存储有向图,还可以存储无向图,十字链表法只能够存储有向图。
那对于无向图而言,有没有一种存储结构既能够提高边的查找效率,又能够节省存储空间呢?
在邻接表中,容易求得顶点和边的各种信息,但求两个顶点之间是否存在边儿执行删除边等操作时,需要分别在两个顶点的边表中遍历,效率低。
邻接多重表(Adjacency Multilist)是无向图的一种链式存储结构。与十字链表类似,在邻接多重表中,每条边用一个结点表示,其结构如下所示:
graph TB
a[ivex]
b[ilink]
c[jvex]
d[jlink]
e[info]
其中:
ivex
与jvex
中存储的是该边依附的两个顶点编号;ilink
指向的时依附于顶点 i
的下一条边jlink
指向的时依附于顶点 j
的下一条边info
中存放的时该边的相关信息,如边的权值每个顶点也用一个结点表示,它由两个域组成:
data
域存放该顶点的相关信息firstedge
域指向的时依附于该顶点的第一条边在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,因为每条边依附于两个顶点,所以每个边结点同时链接在两个链表中。
在无向图中,其邻接表与邻接多重表的差别在于——同一条边在两个表中的结点数量不同:
邻接多重表的存储结构中,同样需要定义两种结点类型:
#define MAXSIZE 5
typedef int Edge_ElemType; // 边信息数据类型
typedef int Vert_ElemType; // 顶点信息数据类型
typedef struct Edge_Node {
int ivex; // 顶点i的编号
int jvex; // 顶点j的编号
struct Edge_Node* ilink, * jlink; // 依附于顶点i与顶点j的边
Edge_ElemType info; // 边的信息
}ENode; // 边结点
typedef struct VertexNode {
Vert_ElemType data; // 顶点信息
ENode* firstedge; // 依附于顶点的第一条边的结点
}VNode; // 顶点结点
typedef struct Adjacency_Multilist {
VNode vert_list[MAXSIZE]; // 顶点表
int edge_num; // 边的数量
int vert_num; // 顶点数量
}AMGraph; // 邻接多重表
当图中的边没有权值时,可以省略边结点中的info
域;
邻接多重表的算法评价同样以邻接多重表的遍历进行评价;
在邻接多重表中,遍历所有顶点就是遍历一个顺序表,对于顶点数为 |V| 的图,其时间复杂度为 O(|V|);
遍历所有边只需要将每一条边都遍历一次,对于边数为 |E| 的图,其时间复杂度为 |E|;
整个图的遍历对应的时间复杂度为 T(N) = O(|V| + |E|);
在邻接多重表中,对于顶点数为 |V| ,边数为 |E| 的图而言,其需要的空间复杂度为 T(N) = O(|V| + |E|);
下面我们会从五个维度来探讨这四种存储方式:
在邻接矩阵中,对于结点数为 n 的图,不管是有向图还是无向图都需要申请 n^2 的空间,因此其空间复杂度均为 n^2 ;
在邻接表中,对于结点数为 |V| 和边数为 |E| 的图,在有向图和无向图中,其空间复杂度并不相同:
在十字链表中,对于结点数为 |V| 和边数为 |E| 的图,需要申请同等数量的结点空间和边空间,其对应的空间复杂度为:O(|V| + |E|);
在邻接多重表中,对于结点数为 |V| 和边数为 |E| 的图,需要申请同等数量的结点空间和边空间,其对应的空间复杂度为:O(|V| + |E|);
在邻接矩阵中,当我们要查找一个顶点的相邻边时,我们只需要遍历该顶点对应的行或者列。在邻接矩阵中,行数与列数都是图的顶点数 |V| ,因此对应的时间复杂度为 O(|V|) ;
在邻接表中,当我们要查找一个顶点的相邻边时,对于有向图与无向图而言,其查找邻边的时间复杂度也是有所区别:
在十字链表中,当我们要查找一个顶点的相邻边时,就是查找该顶点的出度与入度,这时只需要分别遍历该结点的出度表与入度表即可
在邻接多重表中,当我们要查找一个顶点的相邻边时,只需要遍历对应的结点所邻接的边表即可
在邻接矩阵中:
在邻接表中:
在十字链表和邻接多重表中:
不难发现,在图中,当我们要删除一个顶点时,实际上就是需要查找该顶点相邻边并进行删除,因此其删除操作是基于查找操作实现;
邻接矩阵由于需要消耗大量的空间用于存储边,因此适用于存储稠密图;
在邻接表中,由于边表是通过链表实现,能够节省存储空间,因此对于稀疏图而言,更加适合用邻接表进行存储;
在十字链表中,只能够存储有向图
在邻接多重表中,只能够存储无向图
邻接矩阵是由各个顶点组成的矩阵,因此,其表示方式是唯一的;
在邻接表、十字链表以及邻接多重表中,由于边表的信息是通过链表进行的存储,因此其边表的表示方式并不唯一;
图的基本操作时独立于图的存储结构的。而对于不同的存储方式,操作算法的具体实现会有着不同的性能。在设计具体算法的实现时,应考虑采用何种存储方式的算法效率会更高。
图的基本操作主要包括:
Adjacent(G, x, y)
: 判断图G是否存在边<x, y>
或(x, y)
;Neighbors(G, x)
: 列出图G中与结点x邻接的边InsertVertex(G, x)
: 在图G中插入顶点xDeleteVertex(G, x)
: 在图G中删除顶点xAddEdge(G, x, y)
: 若无向边(x, y)
或有向边<x, y>
不存在,则向图G中添加改边RemoveEdge(G, x, y)
: 若无向边(x, y)
或有向边<x, y>
存在,则从图G中删除该边FirstNeighbors(G, x)
: 求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1NextNeighbors(G, x, y)
: 假设图G中顶点 y 是顶点 x 的一个邻接点,返回除 y 外顶点 x 的下一个邻接点的顶点号,若 y 是 x 的最后一个邻接点,则返回-1Get_edge_value(G, x, y)
: 获取图G中边(x, y)
或 <x, y>
对应的权值Set_edge_value(G, x, y, v)
: 设置图G中边(x, y)
或 <x, y>
对应的权值此外,还有图的遍历算法:按照某种方式访问图中的每个顶点,且仅访问一次。图的遍历算法有两种:
具体内容会在下一个篇章中进行介绍。
邻接多重表以“单边双链”的革新设计,将无向图的存储效率推向新高度——空间占用减半、删边操作跃升至O(1),完美解决了邻接表的冗余与低效痛点。
从邻接矩阵的刚性布局到链式结构的动态灵动,每一种存储方案都是空间与时间的精妙权衡,而邻接多重表无疑是高频删边场景的无向图终极答案。
但存储结构只是图算法的起点,真正的挑战在于如何基于这些结构实现高效操作。下一篇将深入图的广度优先遍历(BFS),解析其在邻接矩阵、邻接表及邻接多重表中的性能差异,并揭秘如何通过存储优化让BFS在千万级节点图中依然游刃有余!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有