图,虽然在面试和实际应用中不是那么常见,但是也完全不可以忽视图的重要性哦!
什么是图呢?
图是一种比树更复杂的的数据结构。树描述的是一对多的关系,但是实际生活中的人际关系并不是都像父母与孩子的关系那样,一对父母可以有多个孩子,一个孩子只能有一对父母。所以要引入图来描述多对多的关系模式,比如说我的朋友,他们之间可能也相互认识。
直接上图吧比较直观
如图,把各个顶点看作是人,顶点之间的连线表示朋友关系。假如 B、D、E、F都是A的朋友,C又是B和D的朋友,像这样的许多人构成的一个多对多的关系网就是数据结构中的图(Graph)
图的术语
图中,蓝色部分是图的顶点(vertex)是最基本的单元,相当于树中的节点。关联两个顶点之间的关系的是边(edge)。每一条边并不是完全相同的,就像你的朋友在你心中的分量不一样,这就引出一个新的概念:边的权重(weight)。像上面这样涉及到权重的图就是带权图;如果几个朋友关系都一样好,那么权重可以忽略,这样的图就是无向图;还有可能你认识的人并不认识你,这种有方向的图就是有向图。
有向图如下
图的表示
图的基本概念懂了,那如何存储一个图呢?
图的存储方式有很多种,比如邻接矩阵表示法、邻接表表示法、十字链表表示法、邻接多重表表示法,虽然这有四种方式,不过它们成立的原则都是一样的:存入电脑后能根据自身这一种存储方式就能复原图。不像树要根据先序、中序遍历或者中序、后序遍历才能复原。
下面一一讲解这四种存储方式
邻接矩阵
无向图的邻接矩阵
如果一个图有n个顶点,那么这n个最多可以有n(n-1)条连接线。像这样的既要存储顶点,又要存储顶点之间的连线关系,二维数组再合适不过啦。如上图,右图是左图的邻接矩阵表现。先看顶点A和B,它们之间有边关联,那么矩阵中的元素[0][1]和[1][0]的值就是1;再看顶点A和C,它们之间没有边关联,所以[0][3]和[3][0]的值都是0。
需要注意的是,矩阵中左上角到右下角这条对角线上的值都是0。这点很好理解对角线上的点都是顶点自己连接自己,那么肯定就不需要连接线啦。并且矩阵是关于这条对角线对称的,因为这里的图是无向图,可以从A到B,反过来也是可行的。
有向图的邻接矩阵
无向图的邻接矩阵已经搞定了,那么可以举一反三得到有向图的邻接矩阵表示
有向图的邻接矩阵跟无向图很相似,但是要知道行的含义是以这个顶点为起点的边,列的含义是以这个顶点为终点的边。如上图中A到B有边关联,但B到A没有,所以[0][1]的值是1,,1][0]的值是0,也正因此,矩阵不再具有对称性。
那么问题来啦,如果边上有权重的话,用邻接矩阵该如何表示呢?
带权图的邻接矩阵
带权图的邻接矩阵我们要解决的问题就是,权在矩阵矩阵终于任何表示,其他的还是沿用有向图的方法。如上图,A到B有边关联且权值为5,所以[0][1]的值是5;B到A没有边关联,这里用无穷的符号来表示。
三种图的邻接矩阵都已经实现,我们思考一下,如果一个图有n个顶点,那么就需要n*n个单元来存储边,占用的空间太多了。既然这样为什么还会使用这种方式呢?那是因为这种方式比较容易实现图的操作,比如:求某个顶点的度、判断顶点之间是否有边、找顶点的邻接顶点等等。
图的邻接矩阵的实现
需要注意的一点:用来两个数组分别存储顶点表和邻接矩阵
#define INFINITY INT_MAX //最大值∞
#define MAX_VERTEX_NUM 20 //假设的最大顶点数
typedef enum {DG, DN, AG, AN } GraphKind; //有向/无向图,有向/无向网
typedef struct ArcCell{
VRType adj; //顶点间关系,无权图取1或0;有权图取权值类型
InfoType *info; //该边相关信息的指针
} AdjMatrix [ MAX_VERTEX_NUM ] [MAX_VERTEX_NUM ];
struct Mgraph{ //图的定义
VertexType vexs [MAX_VERTEX_NUM ] ; //顶点表,用一维向量即可(n)
AdjMatrix arcs; //邻接矩阵n*n
int vernum, arcnum; //顶点总数n,边总数e
GraphKind kind; //图的种类标志
};
//无向网的构造算法,用数组表示法表示
Status CreateUDN(Mgraph &G){
scanf(&G.vexnum, &G.arcnum, &IncInfo);
for(i=0;i<G.vexnum,;++i)
scanf(&G.vexs[i] ); //输入n个顶点值,存入一维向量
for(i=0; i<G.vexnum; ++i) //给邻接矩阵赋初值
for(j=0;j<G.vexnum;++j)
G.arcs[i][j]={INFINITY, NULL};
for(k=0;k<G.arcnum;++k){
scanf(&v1, &v2, &w); //输入边的两顶点以及对应权值
i=LocateVex(G,v1);
j=LocateVex(G,v2); //找到两顶点在矩阵中的位置(n次)
G.arcs[i][j].adj=w; //输入对应权值
if(IncInfo)
Input(*G.arcs[i][j].info); //如果边有信息则填入
G.arcs[i][j] = G.arcs [j] [i];//无向网是对称矩阵
}
return OK; } // CreateUDN
对于有n个顶点e条边的网,创建网的时间效率是O(n+n2+e*n)
邻接表
为了解决空间效率的问题,所以诞生了邻接表。邻接表是用单链表的形式实现的,就是对每一个顶点创建一个单链表,这个顶点作为链表的头结点,后面链接着这个顶点能够直接到达的相邻接点。
把头结点分成两个域,分别是数据域和链域(指向单链表的第一个结点);头结点后面链接的相邻接点也分成两个域,分别是邻接点域(邻接点是位置)和链域(指向下一个结点)。
链表左边的数字“0,1,2,3”是顶点的链人顺序,各个边的节点的链入顺序是任意的,所以邻接表不是唯一的!
使用邻接表很容易就能查找到A的邻接点或者A到C的路径。但是如果要查找哪些顶点能够直接到达A,对于上面的有向图只有4个顶点,挨个遍历很快就能够找到,如果要是有100,1000甚至更过的顶点的话效率会很低。
像这样逆向查找,我们可以通过逆邻接表来解决。
逆邻接表实际上就是把邻接表反过来,逆邻接表每一个顶点作为链表的头节点,后面链接的是能够直达到这个顶点的相邻接点。
虽然逆向查找的问题解决啦,但是在实际应用当中,要使用邻接表,还要使用逆邻接表,实现过程很麻烦。这个问题后面的十字链表会帮我们解决,我们先看下图的邻接表用代码如何表示。
图的邻接表的表示
#define MAX_VERTEX_NUM 20 //假设的最大顶点数
struct ArcNode { //边结构
int adjvex; //该边所指向的顶点位置
struct ArcNode *nextarcs; //指向下一条边的指针
InfoArc *info; //该边相关信息的指针
};
typedef struct VNode{ //顶点结构
VertexType data; //顶点信息
ArcNode * firstarc; //指向依附该顶点的第一条边的指针
}AdjList[ MAX_VERTEX_NUM ];
struct ALGraph{ //图结构
AdjList vertics ; //应包含邻接表
int vexnum, arcnum; //应包含顶点总数和边总数
int kind; //说明图的种类(用标志)
};
十字链表
它是有向图的另一种链式结构,将邻接矩阵用链表存储,是邻接表和逆邻接表的结合
看到这张图第一感觉就是懵,这种东西确实有点麻烦,不过看懂它的结构就很好理解啦。
大致思路就是开设两个结点:顶点结点和边结点,结点结构如下
起点顶点位置 | 终点顶点位置 | 终点相同的下一边位置 | 起点相同的下一边位置 |
这个是边结点,是用来存储边的信息的结点。把它分成4个域,下面一一解释各个域的存储信息
例如顶点A,以A作为起点的边有两条,一条从A指向B(下文简称边AB),一条从A指向C。既然有两条边就需要有两个边结点来存储边信息,就拿A的第一个边结点来说,它代表的是由A指向B的这条边。这个边结点的第一个域存放的是边的起点A的位置,第二个域存放的是边的终点B的位置,第三个和第四个域存放的都是地址分别指向与边AB终点相同的下一条边的边结点和与边AB起点相同的下一条边的边结点。这样看也不是很难,捋顺了就懂了。
顶点信息 | 以该顶点为终点的第一条边结点 | 以该顶点为起点的第一条边结点 |
这个是顶点结点,存放的是顶点的信息。分为3个域,分别是……上面有就不废话啦,顶点结点与边结点大致相同,不懂的话再捋捋前面的边结点。
上面的例子讲的是有向图的十字链表,那带权图的该怎么办呢?
很简单,给边结点再增加一个域,用来存放权值,问题就解决啦。
十字链表集合了邻接表和逆邻接表,空间复杂度和建表的时间复杂度也与邻接表相同,也很容易操作。既然说到操作啦就来看看十字链表存储结构怎样构建吧。
#define MAX_VERTEX_NUM 20
struct ArcBox { //边结点结构,5个域
int tailvex , headvex ;
struct ArcBox * hlink , tlink;
InfoType *info;
};
struct VexNode{ //顶点结构, 3个域
VertexType data;
ArcBox * firstin,*firstout;
};
struct OLGraph{ //图结构,整体概念
VexNode xlist[ MAX_VERTEX_NUM ]; //表头向量
int vexnum, arcnum;
};
邻接多重表
这是无向图的另一种存储结构,当对边操作是一般采用这种结构存储。前面的十字链表能够理解的话,邻接多重表也没什么问题,他们道理是一样的。
看到这张图又要怀疑人生啦,那么多的线!不慌,听我娓娓道来,很明显还是开设两个结点:头结点和边结点。
结构如下表:
边依附的顶点i的位置 | 指向下一条依附顶点i的边位置 | 边依附的顶点j位置 | 指向下一条依附顶点j的边位置 |
顶点信息 | 依附顶点的第一条边结点 |
举个例子你就明白啦。还是顶点A,A的顶点结点的第二个域存放的是与顶点A相连的第一条边的边结点的地址,也就存储边AB的边结点。边结点有分为4个域,第一个域存储的是“0”也就是A的位置;第三个域存储的是“1”是B的位置;第二个域存放的是与A相关的下一条边的边结点的地址;第四个域存放的是与B相关的下一条边的地址。很绕,结合上面的图多捋几遍还是可以理解的。
邻接多重表把所有的与某一个顶点有关的边都串联在同一个链表中,因为每条边上有两个顶点,所以每个边结点同时链接在两个链表中。
邻接多重表的表示
#define MAX_VERTEX_NUM 20
typedef int Status;
typedef int InfoType; //定义弧相关信息为整形
typedef char VertexType; //定义顶点类型为字符型
typedef enum {unvisited,visited} VisitIF; //定义{未访问,已访问}等枚举常量,同时定义VisitIF为枚举变量类型
typedef struct ArcNode
{
VisitIF mark; //访问标记
int ivex,jvex; //该弧所依附的两个顶点在顺序结构中的位置
struct ArcNode *ilink,*jlink; //分别指向依附这两个顶点的下一条边
InfoType *info; //该弧相关信息的指针
}ArcNode; //弧结构
typedef struct VNode
{
VertexType data; //顶点
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode,AMLList[MAX_VERTEX_NUM]; //顶点结构
typedef struct
{
AMLList vertices; //顶点数组
int vexnum,arcnum; //顶点数和弧数
}AMLGraph; //多重邻接表表示的图结构
好啦!小伙伴们,今天的分享就到这,喜欢的话可以关注收藏。有什么问题可以在微信公众号私信给我。
明天见喽!