一、图的初识
在一个社交网络中,每个帐号和他们之间的关系构成了一张巨大的网络,就像下面这张图:
image.png那么在电脑中,我们要用什么样的数据结构来保存这个网络呢?这个网络需要用一个之前课程里未提到过的数据结构,也就是接下来要讲解的 图 结构来保存。
到底什么是图?图是由一系列顶点和若干连结顶点集合内两个顶点的边组成的数据结构。数学意义上的图,指的是由一系列点与边构成的集合,这里我们只考虑有限集。通常我们用 表示一个图结构,其中 表示点集, 表示边集。
在顶点集合所包含的若干个顶点之间,可能存在着某种两两关系——如果某两个点之间的确存在这样的关系的话,我们就在这两个点之间连边,这样就得到了边集的一个成员,也就是一条边。对应到社交网络中,顶点就是网络中的用户,边就是用户之间的好友关系。
如果用边来表示好友关系的话,对于微信这种双向关注的社交网络没有问题,但是对于微博这种单向关注的要如何表示呢?
于是引出了两个新的概念:有向边 和 无向边。
1.1 有向边 与 无向边。
简而言之,一条有向边必然是从一个点指向另一个点,而相反方向的边在有向图中则不一定存在;而有的时候我们并不在意构成一条边的两个顶点具体谁先谁后,这样得到的一条边就是无向边。就像在微信中, 是 的好友,那 也一定是 的好友,而在微博中, 关注 并不意味着 也一定关注 。
对于图而言,如果图中所有边都是无向边,则称为无向图,反之称为有向图。
简而言之,无向图中的边是“好友”,而有向图中的边是“关注”。一般而言,我们在数据结构中所讨论的图都是有向图,因为有向图相比无向图更具有代表性。实际上,无向图可以由有向图来表示。如果 两个点之间存在无向边的话,那用有向图也可以表示为 两点之间同时存在 到 与 到 两条有向边。仍然以社交网络举例:虽然微博中并不存在明确定义的好友关系,但是一般情况下,如果你和另一个 ID 互相关注的话,那么我们也可以近似认为,你和 TA 是好友。
1.2 图的准确定义
我们来形式化地定义一下图:图是由顶点集合(简称 点集)和顶点间的边(简称 边集)组成的数据结构,通常用 来表示。其中点集用 来表示,边集用 来表示。在 无向图 中,边连接的两个顶点是无序的,这些边被称为 无向边。例如下面这个无向图 ,其点集 ,边集为 。
image.png而在有向图中,边连接的两个顶点之间是有序的。箭头的方向就表示有向边的方向。例如下面这张有向图 :
image.png其点集 ,边集为 。对于每条边 ,我们称其为从 到 的一条有向边, 是这条有向边的 起点, 是这条有向边的 终点。注意在有向图中, 和 是不同的两条有向边。
二、图的特征
2.1 图的分类
有很少边或弧(如 , 指边数, 指顶点数)的图称为 稀疏图,反之称为 稠密图。对应到微博里,如果在一个圈内,同学们都互相关注,则我们可以认为该关系图是一个稠密图,如果只有几个人关注了别人,则我们可以认为这是一个稀疏图。如果图中边集为空,则称该图为 零图。
如果无向图中任何一对顶点之间都有一条边相连,也就是有 条不重复的边,则这个无向图被称为 完全图。类似地,如果有向图中任何一对顶点 之间都有两条有向边 相连,则称这个有向图为 有向完全图。下图就是由 个顶点组成的无向完全图。
image.png对于一个图,如果以任意一个点为起点,在图上沿着边走都可以到达其他所有点(有向图必须沿有向边的方向),那么这个图就是连通图。显然完全图一定是连通图。
2.2 度的概念
在无向图中,顶点的 度 是指某个顶点连出的边数。例如在下图中,顶点 的度数为 ,顶点 的度数为 。
image.png在有向图中,和度对应的是 入度 和 出度 这两个概念。顶点的入度是指以该顶点为终点的有向边数量;顶点的出度是指以顶点为起点的有向边数量。需要注意的是,在有向图里,顶点是没有 度 的概念的。例如在下图中,顶点 的入度为 ,出度为 ;顶点 的入度为 ,出度为 。
image.png2.3 度的性质
在无向图或有向图中,顶点的度数总和为边数的两倍,即:
而在有向图中,有一个很明显的性质就是,所有顶点的入度和等于出度和。
三、邻接矩阵
这一节我们来学习的图的一种储存方式——邻接矩阵。
3.1 邻接矩阵与图
什么是邻接矩阵呢?所谓邻接矩阵存储结构就每个顶点用一个一维数组存储边的信息,这样所有点合起来就是用矩阵表示图中各顶点之间的邻接关系。所谓矩阵其实就是二维数组。
对于有 个顶点的图 来说,我们可以用一个 的矩阵 来表示 中各顶点的相邻关系,如果 和 之间存在边(或弧),则 ,否则 。下图为有向图 和无向图 对应的邻接矩阵:
image.pngimage.png
image.png
image.png
一个图的邻接矩阵是唯一的,矩阵的大小只与顶点个数 有关,是一个 的矩阵。前面我们已经介绍过,在无向图里,如果顶点 和 之间有边,则可认为顶点 到 有边,顶点 到 也有边。对应到邻接矩阵里,则有 。因此我们可以发现,无向图的邻接矩阵是一个对称矩阵。
在邻接矩阵上,我们可以直观地看出两个顶点之间是否有边(或弧),并且很容易求出每个顶点的度,入度和出度。
这里我们以 为例,演示下如何利用邻接矩阵计算顶点的入度和出度。顶点的出度,即为邻接矩阵上点对应行上所有值的总和,比如顶点 出度即为 ;而每个点的入度即为点对应列上所有值的总和,比如顶点 对应的入度即为 。
image.png image.png接下来我们就先一起学习构造和使用邻接矩阵的方法。邻接矩阵是一个由 和 构成的矩阵。处于第 行、第 列上的元素 和 分别代表顶点 到 之间存在或不存在一条有向边。
显然在构造邻接矩阵的时候,我们需要实现一个整型的二维数组。由于当前的图还是空的,因此我们还要把这个二维数组中的每个元素都初始化为 。
在构造好了一个图的结构后,我们需要把图中各边的情况对应在邻接矩阵上。实际上,这一步的实现非常简单,当从顶点 到 上存在边时,我们只要把二维数组对应的位置置为 就好了。
image.png用邻接矩阵来构建图需要如下几步,我们可以用二维数组G
来表示一个图。
3.2 初始化
初始化的过程很简单,只需要把数组初始化为 即可。可以借助memset
来快速地将一个数组中的所有元素都初始化为 。
memset(G, 0, sizeof(G));
注意,memset
只能用来初始化 和 ,并且需要加上头文件cstring
。
上面的代码等价于:
for (int i = 0; i < N1; i++) { // N1 为数组第一维大小
for (int j = 0; j < N2; j++) { // N2 为数组第二维大小
G[i][j] = 0;
}
}
当然我们平常使用邻接矩阵的时候下标只用 到 或者 到 (这个看题目中点的编号)。
3.3 邻接矩阵元素的修改
插入边
如果插入一条无向边 ,只需要
G[u][v] = 1;
G[v][u] = 1;
也可以写成G[u][v] = G[v][u] = 1;
。
如果插入一条有向边 ,只需要G[u][v] = 1;
。
访问边
如果G[u][v] = 1
,说明有一条从 到 的边,否则没有从 到 的边。
3.4 带权值的图
在前面的课程中,图中的边都只是用来表示两个点之间是否存在关系,而没有体现出两个点之间关系的强弱。比如在社交网络中,不能单纯地用 、 来表示两个人否为朋友。当两个人是朋友时,有可能是很好的朋友,也有可能是一般的朋友,还有可能是不熟悉的朋友。
我们用一个数值来表示两个人之间的朋友关系强弱,两个人的朋友关系越强,对应的值就越大。而这个值就是两个人在图中对应的边的权值,简称 边权。对应的图我们称之为 带权图。
如下就是一个带权图,我们把每条边对应的边权标记在边上:
image.png带权图也分成带权有向图和带权无向图。前面学到的关于图的性质在带权图上同样成立。实际上,我们前面学习的图是一种特殊带权图,只不过图中所有边的权值只有 一种;而在带权图中,边的权值可以是任意的。
用邻接矩阵存储带权图和之前的方法一样,用G[a][b]
来表示 和 之间的边权(我们需要用一个数值来表示边不存在,如0
)。同样,对于无向图,这个矩阵依然是对称的。
如上所示,左边的图对应的右边的邻接矩阵。
网友评论