什么是图
在一个社交网络中,每个帐号和他们之间的关系构成了一张巨大的网络,就像下面这张图:
那么在电脑中,我们要用什么样的数据结构来保存这个网络呢?这个网络需要用一个之前课程里未提到过的数据结构,也就是接下来要讲解的 图 结构来保存。
到底什么是图?图是由一系列顶点和若干连结顶点集合内两个顶点的边组成的数据结构。数学意义上的图,指的是由一系列点与边构成的集合,这里我们只考虑有限集。通常我们用
G
=
(
V
,
E
)
\ G = (V, E)
G=(V,E) 表示一个图结构,其中
V
\ V
V 表示点集,
E
\ E
E 表示边集。
在顶点集合所包含的若干个顶点之间,可能存在着某种两两关系——如果某两个点之间的确存在这样的关系的话,我们就在这两个点之间连边,这样就得到了边集的一个成员,也就是一条边。对应到社交网络中,顶点就是网络中的用户,边就是用户之间的好友关系。
如果用边来表示好友关系的话,对于微信这种双向关注的社交网络没有问题,但是对于微博这种单向关注的要如何表示呢?
于是引出了两个新的概念:有向边和无向边。
简而言之,一条有向边必然是从一个点指向另一个点,而相反方向的边在有向图中则不一定存在;而有的时候我们并不在意构成一条边的两个顶点具体谁先谁后,这样得到的一条边就是无向边。就像在微信中,
A
\ A
A 是
B
\ B
B 的好友,那
B
\ B
B 也一定是
A
A
A的好友,而在微博中,
A
A
A 关注
B
B
B 并不意味着
B
\ B
B 也一定关注
A
A
A。
对于图而言,如果图中所有边都是无向边,则称为无向图,反之称为有向图。
简而言之,无向图中的边是“好友”,而有向图中的边是“关注”。一般而言,我们在数据结构中所讨论的图都是有向图,因为有向图相比无向图更具有代表性。
实际上,无向图可以由有向图来表示。如果 \ AB AB 两个点之间存在无向边的话,那用有向图也可以表示为
A
B
AB
AB 两点之间同时存在
A
A
A 到
B
B
B 与
B
B
B 到
A
A
A 两条有向边。
仍然以社交网络举例:虽然微博中并不存在明确定义的好友关系,但是一般情况下,如果你和另一个 ID 互相关注的话,那么我们也可以近似认为,你和 TA 是好友。
现在,我们已经了解了图的定义和有向图、无向图的概念。在接下来的课程中,我们会继续学习图的一些基本概念,并学习如何实现图结构的存储和输出。
在有向图中,我们需要学习顶点的 入度 和 出度 这两个概念。顶点的入度是指以顶点为弧头的弧的数目,也就是以该顶点为终点的弧的数目;顶点的出度是指以顶点为弧尾的弧的数目,也就是以该顶点为起点的弧的数目。需要注意的是,在有向图里,顶点的度为入度与出度之和。
邻接矩阵 和 邻接表
什么是邻接矩阵呢?所谓邻接矩阵存储结构就是用一维数组存储图中顶点的信息,用矩阵表示图中各顶点之间的邻接关系。
对于有 n 个顶点的图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E) 来说,我们可以用一个
n
×
n
n \times n
n×n 的矩阵
A
A
A 来表示
G
G
G 中各顶点的相邻关系,如果
v
i
v_i
vi 和
v
j
v_j
vj 之间存在边(或弧),则
A
[
i
]
[
j
]
A[i][j]
A[i][j] = 1,否则
A
[
i
]
[
j
]
A[i][j]
A[i][j] = 0。下图为有向图
G
1
G_1
G1 和无向图
G
2
G_2
G2 对应的邻接矩阵:
图的邻接矩阵是唯一的,矩阵的大小只与顶点个数
N
\N
N有关,是一个
N
×
N
N \times N
N×N 的矩阵。前面我们已经介绍过,在无向图里,如果顶点
v
i
v_i
vi 和
v
j
v_j
vj 之间有边,则可认为顶点
v
i
v_i
vi 到
v
j
v_j
vj 有边,顶点
v
j
v_j
vj 到
v
i
\ v_i
vi 也有边。对应到邻接矩阵里,则有
A
[
i
]
[
j
]
=
A
[
j
]
[
i
]
=
1
\ A[i][j] = A[j][i] = 1
A[i][j]=A[j][i]=1。因此我们可以发现,无向图的邻接矩阵是一个对称矩阵。图的邻接矩阵是唯一的,矩阵的大小只与顶点个数
N
N
N 有关,是一个
N
×
N
\ N \times N
N×N 的矩阵。前面我们已经介绍过,在无向图里,如果顶点
v
i
v_i
vi 和
v
j
v_j
vj 之间有边,则可认为顶点
v
i
v_i
vi 到
v
j
v_j
vj 有边,顶点
v
j
v_j
vj 到
v
i
v_i
vi 也有边。对应到邻接矩阵里,则有
A
[
i
]
[
j
]
=
A
[
j
]
[
i
]
=
1
A[i][j]= A[j][i] = 1
A[i][j]=A[j][i]=1。因此我们可以发现,无向图的邻接矩阵是一个对称矩阵。图的邻接矩阵是唯一的,矩阵的大小只与顶点个数
N
N
N 有关,是一个
N
×
N
N \times N
N×N 的矩阵。前面我们已经介绍过,在无向图里,如果顶点
v
i
v_i
vi 和
v
j
v_j
vj 之间有边,则可认为顶点
v
i
v_i
vi 到
v
j
v_j
vj 有边,顶点
v
j
v_j
vj 到
v
i
v_i
vi 也有边。对应到邻接矩阵里,则有
A
[
i
]
[
j
]
=
A
[
j
]
[
i
]
=
1
A[i][j]= A[j][i] = 1
A[i][j]=A[j][i]=1 。因此我们可以发现,无向图的邻接矩阵是一个对称矩阵。
在邻接矩阵上,我们可以直观的看出两个顶点之间是否有边(或弧),并能容易求出每个顶点的度,入度和出度。
这里我们以
G
1
G_1
G1 为例,演示下如何利用邻接矩阵计算顶点的入度和出度。顶点的出度,即为邻接矩阵上点对应行上所有值的总和,比如顶点 1 出度即为0+1+1+1=3;而每个点的入度即为点对应列上所有值的总和,比如顶点 3 对应的入度即为 1+0+0+1=2。
学习了邻接矩阵,我们再来学习邻接表吧。邻接表是图的一种顺序存储与链式存储相结合的存储方法。我们给图
G
G
G 中的每个顶点建立一个单链表,第
i
i
i 个单链表中的结点表示依附于顶点
v
i
v_i
vi 的边(对于有向图是以
v
i
v_i
vi 为起点的弧)。所有单链表的表头结点都存储在一个一维数组中,以便于顶点的访问。下图为图
G
1
G_1
G1 对应的邻接表。
在无向图的邻接表中,顶点
v
i
v_i
vi 的度为第
i
i
i 个单链表中的结点数;而在有向图中,第
i
i
i 个单链表中的结点数表示的是顶点
v
i
v_i
vi 的出度,如果要求入度,则要遍历整个邻接表。另外,在邻接表中,我们很容易就能知道某一顶点和哪些顶点相连接。
学习完两种存储结构,可能你会有这样的疑问:那我们什么时候用邻接矩阵,什么时候用邻接表呢?
我们可以看到,邻接矩阵存储结构最大的优点就是简单直观,易于理解和实现。其适用范围广泛,有向图、无向图、混合图、带权图等都可以直接用邻接矩阵表示。另外,对于很多操作,比如获取顶点度数,判断某两点之间是否有连边等,都可以在常数时间内完成。
然而,它的缺点也是显而易见的:从以上的例子我们可以看出,对于一个有
n
n
n 个顶点的图,邻接矩阵总是需要
n
2
n^2
n2 的存储空间。当边数很少的时候,就会造成空间的浪费。
因此,具体使用哪一种存储方式,要根据图的特点来决定:如果是稀疏图,我们一般用邻接表来存储,这样可以节省空间;如果是稠密图,考虑到邻接表中要附加链域,我们一般用邻接矩阵来存储。
邻接矩阵的实现
在了解了邻接矩阵和邻接表的性质和作用之后,接下来我们就先一起学习构造和使用邻接矩阵的方法。
首先来学习邻接矩阵的实现,我们都知道,邻接矩阵是一个由 1 和 0 构成的矩阵。处于第
i
i
i 行、第
j
j
j 列上的元素
1
1
1 和
0
0
0 分别代表顶点
i
i
i 到
j
j
j 之间存在或不存在一条又向边。
显然在构造邻接矩阵的时候,我们需要实现一个整形的二维数组。由于当前的图还是空的,因此我们还要把这个二维数组中的每个元素都初始化为
0
0
0。
在构造好了一个图的结构后,我们需要把图中各边的情况对应在邻接矩阵上。实际上,这一步的实现非常简单,当从顶点
x
x
x x 到
y
y
y上存在边时,我们只要把二维数组对应的位置置为
1
1
1 就好了。
另外,我们还要学习如何把存储好的图的邻接矩阵输出出来。可能同学们已经想到了,我们只要用两层 for 循环把这个二维数组的所有元素输出就好了。当然,为了使输出是一个邻接矩阵的形状,我们需要在每一次内循环结束时输出一个换行,并在输出每一个元素之后再输出一个空格。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76966.html