图的存储结构
- 线性表:一对一:数组或者链表
- 树:一对多:数组和链表结合
- 图:多对多:?
因为任意两个顶点都可能存在联系, 因此无法以数据元素在内存中的物理位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)。
⭐️ 各种存贮结构的适用类型:
数组(邻接矩阵):有向图和无向图 ——— 稠密图
邻接表(逆邻接表):有向图和无向图 —— 稀疏图
十字链表:有向图
邻接多重表:无向图
邻接矩阵
邻接矩阵(无向图)
考虑到图是由顶点和边或弧两部分组成,合在一起比较困难,那就很自然地考虑到分为两个结构来存储。
- 顶点因为不区分大小,主次,所以用一个一维数组是很不错的选择
- 而边(或者弧)由于是顶点与顶点之间的关系,需要用二维数组来实现。
💡邻接矩阵
图的邻接矩阵的存储方式是用两个数组来表示图。一个一位数组存储图种顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
如图,我们可以设置两个数组,顶点数组为 Vertex[4] = {V0, V1, V2, V3},边数组 arc[4][4]为对称矩阵(0表示不存在顶点的边,1表示顶点存在边)
对称矩阵:N阶矩阵的元满足
a[i][j] = a[j][i](0<=i, j <=n)
。即 从矩阵的做对角线到右下角的主对角线为轴。除轴外的右上角元素与左下角的元素相等。
有了这个二维数组组成的对称矩阵,可以轻易的得出图中的信息:
- 任意两顶点是否有边,如Vi和Vj ->arc[i][j]/arc[j[i]
- 要知道某个顶点的度(关联边之和),其实就是这个顶点Vi在邻接矩阵中第i行或者第i列元素之和
邻接矩阵(有向图)
可见顶点数组 Vertext[4] = {V0, V1, V2, V4},弧数组arc[4][4]也是一个矩阵,但因为是有向图,所以这个矩阵并不对称。例如由V1到V0有弧,得到arc[1][0]=1,而V0到V1没有弧,因此 arc[0][1] = 0
关于顶点度的计算。例如顶点Vi,出度就是i行的所有元素之和,而入度就是i列所有元素之和。
例如V1的出度为1+1=2,入度为1。
邻接矩阵(网)
每条边上带有权值的图就叫做网。
这里
∞
代表一个计算机允许的,大于所有边上权值的值
对于网的邻接矩阵,有以下定义:
arc[i][j] = {
W(i,j) , 若 <Vi, Vj>或(Vi,Vj)∈E
∞ , 边或者弧不存在
}
💻编程使用邻接矩阵实现以下图的边和顶点关系的演示:
#include <stdio.h>
/**
* 打印二维数组
* @return
*/
void PrintArray(int a[4][4], char v[]) {
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
if (a[i][j] > 0) //当存在此方向的弧时输出起始地和权值
printf("%c -> %c : %d\n", v[i], v[j], a[i][j]);
}
}
}
int main() {
char Vertex[4] = {'A', 'B', 'C', 'D'};
int Arc[4][4] = {{0, -1, -1, 18},
{8, -1, 2, -1},
{4, -1, 0, -1},
{-1, -1, -1, 0}};
printf("Adjacent Matrix:\n");
PrintArray(Arc, Vertex);
return 0;
}
输出结果:
Adjacent Matrix:
A -> D : 18
B -> A : 8
B -> C : 2
C -> A : 4
邻接表
邻接矩阵无疑有很多优点,例如容易理解,而且索引和编排都很舒服。但是我们也发现,对于边数较少的图,这种结构造成了大量的空间浪费。
考虑将数组和链表结合起来来存储,我们称之为邻接表(Adjacency List)。
⭐️处理方法如下:
- 图中顶点用1个一维数组来存储(也可以使用单链表来存储)
- 图中每个顶点Vi的所有邻接点构成一个线性表,由于邻接点的个数的不确定,所以我们选择用单链表来存储。
如果是有向图,邻接表结构也是类似的,我们先看下把顶点当弧尾建立的邻接表,这样很容易就可以得到每个顶点的出度:
但是有时候为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表。
对于带权值的网图,可以在边表结构中再增加一个数据域来存储权值即可:
总结:在无向图的邻接表中,顶点vi的度就是第i个链表的结点数。
而在有向图中,第i个链表中的结点个数只是顶点vi的出度或者入度。
十字链表
对有向图的处理,同时应用邻接表和逆邻接表显然有些复杂,我们尝试寻找一种数据结构把这两种结构结合起来。这就是我们要谈的十字链表。
🍉数据结构简图
顶点结点:
data | firstIn | firstOut |
---|---|---|
数据域 | 首入弧索引 | 首出弧索引 |
弧结点结构:
tailVex | headVex | headLink | tailLink | info |
---|---|---|---|---|
弧尾顶点(j) | 弧头顶点(i) | 同顶点(i)下一条入度 | 同顶点(j)下一条出度 | 信息 |
蓝色:出度 ;红色:入度
💡十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样极容易找到以Vi为尾的弧,也容易找到以Vi为头的弧,因而容易求得顶点的出度和入度。
十字链表的时间复杂度和邻接表相同
邻接多重表
⚠️邻接多重表的画法并不唯一,实现效果即可
对于无向图,邻接表是不错的选择,但如果我们关注边的操作,比如对已经访问过的边做标记,或者删除某一条边等一系列操作,邻接表就显得的不那么方便了。
因此我们仿照十字链表的方式对边表结构进行改装,重新定义的边表结构如下。
iVex | iLink | jVex | jLink |
---|---|---|---|
依附的一个顶点 | 依附的另一个顶点 | 依附Vi的下一条边 | 依附Vj的下一条边 |
也就是说在邻接多重表中,边表存放的是一条边,而不是一个顶点
步骤:
(1)首先分发结点,对于边的下标顺序并不重要,因此邻接多重表的结构并不唯一;
(2)首先使得V1、V3、V5分别指向边(0,1) (2,1) (4,1);
(3)观察 (0,1),它对于V1的临边有(0,3),因此需要让(0,1)的i指针域指向(0,3)
(4)观察V2,与V2关联的边有(0,1) (2,1) (4,1),按照顺序先将V2的FristArc指向(0,1),然后将(0,1)的j指针域指向(2,1),之后让(2,1)的j指针域指向(4,1);
(5)观察边(2,1),对于V3的临边有(2,3)和(2,4),因此按照顺序,先让(2,1)i 的指针域指向 (2,3),然后让(2,3)的 i 指针域指向 ( 2, 4);
(6)观察顶点V4,与V4相关联的边有(0,3)和(2,3),此时有两种方式,先指向(0,3)和先指向(2,3),我们这里让V4的FristArc指向(2,3),然后让(2,3)的 j 指针域指向(0,3);
(7)观察(4,1)的临边对于V5的临边有(2,4),因此只需要将 (4,1)i的指针域指向(2,4)即可;
(8)将未指向任何内存空间的边左右指针域赋值为空。
🔔注意
在边的Vi或者Vj的指针域寻找指向的过程中,只要下一条边中含有Vi或者Vj(无论方向)任一,即可使得Vi或者Vj的指针域指向下一条边。
边集数组
边集数组是由两个一位数组构成,一个是存储顶点的信息,一个是存储边的信息。这个边数组的每个元素的数据结构如下:
begin | end | weight |
---|---|---|
起点下标 | 终点下标 | 权 |