算法详解(卷2):图算法和数据结构
上QQ阅读APP看书,第一时间看更新

我们可以采用不止一种方法对图进行编码,以便在算法中使用。

在本系列图书中,我们主要采用图的“邻接列表”表示形式(见1.4.1节),但读者同时也应该熟悉“邻接矩阵”表示形式(见1.4.2节)。

图的邻接列表表示形式是我们在本系列图书中采用的主要形式。

邻接列表的组成部分

1.一个包含图的顶点集的数组。

2.一个包含图的边集的数组。

3.对于每条边,有一个指针指向它的每个顶点。

4.对于每个顶点,有一个指针指向它的每条关联边。

邻接列表表示形式可以简化为两个数组(或者链表):一个用于记录顶点,另一个用于记录边。这两个数组以一种自然的方式交叉引用对方,每条边都有相关联的指针指向它的每个顶点,每个顶点都有指针指向以它为顶点的边。

对于有向图,每条边记录哪个顶点是尾顶点,哪个顶点是头顶点。每个顶点v维护两个指针数组,一个表示外向边(v是尾顶点),另一个表示入射边(v是头顶点)。

邻接列表表示形式的内存需求是怎么样的呢?

小测验1.2

图的邻接列表表示形式需要多大的空间?(用顶点数量和边的数量的函数形式表示。)

(a)Θ (n)

(b)Θ (m)

(c)Θ (m+n)

(d)Θ (n2)

(正确答案和详细解释参见1.4.4节。)

考虑一个无向图G=(V, E),它具有n个顶点且没有平行边,并用1,2,3,…,n标识它的顶点。G的邻接矩阵表示形式是一个正方形的n×n矩阵A,相当于一个二维数组,其元素是0或1。每个元素Aij被定义为:

如果边(i, j)属于E,则Aij=1;

否则,Aij=0。

因此,邻接矩阵用一个位表示每一对顶点,标记这对顶点之间是否存在边(见图1.7)。

图1.7 图的邻接矩阵为每一对顶点维护一个位,
表示是否存在一条边连接这两个顶点

我们可以很方便地在图的邻接矩阵表示形式中添加一些“花样”,提示更多的信息。

•平行边。如果在同一对顶点之间有多条边,那么Aij可以定义为顶点ij之间的边数。

•权重图。类似地,如果每条边(i, j)具有权重wij,例如表示价格或距离,那么每个元素Aij可以存储wij

•有向图。对于有向图G,邻接矩阵的每个元素被定义为:

如果边(i, j)属于E,那么Aij=1;

否则,Aij=0。

现在,“边(i, j)”表示从ij的有向边。每个无向图的邻接矩阵都是对称的,但有向图的邻接矩阵通常是不对称的。

邻接矩阵的内存需求是怎么样的呢?

小测验1.3

图的邻接矩阵需要占据多大的内存空间呢?(用顶点数量n和边的数量m的函数形式表示。)

(a)Θ (n)

(b)Θ (m)

(c)Θ (m+n)

(d)Θ (n2)

(正确答案和详细解释参见1.4.4节。)

图有两种不同的表示形式也是一件令人烦恼的事情。我们不禁会问:哪种形式更好呢?答案通常是“取决于具体情况”。首先,它取决于图的密度,也就是边的数量与顶点的数量的相对数量比。小测验1.2和小测验1.3向我们提示邻接矩阵用于表示稠密图是非常高效的,但用来表示稀疏图是极为浪费的。其次,它取决于我们要支持的操作。综合考虑之下,对于本系列图书所描述的算法和应用,邻接列表是更为合理的表示形式。

大多数与图有关的算法涉及图的探索。邻接列表非常适合进行图的探索,当我们访问一个顶点时,邻接列表立即就能告诉我们接下来可以在哪几个步骤中进行选择。如果只能使用图的邻接矩阵表示形式,那么如何才能确定一个特定的顶点相关联的边是哪几条呢?邻接矩阵也有一些合适的应用,但本系列图书并不会讨论这些应用。例如,我们可以通过探索图的邻接矩阵,立即算出每对顶点的公共邻居的数量。

当前对快速图元的兴趣大多来自于巨大的稀疏网络。例如,我们可以考虑Web图(见1.2节),其中顶点对应于Web页面,有向边对应于超链接。对这种类型的图的大小进行精确的测量是非常困难的,但还是在现代计算机的能力范围之内。保守地估计,顶点数量的下界大约是100亿(1010)。存储和读取如此长度的数组需要巨量的计算资源,不过现代计算机还是能够胜任。但是,这种图的邻接矩阵的大小规模达到了百万的三次方的100倍(1020)。对于当前的技术而言,存储和处理如此巨量的数据是力所不及的。但是,Web图是稀疏图,从一个顶点出发的边的平均数量小于100。因此,Web图的邻接列表的内存需求大约为1012(万亿级)。这个规模对于笔记本计算机来说可能过于庞大,但对于最前沿的数据处理系统而言,还是在它的能力范围之内的。例如,Google原创的用于评估网页重要性的网页排名算法的本质就依赖于Web图的高效搜索。

小测验1.2的答案

正确答案:(c)。邻接列表表示形式所需要的空间与图的大小(即顶点的数量加上边的数量)呈线性关系,是比较理想的。警告:邻接矩阵增加了一个维度,因此它的主要约束条件更大。前导的常数因子要比邻接矩阵大一个数量级。要理解这一点略有难度,我们逐个观察它的4个组成部分。顶点数组和边数组的长度分别是nm,分别需要Θ (n)和Θ (m)的空间。第3个组成部分将两个指针与每条边相关联(每个顶点与一个指针关联),这2m个指针产生了额外的Θ (m)的空间需求。

第4个组成部分可能会令我们感到困惑。无论怎样,总共n个顶点中的每一个顶点均可以参与到多达n−1条的边中,即可以与其他的每个顶点形成一条边,因此看上去会导致空间需求的上界为Θ (n2)。这种平方级的上界对于极为稠密的图而言是准确的,但对于稀疏图来说却显得过大了。关键在于:对于第4个组成部分中的每个“顶点→边”指针,在第3个组成部分中都存在一个对应的“边→顶点”指针。如果边e指向顶点v,那么边e就有一个指向这个顶点v的指针。反之,顶点v也有一个指向它的关联边e的指针。我们可以得出结论,即第3个组成部分和第4个组成部分中的指针具有一对一的对应关系,因此它们需要相同数量的空间,也就是Θ (m)。最终的空间需要:

无论图是否为连接图,以及图是否具有平行边,这个Θ (m+n)的上界均适用。如果是连接图,那么m≥n−1(小测验1.1的结论),因此可以用Θ (m)代替Θ (m+n)。

小测验1.3的答案

正确答案:(d)。邻接矩阵的一种简单存储方法是一个n×n的二维位数组,这需要Θ (n2)的空间,不过还有一个较小的隐藏常量因子。对于稠密图,边的数量接近于n的平方,邻接矩阵所需要的空间大致与图的大小呈线性关系。但是,对于边的数量大致与n呈线性关系的稀疏图,邻接矩阵表示形式太浪费空间了。对稀疏矩阵(即存在大量0的矩阵)使用一些存储和操作技巧,可以减少这种浪费。例如,MATLAB和Python的SciPy程序包均支持稀疏矩阵表示形式。