图的存储结构
图中的一个顶点会和多个顶点产生联系,这就导致了图结构不能够被简单的存储结构直接存。如果把和一个顶点有关联的顶点都链到这个定点上,操作只会变得复杂且难以提取,这么做意义不大。所以我们就需要一些特殊的结构,来对顶点和顶点间的关系进行描述。
邻接矩阵
存储手法
因为描述一个图结构,关键是描述清楚顶点和边(弧)的关系。为了描述顶点,由于顶点不考虑顺序,因此可以用用数组来存储。不过对于边而言,边需要同时描述两个顶点间的关系,那就要两个结构分别来存喽。大可不必,我们想到二维数组是一个拥有 2 个维度的存储结构,因此可以用二维数组来存储,那么这种存储方式就是邻接矩阵。邻接矩阵使用一个一维数组存储顶点的信息,一个二维数组描述边的关系,序号与一维数组的顶点相对应。
无向图
首先我们先看看无向图,我们结合一个例子来看。如图所示,无顶点数组为:vert[8] = {a,b,c,d,e,f,g,h} 。无向图的话呢从一个顶点到另一个顶点,这个关系可逆,因此当两个顶点间存在边时,就将二维数组对应位置的元素赋值为两个顶点的值(若不考虑就赋值为 1),请注意要修改 2 个地方。也就是所如果 1 号顶点和 2 号顶点是有边的,则二维数组的 [1][2] 和 [2][1] 两个单元都需要赋值。
值得我们注意的是,对于邻接矩阵的对角线而言,边的关系都会是 0,因为顶点和它本身之间是不可能存在边的。因此我们也可以观察出,无向图的邻接矩阵会是一个对称矩阵。
对称矩阵(Symmetric Matrices)是指以主对角线为对称轴,各元素对应相等的矩阵。在线性代数中,对称矩阵是一个方形矩阵,其转置矩阵和自身相等。1855年,埃米特(C.Hermite,1822-1901年)证明了别的数学家发现的一些矩阵类的特征根的特殊性质,如称为埃米特矩阵的特征根性质等。后来,克莱伯施(A.Clebsch,1831-1872年)、布克海姆(A.Buchheim)等证明了对称矩阵的特征根性质。泰伯(H.Taber)引入矩阵的迹的概念并给出了一些有关的结论。——百度百科
这个矩阵对什么方面的操作提供了便利呢?首先是判断两个顶点是否有联系,即有没有边会变得容易,直接访问二维数组就好了。我们要确定某个顶点的度也会变得容易,入度就去统计顶点所在列或行的边个数。求某个顶点的列结点也变得容易,把对应顶点的行找一找有多少边即可。
有向图
接着我们看看有向图,还是结合一个例子来看。如图所示,无顶点数组为:vert[4] = {v0,v1,v2,v3} 。无向图的话呢从一个顶点到另一个顶点,这个关系是不可逆的,因此当两个顶点间存在边时,就将二维数组对应位置的元素赋值为两个顶点的值(若不考虑就赋值为 1),和上面不同的是,有向图只需要修改 1 个地方即可。也就是所如果 1 号顶点和 2 号顶点是有边的,且关系是 1 -> 2,则二维数组只有 [1][2] 单元需要赋值。
对于有向图我们可以研究出度和入度,所谓出度就是从该顶点出发可以到达几个顶点,入度就是从哪些顶点出发可以到达该顶点。通过观察可以发现一个顶点的入度是邻接矩阵中对应的列的数字和,出度是邻接矩阵中对应的行的数字和。
网
接着我们看看网,网就是在有向图的基础上为每个边附带权值。值得一提的是由于权值可能为任何值,因此我们才需要引入 ∞ 来表示不存在的边,因此为了描述这层关系,我们可以把不存在的边赋值为 ∞ ,表示这两个顶点之间距离无限大,以至于一定不产生联系,这里可以用一个超大的值来表示。对于顶点本身应该赋值为 0,而对于具有权值的边就赋值为对应的权值即可。
结构体定义
typedef char VertexType; //顶点的数据类型
typedef struct
{
VertexType vex[MAXV]; //顶点信息表
int edges[MAXV][MAXV]; //邻接矩阵
int n; //顶点数
int e; //弧数
} MGraph; //图的邻接矩阵表示类型
建图代码
该算法的时间复杂度为 O(n2)。
void CreateMGraph(MGraph& G, int n, int e)
{
int i, j;
int point1, point2;
for ( i = 0; i < n; i++) //邻接矩阵初始化
{
for ( j = 0; j < n; j++)
{
G.edges[i][j] = 0;
}
}
G.n = n; //导入结点数
G.e = e; //导入边(弧)数
for ( i = 0; i < e; i++)
{
cin >> point1 >> point2; //输入结点信息
G.edges[point1][point2] = G.edges[point2][point1] = 1; //无向图
//G.edges[point1][point2] = 1; //有向图
}
}
表示法的优缺点
优点
- 便于判断 2 个顶点间是否有边;
- 便于统计顶点的度。
缺点
- 不便于顶点的添加与删除;
- 不便于统计边的数量;
- 时间复杂度高。对于有向图的话,n 个顶点就需要存储 n2 个边,若存储无向图也要操作对称矩阵。
- 对于稀疏图来说,浪费空间(即使无向图可以压缩矩阵)。
邻接表
存储手法
现在我们来看一个情况,假设有如图有向图,在 vert[4] = {v0,v1,v2,v3} 点集中只有如图一条边,那么它的邻接矩阵就只有一个元素值不为 0。由此可见对于一个边数较少的图结构,用邻接矩阵去存储对空间的浪费很大。
根据前面的学习,我们会考虑使用链式存储结构实现。要怎么来描述呢?关键在于我们要描述的对象是一个顶点指向了哪些顶点。回忆一下在树结构中,当我们要描述一个结点的后继时,我们是怎么存的?用的是孩子表示法。
用这种手法来存储图结构,称之为邻接表,即数组描述顶点,链表描述指向关系的存储手法。
有向图
这种手法怎么实现无向图呢?首先我需要一个一维数组,这个一维数组的结构体应当有 2 各成员,数据域存储结点信息,而指针域表示指向顶点的链表头结点。使用一维数组时我们提取结点信息会比较方便,至于指向关系,我们只需要把指向的顶点做成链表的一个结点,然后链到头结点后面去。
使用邻接表的话,得知出度会比较方便,直接去遍历对应顶点的链表就行啦,不过入度就不好数了。
无向图
对于无向图的话,其实操作也是一样的,不过我们要多做一步。因为对于两个顶点而言,之间的边双向的,即我添加结点需要在两个顶点的链表都添加结点。如图所示。
网
对于网也一样,只是我们还需要再开一个成员存储权值。
结构体定义
typedef struct ANode
{
int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表结点类型
typedef int Vertex;
typedef struct Vnode
{
Vertex data; //顶点信息
ArcNode *firstarc; //指向第一条边
} VNode; //邻接表头结点类型
typedef struct
{
typedef VNode AdjList[MAXV]; //表头向量
int n,e; //图中顶点数 n 和边数 e
} AdjGraph; //邻接表结构体
建图代码
void CreateNode(AdjGraph*& G, int point1, int point2)
{ //插入单个结点
ArcNode* ptr;
ptr = new ArcNode;
ptr->adjvex = point2;
ptr->nextarc = G->adjlist[point1].firstarc;
G->adjlist[point1].firstarc = ptr;
}
void CreateAdj(AdjGraph*& G, int n, int e)
{ //建图算法
int point1, point2;
ArcNode* ptr;
G = new AdjGraph;
for (int i = 1; i <= n; i++) //初始化为 NULL
{
G->adjlist[i].firstarc = NULL;
}
for (int i = 0; i < e; i++)
{
cin >> point1 >> point2;
CreateNode(G, point1, point2);
//CreateNode(G, point2, point1); //若是无向图要多插一个结点
}
G->n = n;
G->n = e;
}
表示法的优缺点
优点
- 便于顶点的增加与删除;
- 便于统计边的数量;
- 存储空间的利用率高,空间复杂度为 O(n + e)。
缺点
- 不便判断顶点之间是否有边,若硬要判断就需要去遍历,时间复杂度 O(n);
- 不便于计算有向图各个顶点的度。
十字链表
存储手法
我们来审视一下邻接表,当我们想要知道一个图结构中,某一个结点的入度,用邻接表去统计的话,时间复杂度是“毁灭性的”。如果我非要完成这个工作的话,比较明确的方法就是造一个逆邻接表,但是这样统计出度又成了大问题,“拆了东墙补西墙”。不过把思路逆转一下,还是很明确的,我们的目的就是用链式存储实现图结构,并使得出度和入度的统计变得简单。现在我们有邻接表和逆邻接表,那么我们就想要把这两张表合并成一张,这就是我们要讲得十字链表的思想。
十字链表可以看做由有向图邻接表和逆邻接表组合而成的链式存储结构,它的特点是对于有向图中的每一段弧都有一个结点。弧结点将拥有 5 个域,如图所示。尾域 (tailvex) 和头域 (headvex) 指向的是弧头和弧尾顶点在图中的位置,头链域 (hlink) 指向弧头相同的下一个弧,尾链域 (tlink) 指向弧尾相同的下一个弧,最后数据域 (info) 存储弧的相关信息,如权值等。
每个顶点也有一个结点,如图所示。firstin 和 firstout 都是链域,分别指向以该结点为弧头的第一个弧结点,和以该结点为弧尾的第一个弧结点,data 则是数据域,存储结点信息。
我们来看个例子,由于我作图能力有限,我把我的书本上的图放上来。
——数据结构算法教程
对于十字链表来说,可以把它看做是邻接矩阵的链式存储结构。当我获取了顶点信息和弧信息之后就能建立这样的结构,且时间复杂度和建立邻接表相同。使用十字链表可以快速找到一个顶点的出度和入度,且可以在建立结构的时候顺手得出
结构体定义
typedef struct ANode
{
int tailvex, headvex; //弧的头域和尾域
struct ANode *hlink, *tlink; //弧尾、弧头相同的下一条弧的指针域
int info; //该边的相关信息,如权重
} ArcNode; //弧表结点类型
typedef int Vertex;
typedef struct Vnode
{
Vertex data; //顶点信息
ArcNode *firstin; //指向第一条入弧
ArcNode *firstout; //指向第一条出弧
} VNode; //十字链表头结点类型
typedef struct
{
typedef VNode AdjList[MAXV]; //表头向量
int n,e; //图中顶点数 n 和弧数 e
} OLGraph; //十字链表接表结构体
邻接多重表
存储手法
当我需要对边进行操作的时候,例如删除某一条边,这对有向图的邻接表来说还是可以接受的,因为只需要敲掉一个结点啦。但是对于无向图邻接表来说又是个灾难,因为你需要敲掉 2 个结点,即一条边的两端。和十字链表类似,也有一种存储结构可以用于解决这个问题,我们称之为邻接多重表。
在邻接多重表中,每一条边都用一个结点来表示,一共有 6 个指针域,如图所示。标志域 (mark) 用于标记这条边是否有被搜索过,顶点域 (ivex 和 jvex) 为这条边连接的两个顶点,边域 (ilink 和 jlink) 分别指向依附于顶点 ivex 和 jvex 的下一条边,最后数据域 (info) 存储弧的相关信息,如权值等。
对于顶点来说,也一样用一个结点来表示,data 则是数据域,存储结点信息,firstedge 域指向第一条依附于该顶点的边。
在邻接多重表中,所有依附于同一顶点的边将被串联在同一链表。由于每条边的两端是两个顶点,所以每个边结点会被同时链接到两个链表。对无向图而言,邻接多重表对于邻接表的强化在于在邻接多重表中只有一个结点。从本质上来说,除了在边结点中增加一个标志域外,邻接多重表所需的存储量和邻接表相同。
我们来看个例子,由于我作图能力有限,我把资料上的图放上来。
——数据结构算法教程
结构体定义
typedef enum{unvisited, visited} VisitIf;
typedef struct ANode
{
VisitIf mark; //访问标记
int ivex, jvex; //边依附的两个顶点位置
struct ANode *ilink, *jlink; //指向依附于这两个顶点的下一条边
int info; //该边的相关信息,如权重
} ArcNode; //弧表结点类型
typedef int Vertex;
typedef struct Vnode
{
Vertex data; //顶点信息
ArcNode *firstedge; //指向第一条依附于该顶点的边
} VNode; //邻接多重表头结点类型
typedef struct
{
typedef VNode AdjList[MAXV]; //表头向量
int n,e; //图中顶点数 n 和弧数 e
} AMLGraph; //十字链表接表结构体
边集数组
关注边的手法
之前的 4 种手法关注的是顶点的信息,我们同样可以以关注边的信息来存储图结构,也就是我们接下来要谈的边集数组。边集数组是由两个一维数组构成,关键在于一个一维数组是存储边的信息。边数组每个数据元素由一条边的起点下标、终点下标和权组成。边集数组关注的是边的集合,因此边集数组不适合对顶点相关的操作。例如有这样的图结构:
其边集数组为:
结构体定义
typedef struct
{
int begin;
int end;
int weight;
}Edge;
实例:图着色问题
题干
输入样例
6 8 3
2 1
1 3
4 6
2 5
2 4
5 4
5 6
3 6
4
1 2 3 3 1 2
4 5 6 6 4 5
1 2 3 4 5 6
2 3 4 2 3 4
输出样例
Yes
Yes
No
No
情景分析
对于这个问题的需求,虽然题干是比较长,但是思路还是很明确的。从本质上来说,我们需要按照数字表示的颜色,来确定两个顶点之间是否出现了相同的颜色。因此正确地把图建出来显得更为重要,邻接矩阵和邻接表皆可,我选择邻接表。
判断着色是否合理
伪代码
代码实现
void CreateNode(AdjGraph*& G, int point1, int point2)
{
ArcNode* ptr;
ptr = new ArcNode;
ptr->adjvex = point2;
ptr->nextarc = G->adjlist[point1].firstarc;
G->adjlist[point1].firstarc = ptr;
}
bool judgeOrRight(AdjGraph* G, int ver_color[], int color)
{
int point1, point2;
ArcNode* ptr;
set<int> all_color;
for (int i = 1; i <= G->n; i++)
{
all_color.insert(ver_color[i]);
}
if (all_color.size() != color)
{
return false;
}
for (int point1 = 1; point1 <= G->n; point1++)
{
ptr = G->adjlist[point1].firstarc;
while (ptr)
{
if (ver_color[ptr->adjvex] == ver_color[point1])
{
return false;
}
ptr = ptr->nextarc;
}
}
return true;
}
主函数
参考资料
《大话数据结构》—— 程杰 著,清华大学出版社
《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社
【数据结构】十字链表
数据结构与算法教程,数据结构C语言版教程