【数据结构与算法】图的存储

54 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

🔥 本文由 程序喵正在路上 原创,在稀土掘金首发!
💖 系列专栏:数据结构与算法
🌠 首发时间:2022年11月29日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
🌟 一以贯之的努力 不得懈怠的人生

邻接矩阵法

图中点和点之间是否有连接,我们可以用一个表来表示,其中 “1” 表示两点之间有边,反之

image.png

上图很容易用代码表示出来

#define MaxVertexNum 100				//顶点数目的最大值
typedef struct{
	char Vex[MaxVertexNum];				//顶点表
	int Edge[MaxVertexNum][MaxVertexNum];		//邻接矩阵,边表
	int vexnum, arcnum;				//图的当前顶点数和边数/弧数
} MGraph;

顶点的数组中我们可以存储更复杂的信息,比如一个结构体

结点数为 nn 的图 G=(V,E)G = (V, E) 的邻接矩阵 AAn×nn \times n 的,将 GG 的顶点编号为 v1,v2,,vnv_1, v_2, \dots, v_n, 则

A[i][j]={1,(vi,vj)<vi,vj>E(G)中的边0,(vi,vj)<vi,vj>不是E(G)中的边\begin{align*} \begin{split} A[i][j]= \left \{ \begin{array}{ll} 1, & 若 (v_i, v_j) 或 <v_i, v_j> 是 E(G) 中的边\\ 0, & 若 (v_i, v_j) 或 <v_i, v_j> 不是 E(G) 中的边\\ \end{array} \right. \end{split} \end{align*}

在这种情况下,我们怎么求顶点的度、入度和出度呢?

  • 对于无向图,第 ii 个结点的度 ==ii 行(或第 ii 列)的非零元素个数
  • 对于有向图,第 ii 个结点的出度 ==ii 行的非零元素个数,第 ii 个结点的入度 ==ii 列的非零元素个数,,第 ii 个结点的度 ==ii 行、第 ii 列的非零元素个数之和

邻接矩阵法求顶点的度 / 出度 / 入度的时间复杂度均为 O(V)O(|V|)

邻接矩阵法存储带权图(网)

#define MaxVertexNum 100				//顶点数目的最大值
#define INFINITY 最大的int值			     //宏定义常量 “无穷”
typedef char VertexType;				//顶点的数据类型
typedef int EdgeType;					//带权图中边上权值的数据类型
typedef struct{
	VertexType Vex[MaxVertexNum];			//顶点
	EdgeType Edge[MaxVertexNum][MaxVertexNum];	//边的权
	int vexnum, arcnum;				//图的当前顶点数和弧数
} MGraph;

邻接矩阵法的性能分析

空间复杂度:O(V)O(|V|) —— 只和顶点数相关,和实际的边数无关,适合用于存储稠密图

无向图的邻接矩阵是对称矩阵,我们可以压缩存储,也就是只存储上三角区或者下三角区

邻接矩阵法的性质

image.png

设图 GG 的邻接矩阵为 AA(矩阵元素为 0/10/1),则 AnA^n 的元素 An[i][j]A^n[i][j] 等于由顶点 ii 到顶点 jj 的长度为 nn 的路径的数目

假设 AA 如下表所示

image.png

现在要计算 A2[1][4],A2[2][2]A^2[1][4], A^2[2][2],该怎么做呢?

A2[1][4]=a11×a14+a12×a24+a13×a34+a14×a44=1A2[2][2]=a21×a12+a22×a22+a23×a32+a24×a42=3A^2[1][4] = a_{11} \times a_{14} + a_{12} \times a_{24} + a_{13} \times a_{34} + a_{14} \times a_{44} = 1 \\ A^2[2][2] = a_{21} \times a_{12} + a_{22} \times a_{22} + a_{23} \times a_{32} + a_{24} \times a_{42} = 3

其中 a11a_{11} 表示矩阵的第 11 行第 11 个元素,其他依此类推

我们画出 A2A^2 的表,如下

image.png

可以发现,我们计算出来的值就是这个表中对应位置的值

如果让你计算 A3[1][4]A^3[1][4] 呢?

邻接表法(顺序+链式存储)

image.png

// “边/弧”
typedef struct ArcNode{
	int adjvex;						//边/弧指向哪个结点
	struct ArcNode *next;			//指向下一条弧的指针
	InfoType info;					//边权值(没有可不加)
}ArcNode;

// “顶点”
typedef struct VNode{
	VertexType data;			//顶点信息
	ArcNode *first;				//第一条边/弧
}VNode, AdjList[MaxVertexNum];

//用邻接表存储的图
typedef struct{
	AdjList vertices;
	int vexnum, arcnum;
}ALGraph;

当然,有向图也可以用邻接表法来表示

image.png

空间复杂度分析

对于无向图,由于每一条边都会有一个指向的结点占空间,而且是双向的,比如 AABB 之间有一条边,那么 AABB 这条边就会有一个指向 BB 的结点,同理 BBAA 这条边就会有一个指向 AA 的结点,由于边结点的数量是 2E2|E| ,所以整体的空间复杂度为 O(V+2E)O(|V| + 2|E|)

对于有向图,由于每条边都是有方向的,所以只会有一个指向结点,整体的空间复杂度即为 O(V+E)O(|V| + |E|)

度、入度和出度

那么,下面我们思考一下怎么求顶点的度、入度和出度呢?

  • 对于无向图,我们只要遍历边结点的链表就能求出对应顶点的度
  • 对于有向图,通过遍历边结点的链表我们就能求出对应顶点的出度;但是要得到某个顶点的入度,就很麻烦了,我们需要遍历所有顶点相应的边结点的链表,这也是邻接表法的一个缺点

邻接矩阵和邻接表的对比

图的邻接表表示方式并不唯一,与一个顶点有关系的边的顺序是可以颠倒排列的;但是对于前面的邻接矩阵来说,只要确定了顶点编号,那图的邻接矩阵就是唯一的

对比角度邻接矩阵邻接表
空间复杂度O(V2)O(\|V\|^2)无向图 O(V+2E)O(\|V\| + 2\|E\|);有向图 O(V+E)O(\|V\| + \|E\|)
适合用于存储稠密图存储稀疏图
表示方式唯一不唯一
计算度 / 出度 / 入度必须遍历对应行或列计算有向图的度、入度不方便,其余很方便
找相邻的边必须遍历对应行或列找有向图的入边不方便,其余很方便

十字链表

我们需要定义两个结构:弧结点和顶点结点

image.png

空间复杂度为:O(V+E)O(|V| + |E|)

如何找到指定顶点的所有出边? —— 顺着绿色线路找 如何找到指定顶点的所有入边? —— 顺着橙色线路找

注意:十字链表只用于存储有向图

邻接多重表

image.png

空间复杂度为:O(V+E)O(|V| + |E|)

删除边、删除节点等操作很方便

注意:邻接多重表只用于存储无向图

总结

角度邻接矩阵邻接表十字链表邻接多重表
空间复杂度O(V2)O(\|V\|^2)无向图 O(V+2E)O(\|V\| + 2\|E\|);有向图 O(V+E)O(\|V\| + \|E\|)O(V+E)O(\|V\| + \|E\|)O(V+E)O(\|V\| + \|E\|)
找相邻边遍历对应行或列,时间复杂度为 O(V)O(\|V\|)找有向图的入边必须遍历整个邻接表很方便很方便
删除边或顶点删除边很方便,删除顶点需要移动大量数据无向图中删除边或顶点都不方便很方便很方便
适用于存储稠密图存储稀疏图或其他只能存储有向图只能存储无向图
表示方式唯一不唯一不唯一不唯一