Part I. Foundations
Chapter 2. Getting Started
2.1 插入排序(Insertion sort)
输入: 大小为
n
n
n 的数字序列
⟨
a
1
,
a
2
,
…
,
a
n
⟩
\langle a_1,a_2,\dots,a_n \rangle
⟨a1,a2,…,an⟩。
输出: 输入序列的重新排序
⟨
a
1
′
,
a
2
′
,
…
,
a
n
′
⟩
\langle a_1^{'},a_2^{'},\dots,a_n^{'} \rangle
⟨a1′,a2′,…,an′⟩,满足条件
a
1
′
≤
a
2
′
≤
⋯
≤
a
n
′
a_1^{'} \leq a_2^{'} \leq \dots \leq a_n^{'}
a1′≤a2′≤⋯≤an′。
将要进行排序的序列中的数字可称为键(key)。
区分伪代码(pseudocode)与真实代码:
① 在伪代码中,我们采用最清晰、最简洁的表达方式来描述给定算法。有时会直接使用短语或句子。
② 伪代码通常不会涉及软件工程的问题。为了更简洁地传达算法的本质,通常会忽略数据抽象、模块化、错误处理等问题。
插入排序是一种对少量元素进行排序的有效算法。插入排序的工作机制,与许多人玩牌时对手上扑克牌的排序类似。开始时,左手空着,牌面朝下放在桌子上。接着,一次从桌上取出一张卡,将其插入左手的正确位置。在任何时候,左手拿着的牌都是排序好的。
INSERTION-SORT( A A A)
1 for j = 2 j=2 j=2 to A . l e n g t h A.length A.length
2 k e y = A [ j ] key=A[j] key=A[j]
3 // Insert A [ j ] A[j] A[j] into the sorted sequence A [ 1.. j − 1 ] A[1..j-1] A[1..j−1].
4 i = j − 1 i=j-1 i=j−1
5 while i > 0 i>0 i>0 and A [ i ] > k e y A[i]>key A[i]>key
6 A [ i + 1 ] = A [ i ] A[i+1]=A[i] A[i+1]=A[i]
7 i = i − 1 i=i-1 i=i−1
8 A [ i + 1 ] = k e y A[i+1]=key A[i+1]=key
根据上面的伪代码,C++ 代码如下:
void InsertionSort(vector<int>& A)
{
for(int j=1; j<A.size(); ++j)
{
int key = A[j];
int i = j - 1;
while(i>=0 && A[i]>key)
{
A[i+1] = A[i];
--i;
}
A[i+1] = key;
}
}
循环不变式(loop invariant)与插入排序的正确性
我们将 A [ 1.. j − 1 ] A[1..j-1] A[1..j−1] 的特性声明为循环不变式:在第 1–8 行的 for 循环的每次迭代开始时,子数组 A [ 1.. j − 1 ] A[1..j-1] A[1..j−1] 由最初在 A [ 1.. j − 1 ] A[1..j-1] A[1..j−1] 中的元素组成,但,是按顺序排序好的。
我们使用循环不变式来验证算法的正确性。下面介绍关于循环不变式的 3 个要点:
初始: 在循环的第一次迭代之前,循环不变式为真。
保持: 如果在循环的一次迭代之前为真,那么在下一次迭代之前保持为真。
终止: 当循环终止时,不变式为我们提供了有用的性质,这种性质有助于验证算法是正确的。
上述内容前两个性质与数学归纳法(mathematical induction)相似。 终止性质不同于我们通常使用数学归纳法的方式,在数学归纳法中,我们无限地应用归纳步骤。 在这里,当循环终止时,我们停止“归纳”。
伪代码约定(Pseudocode conventions)
- 缩进表示块结构。例,上述的 for 语句、while 语句等下面的缩进。
- 循环结构 while、for 和 repeat-until,条件结构 if-else 这两者的解释类似于 C,C++,Java,Python 和 Pascal。
- 符号 “//” 表示该行后面余下的部分是注释。
- 形式如 i = j = e i=j=e i=j=e 的多重赋值,将表达式 e e e 的值赋值给变量 i i i 和 j j j。等价于 j = e j=e j=e, i = j i=j i=j。
- 变量(如 i i i, j j j 和 k e y key key)是给定程序的局部变量。没有明确指出,不使用全局变量。
- 我们通过指定数组名称和方括号中的索引来访问数组元素,如 A [ i ] A[i] A[i]。记号“ . . .. ..”用于表示数组中的值范围,如 A [ 1.. j ] A[1..j] A[1..j]。
- 我们通常将复合数据组织到由属性(attribute)组成的对象(object)中。我们使用面向对象的编程语言中的语法访问特定的属性:对象名称,后跟一个点,然后是属性名。例,
A
.
l
e
n
g
t
h
A.length
A.length。
我们将代表数组或对象的变量视为,指向代表数组或对象的数据的指针。对于对象 x x x 的所有属性 f f f,设置 y = x y=x y=x 会使 y . f y.f y.f 等于 x . f x.f x.f。即,在赋值 y = x y=x y=x 之后, x x x 和 y y y 指向同一对象。
我们的属性表示法可以是“级联”的。例如,如果我们赋值 y = x . f y=x.f y=x.f,则 x . f . g x.f.g x.f.g 与 y . g y.g y.g 相同。
有时,指针根本不会指向任何对象。在这种情况下,我们给它一个特殊值 NIL。 - 我们将参数传递给过程,按值(by value)传递。传递对象时,将复制指向代表该对象的数据的指针,传递对象的属性时,不这样。同样,数组是通过指针传递的。
- return 语句将控制权转移回调用过程中的调用点。伪代码允许在单个 return 语句中返回多个值。
- 布尔运算符 “and” 和 “or” 存在短路(short circuiting)运算现象。
- 关键字 error 表示出错,因为被调用的过程的条件错误。调用过程负责处理错误,我们不需要指定措施。
练习
2.1-1 以图2.2为样例,说明数组
A
=
⟨
31
,
41
,
59
,
26
,
41
,
58
⟩
A=\langle 31,41,59,26,41,58 \rangle
A=⟨31,41,59,26,41,58⟩ 上的 INSERTION-SORT 操作。
解: 第1步:31,41,59,26,41,58
第2步:31,41,59,26,41,58
第3步:26,31,41,59,41,58
第4步:26,31,41,41,59,58
第5步:26,31,41,41,58,59
2.1-2 重写 INSERTION-SORT 过程,以非递增而不是非递减的顺序进行排序。
解: 将第 5 行的 while
i
>
0
i>0
i>0 and
A
[
i
]
>
k
e
y
A[i]>key
A[i]>key 改为 while
i
>
0
i>0
i>0 and
A
[
i
]
<
k
e
y
A[i]<key
A[i]<key,其余不变。
2.1-3 考虑查找问题(searching problem):
输入: 大小为
n
n
n 的序列
A
=
⟨
a
1
,
a
2
,
…
,
a
n
⟩
A=\langle a_1,a_2,\dots,a_n \rangle
A=⟨a1,a2,…,an⟩ 和一个值
v
v
v。
输出: 使
v
=
A
[
i
]
v=A[i]
v=A[i] 成立的索引
i
i
i,或者特殊值 NIL(如果
v
v
v 不在
A
A
A 中)。
写出用于线性查找(linear search)的伪代码,扫描整个序列查找
v
v
v。使用循环不变式证明你的算法的正确性。确保你的循环不变式满足 3 个必要的性质。
解: LINEAR-SEARCH(
A
A
A,
v
v
v)
1 for
i
=
1
i=1
i=1 to
A
.
l
e
n
g
t
h
A.length
A.length
2 if
v
=
=
A
[
i
]
v==A[i]
v==A[i]
3 return
i
i
i
4 return NIL
证明:循环不变式为:
A
[
1..
i
−
1
]
A[1..i-1]
A[1..i−1] 不包含
v
v
v。
初始: 在循环第一次迭代之前,
i
=
1
i=1
i=1,则
A
[
1..
i
−
1
]
A[1..i-1]
A[1..i−1] 为空,不包含
v
v
v,循环不变式为真。
保持: 当
v
v
v 不等于
A
[
i
]
A[i]
A[i] 时,则
A
[
1..
i
]
A[1..i]
A[1..i] 不包含
v
v
v,在循环的下一次迭代前,循环不变式为真;当
v
v
v 等于
A
[
i
]
A[i]
A[i] 时,循环终止。
终止: 当
v
v
v 等于
A
[
i
]
A[i]
A[i] 或
i
=
A
.
l
e
n
g
t
h
+
1
i=A.length+1
i=A.length+1(即
A
=
n
+
1
A=n+1
A=n+1)时,循环终止。当
v
v
v 等于
A
[
i
]
A[i]
A[i] 时,条件
v
=
A
[
i
]
v=A[i]
v=A[i] 成立,返回
i
i
i。当
i
=
n
+
1
i=n+1
i=n+1 时,
A
[
1..
n
]
A[1..n]
A[1..n] 不包含
v
v
v,返回 NIL。
所以,算法正确。
2.1-4 考虑问题:将两个
n
n
n 位二进制整数相加,这两个整数存储在两个
n
n
n 位元素数组
A
A
A 和
B
B
B 中。两个整数之和应以二进制形式存储在
n
+
1
n+1
n+1 位元素数组
C
C
C 中。正式表述问题,写出用于将两个整数相加的伪代码。
解: 正式表述问题如下:
输入: 二进制整数
a
a
a 的表示形式为
a
n
…
a
2
a
1
a_n\dots a_2 a_1
an…a2a1,存储在大小为
n
n
n 的数组
A
=
⟨
a
1
,
a
2
,
…
,
a
n
⟩
A = \langle a_1,a_2,\dots,a_n \rangle
A=⟨a1,a2,…,an⟩,二进制整数
b
b
b 的表示形式为
b
n
…
b
2
b
1
b_n\dots b_2 b_1
bn…b2b1,存储在大小为
n
n
n 的数组
B
=
⟨
b
1
,
b
2
,
…
,
b
n
⟩
B=\langle b_1,b_2,\dots,b_n\rangle
B=⟨b1,b2,…,bn⟩。
输出: 大小为
n
+
1
n+1
n+1 的数组
C
=
⟨
c
1
,
c
2
,
…
,
c
n
,
c
n
+
1
⟩
C=\langle c_1,c_2,\dots,c_n,c_{n+1}\rangle
C=⟨c1,c2,…,cn,cn+1⟩,满足条件
c
=
a
+
b
c = a+b
c=a+b,其中,二进制整数
c
c
c 的表示形式为
c
n
+
1
c
n
…
c
2
c
1
c_{n+1}c_n\dots c_2 c_1
cn+1cn…c2c1。
伪代码如下:
ADD-BINARY-INTEGERS(
A
A
A,
B
B
B)
1
n
=
A
.
l
e
n
g
t
h
n=A.length
n=A.length
2
C
=
[
1..
n
+
1
]
C=[1..n+1]
C=[1..n+1]
3
c
a
r
r
y
=
0
carry = 0
carry=0
4 for
i
=
1
i=1
i=1 to
n
n
n
5
n
u
m
=
A
[
i
]
+
B
[
i
]
+
c
a
r
r
y
num=A[i]+B[i]+carry
num=A[i]+B[i]+carry
6
C
[
i
]
=
n
u
m
%
2
C[i] = num \% 2
C[i]=num%2
7
c
a
r
r
y
=
n
u
m
/
2
carry=num/2
carry=num/2
8
C
[
n
+
1
]
=
c
a
r
r
y
C[n+1]=carry
C[n+1]=carry
9 return
C
C
C
2.2 分析算法(Analyzing algorithms)
分析算法意味着预估算法所需的资源。有时,诸如内存、通信带宽或计算机硬件之类的资源是最主要的问题,但是最常见需要测量的资源是计算时间。
在分析算法之前,我们必须具有相关的实现技术的模型,包括技术所需的资源和代价的模型。本书主要将一种通用单处理器、随机存取机器(RAM: random-access machine )计算模型作为实现技术,通过计算机程序实现我们的算法。在 RAM 模型中,指令是一个接一个地执行的,没有并发操作。
① RAM 模型包含实际计算机中常见的指令:算术指令(加,减,乘,除,取余,向下取整 (floor),向上取整(ceiling)),数据移动指令(加载,存储,复制)和控制指令(条件和非条件转移,子程序调用与返回)。每条这样的指令花费常量的时间。
② RAM 模型中的数据类型为整数和浮点数(用于存储实数)。假设数据的每个字(word)的大小有限制。
③ 在 RAM 模型中,我们不对现代计算机中常见的存储器层次进行建模。即不对高速缓存或虚拟内存建模。
④ RAM 模型分析通常能够很好地预测实际计算机上的性能。
插入排序算法的分析
INSERTION-SORT 过程时间开销与输入有关:序列的长度、序列已排序的程度等。通常,算法的时间开销随着输入规模的增加而增加,因此习惯上将程序的运行时间表示为输入规模的函数。
输入规模(input size)的概念取决于正在研究的问题。例如:
① 排序或计算离散傅立叶变换,最自然的度量是输入的元素个数(number of items in the input)。
② 两个整数相乘,最佳度量是用普通二进制表示法表示的输入数字的总位数(total number of bits)。
③ 有时,用两个数来表示输入规模更合适。如,算法输入是图形,可以通过图中的顶点数和边数来表示。
对于正在研究的每个问题,我们需要指出输入规模的度量标准。
算法的运行时间(running time)是指,在特定输入时执行的基本操作数或“步骤”的数量。
定义“步骤”的概念应尽可能独立于机器。采用以下观点:执行伪代码的每一行需要固定的时间;假定每次执行第
i
i
i 行伪代码都花费时间
c
i
c_i
ci,其中
c
i
c_i
ci 是一个常数。 这种观点与 RAM 模型是一致的,并且反映了伪代码在大部分实际计算机上是如何实现的。
设
t
j
t_j
tj 为第 5 行中 while 循环执行的测试次数。计算 INSERTION-SORT 过程总运行时间
T
(
n
)
T(n)
T(n):
T
(
n
)
=
c
1
n
+
c
2
(
n
−
1
)
+
c
4
(
n
−
1
)
+
c
5
∑
j
=
2
n
t
j
+
c
6
∑
j
=
2
n
(
t
j
−
1
)
+
c
7
∑
j
=
2
n
(
t
j
−
1
)
+
c
8
(
n
−
1
)
T(n)=c_1n+c_2(n-1)+c_4(n-1)+c_5\sum_{j=2}^{n}t_j + c_6\sum_{j=2}^{n}(t_j-1) + c_7\sum_{j=2}^{n}(t_j-1) + c_8(n-1)
T(n)=c1n+c2(n−1)+c4(n−1)+c5j=2∑ntj+c6j=2∑n(tj−1)+c7j=2∑n(tj−1)+c8(n−1)
若输入数组是排序好的,则对于
j
j
j 的每个值,在第 5 行,
i
=
j
−
1
i=j-1
i=j−1 时,都有
A
[
i
]
⩽
k
e
y
A[i]\leqslant key
A[i]⩽key。此时,最佳运行时间可以表示为
a
n
+
b
an+b
an+b,常量
a
a
a 和
b
b
b 依赖于
c
i
c_i
ci。这是
n
n
n 的一个线性函数(linear function)。
若输入数组是逆序排序的。此时,最坏运行时间可以表示为
a
n
2
+
b
n
+
c
an^2+bn+c
an2+bn+c,常量
a
a
a、
b
b
b 和
c
c
c 依赖于
c
i
c_i
ci。这是一个关于
n
n
n 的二次函数(quadratic function)。
最坏情况与平均情况分析
在本书中,我们一般考虑算法的最坏情况运行时间(worst-case running time)。理由如下:
① 算法的最坏情况运行时间为我们提供了在任何输入下运行时间的上限。
② 对于某些算法来说,最坏情况出现相当频繁。
③ 通常,“平均情况”与最坏情况大致一样差。
在某些特定情况下,我们对算法的平均情况(average-case)运行时间感兴趣。在本书中,我们将看到概率分析(probabilistic analysis)技术应用于各种算法。我们有时可以使用随机算法(randomized algorithm),该算法可以作出随机的选择,以进行概率分析并生成预期的(expected)运行时间。
增长量级
我们已经使用了一些简化抽象来简化对 INSERTION-SORT 过程的分析。现在进一步简化抽象:真正使我们感兴趣的使运行时间的增长率(rate of growth),或称为增长量级(order of growth)。
对于插入排序,当我们忽略低阶项和前导项的常数系数时,只剩下前导项的
n
2
n^2
n2 因子。我们记插入排序的最坏情况下的运行时间为
Θ
(
n
2
)
\Theta(n^2)
Θ(n2)(发音:“theta of n-squared”)。
练习
2.2-1 用
Θ
\Theta
Θ 形式表示函数
n
3
/
1000
−
100
n
2
−
100
n
+
3
n^3/1000-100n^2-100n+3
n3/1000−100n2−100n+3。
解:
Θ
(
n
3
)
\Theta(n^3)
Θ(n3)
2.2-2 考虑对数组
A
A
A 中的
n
n
n 个数进行排序:在
A
A
A 中找出最小的元素,将它与
A
[
1
]
A[1]
A[1] 中的元素交换。接着,找出
A
A
A 中的第二小的元素,将它与
A
[
2
]
A[2]
A[2] 中的元素交换。对
A
A
A 中的前
n
−
1
n-1
n−1 个元素继续这一过程。写出此算法的伪代码,该算法称为选择排序(selection sort)。此算法的循环不变式是什么?为什么只需在前
n
−
1
n-1
n−1 个元素上运行,而不是在所有
n
n
n 个元素上运行?给出选择算法的最佳情况和最坏情况的运行时间,以
Θ
\Theta
Θ 形式写出。
解: SELECTION-SORT(
A
A
A)
1 for
i
=
1
i=1
i=1 to
(
A
.
l
e
n
g
t
h
−
1
)
(A.length-1)
(A.length−1)
2
m
i
n
I
n
d
e
x
=
i
minIndex = i
minIndex=i
3 for
j
=
i
+
1
j=i+1
j=i+1 to
A
.
l
e
n
g
t
h
A.length
A.length
4 if
A
[
j
]
<
A
[
m
i
n
I
n
d
e
x
]
A[j] < A[minIndex]
A[j]<A[minIndex]
5
m
i
n
I
n
d
e
x
=
j
minIndex = j
minIndex=j
6 exchange
A
[
i
]
A[i]
A[i] with
A
[
m
i
n
I
n
d
e
x
]
A[minIndex]
A[minIndex]
该算法的循环不变式为:
A
[
1..
i
]
A[1..i]
A[1..i] 是从小到大排序好的。
因为
A
[
1..
n
−
1
]
A[1..n-1]
A[1..n−1] 是从小到大排序好的,且
A
[
1..
n
−
1
]
A[1..n-1]
A[1..n−1] 中的所有元素都不大于
A
[
n
]
A[n]
A[n] 中的元素,所以
A
[
1..
n
]
A[1..n]
A[1..n] 是从小到大排序好的。所以只需在前
n
−
1
n-1
n−1 个元素上运行。
算法的最佳情况运行时间:
Θ
(
n
2
)
\Theta(n^2)
Θ(n2)
算法的最坏情况运行时间:
Θ
(
n
2
)
\Theta(n^2)
Θ(n2)
2.2-3 再次考虑线性查找算法(见练习 2.1-3)。假设待查找的元素是数组中任一元素的可能性是相等的,在平均情况下,输入序列中有多少个元素需要检查?在最坏情况下呢?用
Θ
\Theta
Θ 形式表示,线性查找在平均情况和最坏情况的运行时间是怎样的?对你的答案加以说明。
解: 假设数组有
n
n
n 个元素。
在平均情况下,检查
i
i
i 次(
i
i
i 取值为:
1
,
2
,
…
,
n
1,2,\dots,n
1,2,…,n)的可能性均为
1
/
n
1/n
1/n。则需要检查的元素个数为:
1
×
(
1
/
n
)
+
2
×
(
1
/
n
)
+
⋯
+
n
×
(
1
/
n
)
=
(
n
+
1
)
/
2
1\times(1/n) + 2\times(1/n)+\dots+n\times(1/n) =(n+1)/2
1×(1/n)+2×(1/n)+⋯+n×(1/n)=(n+1)/2
即,在平均情况下,检查
(
n
+
1
)
/
2
(n+1)/2
(n+1)/2 个元素。运行时间为
Θ
(
n
)
\Theta(n)
Θ(n)。
在最坏情况下,待查找的元素在数组的第
n
n
n 个位置上,需要检查
n
n
n 个元素。运行时间为
Θ
(
n
)
\Theta(n)
Θ(n)。
2.2-4 我们应该如何修改算法,以使之具有较好的最佳运行时间?
解: 修改算法,测试输入是否满足某些特殊情况,如果满足,则输出预先计算的答案。 最佳情况下的运行时间通常不是衡量算法的好方法。
2.3 设计算法(Designing algorithms)
我们可从多种算法设计技术中进行选择。对于插入排序,我们使用了一种增量(incremental)方法:对子数组
A
[
1..
j
]
A[1..j]
A[1..j] 进行了排序之后,将单个元素
A
[
j
]
A[j]
A[j] 插入其适当位置,生成排序后的子数组
A
[
1..
j
]
A[1..j]
A[1..j]。
在本节中,我们研究另一种设计方法,称为“分治”。我们使用分治来设计一种排序算法,该算法在最坏情况下的运行时间比插入排序的小很多。
2.3.1 分治法
许多算法在结构上都是递归的(recursive):为了解决给定的问题,它们会递归地调用自身一次或多次,以处理相关的子问题。这些算法通常遵循分而治之(divide-and-conquer)的方法:将问题分解为几个与原问题相似但规模较小的子问题,递归求解子问题,然后组合这些解,得出原问题的解。
分治范式在递归的每次递归时都有三个步骤:
① 将问题分解(divide)为多个子问题,这些子问题是同一问题的较小实例。
② 通过递归解决(conquer)子问题。 如果子问题足够小,只需直接解决子问题即可。
③ 将子问题的解合并(combine)为原问题的解。
合并排序(merge sort)算法遵循上述分治法的范式。 操作如下。
分解: 将
n
n
n 个元素的序列分为两个子序列,每个子序列有
n
/
2
n/2
n/2 个元素。
解决: 使用合并排序对两个子序列进行递归排序。
合并: 合并两个已排序的子序列,得出排序的解。
假设子数组 A [ p . . q ] A[p..q] A[p..q] 和 A [ q + 1.. r ] A[q+1..r] A[q+1..r] 是排序好的,将它们合并成一个排序好的数组代替当前的 A [ p . . r ] A[p..r] A[p..r]。MERGE 过程的时间代价为 Θ ( n ) \Theta(n) Θ(n),其中 n = r − p + 1 n=r-p+1 n=r−p+1,为待合并的元素个数。伪代码如下。使用 ∞ \infty ∞ 作为哨兵值(sentinel value)。
MERGE( A A A, p p p, q q q, r r r)
1 n 1 = q − p + 1 n_1=q-p+1 n1=q−p+1
2 n 2 = r − q n_2=r-q n2=r−q
3 let L [ 1.. n 1 + 1 ] L[1..n_1+1] L[1..n1+1] and R [ 1.. n 2 + 1 ] R[1..n_2+1] R[1..n2+1] be new arrays
4 for i = 1 i=1 i=1 to n 1 n_1 n1
5 L [ i ] = A [ p + i − 1 ] L[i]=A[p+i-1] L[i]=A[p+i−1]
6 for j = 1 j=1 j=1 to n 2 n_2 n2
7 R [ j ] = A [ q + j ] R[j]=A[q+j] R[j]=A[q+j]
8 L [ n 1 + 1 ] = ∞ L[n_1+1]=\infty L[n1+1]=∞
9 R [ n 2 + 1 ] = ∞ R[n_2+1]=\infty R[n2+1]=∞
10 i = 1 i=1 i=1
11 j = 1 j=1 j=1
12 for k = p k=p k=p to r r r
13 if L [ i ] ≤ R [ j ] L[i]\leq R[j] L[i]≤R[j]
14 A [ k ] = L [ i ] A[k]=L[i] A[k]=L[i]
15 i = i + 1 i=i+1 i=i+1
16 else
17 A [ k ] = R [ j ] A[k]=R[j] A[k]=R[j]
18 j = j + 1 j=j+1 j=j+1
将 MERGE 过程作为合并排序算法中的子程序。过程 MERGE-SORT( A A A, p p p, r r r) 对子数组 A [ p . . r ] A[p..r] A[p..r] 排序。
MERGE-SORT( A A A, p p p, r r r)
1 if p < r p<r p<r
2 q = ⌊ ( p + r ) / 2 ⌋ q=\lfloor (p+r)/2\rfloor q=⌊(p+r)/2⌋
3 MERGE-SORT( A A A, p p p, q q q)
4 MERDE-SORT( A A A, q + 1 q+1 q+1, r r r)
5 MERGE( A A A, p p p, q q q, r r r)
C++代码如下:
// 条件:子序列 A[p..q] 是从小到大排序好的,子序列 A[q+1..r] 是从小到大排序好的
void merge(vector<int>& A, int p, int q, int r)
{
int n1 = q - p + 1; // 子序列 A[p..q] 的个数
int n2 = r - q; // 子序列 A[q+1..r] 的个数
vector<int> L(n1 + 1);
vector<int> R(n2 + 1);
for(int i=0; i<n1; ++i) // 将 A[p..q] 的值赋值到 L[0..n1-1]
L[i] = A[p + i - 1];
for(int j=0; j<n2; ++j) // 将 A[q+1..r] 的值赋值到 R[0..n2-1]
R[j] = A[q + j];
L[n1] = INT_MAX;
R[n2] = INT_MAX;
int i = 0;
int j = 0;
for(int k=p; k<=r; ++k)
{
if(L[i] <= R[j])
{
A[k] = L[i];
++i;
}
else
{
A[k] = R[j];
++j;
}
}
}
void mergeSort(vector<int>& A, int p, int r)
{
if(p < r) // 当 p == r 时,A 中包含一个元素,是排序好的
{
int q = (p + r) / 2;
mergeSort(A, p, q); // 对子数组 A[p..q] 进行排序
mergeSort(A, q+1, r); // 对子数组 A[q+1..r] 进行排序
merge(A, p, q, r);
}
}
2.3.2 分析分治算法
当算法包含对自身的递归调用时,我们可以通过递归方程(recurrence equation)或递归式(recurrence)来描述其运行时间。使用数学工具来解递归方程式,给出算法性能的界限。
设问题规模为
n
n
n。如果规模足够小,如
n
≤
c
n\leq c
n≤c,
c
c
c 为常数,则运行时间
T
(
n
)
T(n)
T(n) 为常量,可写为
Θ
(
1
)
\Theta(1)
Θ(1)。假设将问题分解成
a
a
a 个子问题,每个子问题的大小是原问题的
1
/
b
1/b
1/b。分解问题的时间记为
D
(
n
)
D(n)
D(n),合并问题的时间记为
C
(
n
)
C(n)
C(n)。得到递归式如下:
T
(
n
)
=
{
Θ
(
1
)
i
f
n
≤
c
a
T
(
n
/
b
)
+
D
(
n
)
+
C
(
n
)
o
t
h
e
r
w
i
s
e
T(n)=\left\{ \begin{array}{rcl} \Theta(1) & & {if \quad n\leq c} \\aT(n/b)+D(n)+C(n) & & otherwise \end{array} \right.
T(n)={Θ(1)aT(n/b)+D(n)+C(n)ifn≤cotherwise
合并排序算法的分析
为了简化基于递归的分析,假定问题的规模为 2 的幂次。这样,每次分解后生成两个长度为
n
/
2
n/2
n/2 的子序列。对一个元素进行合并排序需要常量时间。当元素个数
n
>
1
n>1
n>1 时,运行时间分解如下。
分解: 计算子数组的中间位置,需要常量时间。
D
(
n
)
=
Θ
(
1
)
D(n)=\Theta(1)
D(n)=Θ(1)。
解决: 递归解决两个子问题,每个子问题规模为
n
/
2
n/2
n/2,运行时间为
2
T
(
n
/
2
)
2T(n/2)
2T(n/2)。
合并: 在一个含有
n
n
n 个元素的子数组中,MERGE 过程需要时间
Θ
(
n
)
\Theta(n)
Θ(n)。
C
(
n
)
=
Θ
(
n
)
C(n)=\Theta(n)
C(n)=Θ(n)。
函数
Θ
(
1
)
\Theta(1)
Θ(1) 与函数
Θ
(
n
)
\Theta(n)
Θ(n) 相加,得到一个线性函数,即
Θ
(
n
)
\Theta(n)
Θ(n)。
令
Θ
(
1
)
=
c
\Theta(1)=c
Θ(1)=c,则
Θ
(
n
)
=
c
n
\Theta(n)=cn
Θ(n)=cn。将上述递归式重写为:
T
(
n
)
=
{
c
i
f
n
=
1
2
T
(
n
/
2
)
+
c
n
i
f
n
>
1
T(n)=\left\{ \begin{array}{rcl} c & & {if \quad n=1} \\2T(n/2)+cn & & if \quad n>1 \end{array} \right.
T(n)={c2T(n/2)+cnifn=1ifn>1
当 n > 1 n>1 n>1 时,构造递归树(recursion tree),计算整棵树的总代价为 c n ( lg n + 1 ) = c n lg n + c n cn(\lg n+1)=cn\lg n+cn cn(lgn+1)=cnlgn+cn。此处, lg n \lg n lgn 为 log 2 n \log_2 n log2n。忽略低阶项和常数,得出 T ( n ) = Θ ( n lg n ) T(n)=\Theta(n\lg n) T(n)=Θ(nlgn)。
练习
2.3-1 以图 2.4 为模型,说明数组
A
=
⟨
3
,
41
,
52
,
26
,
38
,
57
,
9
,
49
⟩
A=\langle 3,41,52,26,38,57,9,49\rangle
A=⟨3,41,52,26,38,57,9,49⟩ 合并排序的执行过程。
解:
2.3-2 重写 MERGE 过程,不使用哨兵,而改为:一旦数组
L
L
L 或
R
R
R 的所有元素都复制回数组
A
A
A 中,立即停止,将另一个数组余下的元素复制回数组
A
A
A 中。
解: MERGE(
A
A
A,
p
p
p,
q
q
q,
r
r
r)
1
n
1
=
q
−
p
+
1
n_1=q-p+1
n1=q−p+1
2
n
2
=
r
−
q
n_2=r-q
n2=r−q
3 let
L
[
1..
n
1
]
L[1..n_1]
L[1..n1] and
R
[
1..
n
2
]
R[1..n_2]
R[1..n2] be new arrays
4 for
i
=
1
i=1
i=1 to
n
1
n_1
n1
5
L
[
i
]
=
A
[
p
+
i
−
1
]
L[i]=A[p+i-1]
L[i]=A[p+i−1]
6 for
j
=
1
j=1
j=1 to
n
2
n_2
n2
7
R
[
j
]
=
A
[
q
+
j
]
R[j]=A[q+j]
R[j]=A[q+j]
8
i
=
1
i=1
i=1
9
j
=
1
j=1
j=1
10
k
=
p
k=p
k=p
11 while
i
≤
n
1
i\leq n_1
i≤n1 and
j
≤
n
2
j\leq n_2
j≤n2
12 if
L
[
i
]
≤
R
[
j
]
L[i]\leq R[j]
L[i]≤R[j]
13
A
[
k
]
=
L
[
i
]
A[k]=L[i]
A[k]=L[i]
14
i
=
i
+
1
i=i+1
i=i+1
15 else
16
A
[
k
]
=
R
[
j
]
A[k]=R[j]
A[k]=R[j]
17
j
=
j
+
1
j=j+1
j=j+1
18
k
=
k
+
1
k=k+1
k=k+1
19 if
i
>
n
1
i>n_1
i>n1
20 while
k
≤
r
k\leq r
k≤r
21
A
[
k
]
=
R
[
j
]
A[k]=R[j]
A[k]=R[j]
22
k
=
k
+
1
k=k+1
k=k+1
23
j
=
j
+
1
j=j+1
j=j+1
24 else
25 while
k
≤
r
k\leq r
k≤r
26
A
[
k
]
=
L
[
i
]
A[k]=L[i]
A[k]=L[i]
27
k
=
k
+
1
k=k+1
k=k+1
28
i
=
i
+
1
i=i+1
i=i+1
void merge(vector<int>& A, int p, int q, int r)
{
int n1 = q - p + 1; // 子序列 A[p..q] 的个数
int n2 = r - q; // 子序列 A[q+1..r] 的个数
vector<int> L(n1);
vector<int> R(n2);
for(int i=0; i<n1; ++i) // 将 A[p..q] 的值赋值到 L[0..n1-1]
L[i] = A[p + i - 1];
for(int j=0; j<n2; ++j) // 将 A[q+1..r] 的值赋值到 R[0..n2-1]
R[j] = A[q + j];
int i = 0;
int j = 0;
int k = p;
while(i<n1 && j<n2)
{
if(L[i] <= R[j])
{
A[k] = L[i];
++i;
}
else
{
A[k] = R[j];
++j;
}
}
if(i >= n1)
{
for(; k <= r; ++k, ++j)
A[k] = R[j];
}
else
{
for(; k <= r; ++k, ++i)
A[k] = L[i];
}
}
2.3-3 利用数学归纳法证明:当
n
n
n 是 2 的整数次幂,递归式
T
(
n
)
=
{
2
i
f
n
=
2
2
T
(
n
/
2
)
+
n
i
f
n
=
2
k
,
f
o
r
k
>
1
T(n)=\left\{ \begin{array}{rcl} 2 & & {if \ n=2} \\2T(n/2)+n & & if \ n=2^k,\ for \ k>1 \end{array} \right.
T(n)={22T(n/2)+nif n=2if n=2k, for k>1 的解是
T
(
n
)
=
n
lg
n
T(n)=n\lg n
T(n)=nlgn。
证: ① 验证
k
=
1
,
2
k=1,2
k=1,2,即
n
=
2
,
4
n=2,4
n=2,4 时,等式
T
(
n
)
=
n
lg
n
T(n)=n\lg n
T(n)=nlgn 成立。当
k
=
1
k=1
k=1 时,左边
=
2
=2
=2,右边
=
2
lg
2
=
2
=2\lg 2=2
=2lg2=2,所以这个等式在
k
=
1
k=1
k=1 时成立。当
k
=
2
k=2
k=2 时,左边
=
2
T
(
2
)
+
4
=
8
=2T(2)+4=8
=2T(2)+4=8,右边
=
4
lg
4
=
8
=4\lg 4=8
=4lg4=8,所以这个等式在
k
=
2
k=2
k=2 时成立。第一步完成。
② 我们需要证明,如果假设
k
=
m
k=m
k=m 时公式成立(
m
>
1
m>1
m>1),那么可以推导出
k
=
m
+
1
k=m+1
k=m+1 时公式也成立,即,从
T
(
2
m
)
=
2
m
lg
2
m
T(2^m)=2^m \lg 2^m
T(2m)=2mlg2m 推导出
T
(
2
m
+
1
)
=
(
m
+
1
)
lg
2
m
+
1
T(2^{m+1})=(m+1)\lg 2^{m+1}
T(2m+1)=(m+1)lg2m+1 可证明等式
T
(
n
)
=
n
lg
n
T(n)=n\lg n
T(n)=nlgn 成立。证明步骤如下。
我们先假设
k
=
m
k=m
k=m 时公式成立。此时,
n
=
2
m
n=2^m
n=2m。则有:
左边
=
T
(
n
)
=
T
(
2
m
)
=
2
T
(
2
m
/
2
)
+
2
m
=
2
T
(
2
m
−
1
)
+
2
m
=T(n)=T(2^m)=2T(2^m/2)+2^m=2T(2^{m-1}) +2^m
=T(n)=T(2m)=2T(2m/2)+2m=2T(2m−1)+2m。
右边
=
n
lg
n
=
2
m
lg
2
m
=
m
×
2
m
=n \lg n=2^m \lg 2^m = m\times 2^m
=nlgn=2mlg2m=m×2m。
由 左边 = 右边,得到,
2
T
(
2
m
−
1
)
+
2
m
=
m
×
2
m
2T(2^{m-1}) +2^m=m\times 2^m
2T(2m−1)+2m=m×2m。
将两边同时乘以 2,得到
4
T
(
2
m
−
1
)
+
2
m
+
1
=
m
×
2
m
+
1
4T(2^{m-1}) +2^{m+1}=m\times 2^{m+1}
4T(2m−1)+2m+1=m×2m+1。
将两边同时加上
2
m
+
1
2^{m+1}
2m+1,得到
4
T
(
2
m
−
1
)
+
2
×
2
m
+
1
=
(
m
+
1
)
×
2
m
+
1
4T(2^{m-1}) +2\times 2^{m+1}=(m+1)\times 2^{m+1}
4T(2m−1)+2×2m+1=(m+1)×2m+1 (1)
4
T
(
2
m
−
1
)
+
2
×
2
m
+
1
=
2
(
2
T
(
2
m
/
2
)
+
2
m
)
+
2
m
+
1
=
2
T
(
2
m
)
+
2
m
+
1
=
T
(
2
m
+
1
)
4T(2^{m-1}) +2 \times 2^{m+1} = 2(2T(2^m/2)+2^m)+2^{m+1} = 2T(2^m)+2^{m+1} = T(2^{m+1})
4T(2m−1)+2×2m+1=2(2T(2m/2)+2m)+2m+1=2T(2m)+2m+1=T(2m+1) (2)
(
m
+
1
)
×
2
m
+
1
=
(
m
+
1
)
lg
2
m
+
1
(m+1)\times 2^{m+1}=(m+1)\lg 2^{m+1}
(m+1)×2m+1=(m+1)lg2m+1 (3)
由 (1)、(2)、(3),得出
T
(
2
m
+
1
)
=
(
m
+
1
)
lg
2
m
+
1
T(2^{m+1})=(m+1)\lg 2^{m+1}
T(2m+1)=(m+1)lg2m+1。
综上所述,等式
T
(
n
)
=
n
lg
n
T(n)=n\lg n
T(n)=nlgn 成立,即递归式的解时
T
(
n
)
=
n
lg
n
T(n)=n\lg n
T(n)=nlgn 。
2.3-4 我们可以将插入排序表示为递归过程。为了排序
A
[
1..
n
]
A[1..n]
A[1..n] ,我们递归地排序
A
[
1..
n
−
1
]
A[1..n-1]
A[1..n−1],接着将
A
[
n
]
A[n]
A[n] 插入到已排序的
A
[
1..
n
−
1
]
A[1..n-1]
A[1..n−1] 中。对于这个递归版本的插入排序,写出其运行时间的递归式。
解: C++ 代码如下:
void insertionSort(vector<int>& A, int n)
{
if(n == 1) // A中包含一个元素是排序好的
return;
int elem = A[n-1];
insertionSort(A, n-1);
int i = n-1;
for(; i>0; --i)
{
if(A[i-1] > elem)
A[i] = A[i-1];
else
break;
}
A[i] = elem;
}
最坏情况下,运行时间的递归式如下:
T
(
n
)
=
{
Θ
(
1
)
i
f
n
=
1
T
(
n
−
1
)
+
Θ
(
n
)
i
f
n
>
1
T(n)=\left\{ \begin{array}{rcl} \Theta(1) & & {if \quad n=1} \\T(n-1)+\Theta(n) & & if \quad n>1 \end{array} \right.
T(n)={Θ(1)T(n−1)+Θ(n)ifn=1ifn>1
2.3-5 回顾一下查找问题(见练习 2.1-3),观察到,如果序列
A
A
A 已经排序,我们可以将序列的中点与
v
v
v 进行比较,再进一步考虑时,可以剔除原序列的一半。二分查找(binary search)算法重复此过程,每次序列余下的部分大小减半。 写出二分查找的伪代码,可以使用迭代或递归。 说明二分查找的最坏情况运行时间为
Θ
(
lg
n
)
\Theta(\lg n)
Θ(lgn)。
解: 迭代法如下:
BINARY-SEARCH(
A
A
A,
v
v
v)
1
l
o
w
=
1
low=1
low=1
2
h
i
g
h
=
A
.
l
e
n
g
t
h
high=A.length
high=A.length
3 while
l
o
w
≤
h
i
g
h
low\leq high
low≤high
4
m
i
d
=
l
o
w
+
⌊
(
h
i
g
h
−
l
o
w
)
/
2
⌋
mid=low+\lfloor (high-low)/2\rfloor
mid=low+⌊(high−low)/2⌋
5 if
v
=
=
A
[
m
i
d
]
v==A[mid]
v==A[mid]
6 return
m
i
d
mid
mid
7 else if
v
<
A
[
m
i
d
]
v<A[mid]
v<A[mid]
8
h
i
g
h
=
m
i
d
−
1
high=mid-1
high=mid−1
9 else
10
l
o
w
=
m
i
d
+
1
low=mid+1
low=mid+1
11 return NIL
递归法如下:
BINARY-SEARCH(
A
A
A,
v
v
v,
l
o
w
low
low,
h
i
g
h
high
high)
1 if
l
o
w
>
h
i
g
h
low>high
low>high
2 return NIL
3
m
i
d
=
l
o
w
+
⌊
(
h
i
g
h
−
l
o
w
)
/
2
⌋
mid=low+\lfloor (high-low)/2\rfloor
mid=low+⌊(high−low)/2⌋
4 if
v
=
=
A
[
m
i
d
]
v==A[mid]
v==A[mid]
5 return
m
i
d
mid
mid
6 else if
v
<
A
[
m
i
d
]
v<A[mid]
v<A[mid]
7 BINARY-SEARCH(
A
A
A,
v
v
v,
l
o
w
low
low,
m
i
d
−
1
mid-1
mid−1)
8 else
9 BINARY-SEARCH(
A
A
A,
v
v
v,
m
i
d
+
1
mid+1
mid+1,
h
i
g
h
high
high)
每次查找范围减半,所以最坏情况下,运行时间的递归式如下:
T
(
n
)
=
{
Θ
(
1
)
当
范
围
为
空
,
即
l
o
w
>
h
i
g
h
T
(
n
/
2
)
+
Θ
(
1
)
其
他
,
即
l
o
w
≤
h
i
g
h
T(n)=\left\{ \begin{array}{rcl} \Theta(1) & & {当范围为空,即\ low>high} \\T(n/2)+\Theta(1) & & 其他,即 \ low\leq high \end{array} \right.
T(n)={Θ(1)T(n/2)+Θ(1)当范围为空,即 low>high其他,即 low≤high
由递归树可推导出
T
(
n
)
=
Θ
(
lg
n
)
T(n)=\Theta(\lg n)
T(n)=Θ(lgn)。
2.3-6 观察章节 2.1 中 INSERTION-SORT 过程,在 5 - 7 行的 while 循环中,使用线性查找方式,在已排序的
A
[
1..
j
]
A[1..j]
A[1..j] 中(反向)扫描。我们可以使用二分查找(见练习 2.3-5),将插入排序的最坏情况运行时间改善至
Θ
(
n
lg
n
)
\Theta(n\lg n)
Θ(nlgn)。
解: 不能。找到一个元素的插入位置所需时间可以改善至
Θ
(
lg
n
)
\Theta(\lg n)
Θ(lgn),但将一个元素插入到适当位置则依旧需要时间
Θ
(
n
)
\Theta(n)
Θ(n)。
2.3-7 写出一个运行时间为
Θ
(
n
lg
n
)
\Theta(n\lg n)
Θ(nlgn) 的算法:给出一个包含
n
n
n 个整数的集合
S
S
S,一个整数
x
x
x,找出集合
S
S
S 中是否存在两个元素,这两个元素之和等于
x
x
x。
解: CHECK(
S
S
S,
x
x
x)
1 MERGE-SORT(
S
S
S,
1
1
1,
n
n
n)
2
l
o
w
=
1
low=1
low=1
3
h
i
g
h
=
n
high=n
high=n
4 while
l
o
w
<
h
i
g
h
low<high
low<high
5 if
S
[
l
o
w
]
+
S
[
h
i
g
h
]
=
=
x
S[low]+S[high]==x
S[low]+S[high]==x
6 return
S
[
l
o
w
]
S[low]
S[low],
S
[
h
i
g
h
]
S[high]
S[high]
7 else if
S
[
l
o
w
]
+
S
[
h
i
g
h
]
>
x
S[low]+S[high]>x
S[low]+S[high]>x
8
h
i
g
h
=
h
i
g
h
−
1
high=high-1
high=high−1
9 else
10
l
o
w
=
l
o
w
+
1
low=low+1
low=low+1
11 return NIL
先使用归并排序算法对集合
S
S
S 进行排序,需要时间为
Θ
(
n
lg
n
)
\Theta(n\lg n)
Θ(nlgn)。接着使用双指针法找出集合
S
S
S 中和为
x
x
x 的两个元素,需要时间为
Θ
(
n
)
\Theta(n)
Θ(n)。所以,总运行时间为
T
(
n
)
=
Θ
(
n
lg
n
)
+
Θ
(
n
)
+
Θ
(
1
)
T(n)=\Theta(n\lg n)+\Theta(n)+\Theta(1)
T(n)=Θ(nlgn)+Θ(n)+Θ(1),去掉低阶项和常量,得到
T
(
n
)
=
Θ
(
n
lg
n
)
T(n)=\Theta(n\lg n)
T(n)=Θ(nlgn)。
问题
2-1 在合并排序中对小数组进行插入排序
尽管合并排序的最坏情况运行时间为
Θ
(
n
lg
n
)
\Theta(n\lg n)
Θ(nlgn),插入排序的最坏情况运行时间为
Θ
(
n
2
)
\Theta(n^2)
Θ(n2),但是在许多机器上,问题规模较小时,插入排序的常数因子使得它实际运行得更快。因此,在合并排序中,当子问题的规模足够小时,使用插入排序可行。考虑对合并排序的作出修改:使用插入排序对
n
/
k
n/k
n/k 个长度为
k
k
k 子列表进行排序,然后使用标准合并机制进行合并,其中
k
k
k 是一个待定的值。
a. 证明:在最坏情况下,使用插入排序对
n
/
k
n/k
n/k 个长度为
k
k
k 子列表进行排序,运行时间为
Θ
(
n
k
)
\Theta(nk)
Θ(nk)。
b. 在最坏情况下,如何在时间
Θ
(
n
lg
(
n
/
k
)
)
\Theta(n\lg (n/k))
Θ(nlg(n/k)) 内,完成子列表的合并。
c. 假定修改后的算法在最坏情况下运行时间为
Θ
(
n
k
+
n
lg
(
n
/
k
)
)
\Theta(nk+n\lg (n/k))
Θ(nk+nlg(n/k)),要使修改后的算法运行时间与标准合并排序一样,
k
k
k 的最大值是多少?以
Θ
\Theta
Θ 形式表示为关于
n
n
n 的函数。
d. 在实际中,应该如何选择
k
k
k 值?
解: a. 证:在最坏情况下,对一个长度为
k
k
k 子列表进行插入排序,其运行时间可表示为
Θ
(
k
2
)
\Theta(k^2)
Θ(k2)。因为要排序
n
/
k
n/k
n/k 个子列表,所以总运行时间为
(
n
/
k
)
⋅
Θ
(
k
2
)
=
Θ
(
n
k
)
(n/k)\cdot \Theta(k^2)=\Theta(nk)
(n/k)⋅Θ(k2)=Θ(nk)。
b. 子列表的合并使用合并排序算法。构造递归树,在该树中,总共有
lg
(
n
/
k
)
+
1
\lg(n/k)+1
lg(n/k)+1 层,每层的代价都是
Θ
(
n
)
\Theta(n)
Θ(n),所以,整棵树的总代价是
Θ
(
n
lg
(
n
/
k
)
+
n
)
\Theta(n\lg(n/k)+n)
Θ(nlg(n/k)+n),忽略低阶项,运行时间为
Θ
(
n
lg
(
n
/
k
)
)
\Theta(n\lg(n/k))
Θ(nlg(n/k))。
c. 根据题意,
Θ
(
n
k
+
n
lg
(
n
/
k
)
)
\Theta(nk+n\lg(n/k))
Θ(nk+nlg(n/k)) 的最高阶项的幂次不能超过
Θ
(
n
lg
n
)
\Theta(n\lg n)
Θ(nlgn)。
k
k
k 的最大值是
Θ
(
lg
n
)
\Theta(\lg n)
Θ(lgn)。
d. 选择的
k
k
k 值应该使得插入排序的运行时间比合并排序的运行时间要快。
2-2 冒泡排序算法的正确性
冒泡排序法是一种流行的但效率不高的算法,其工作机制是通过重复交换两个相邻的无序的元素来排序。
BUBBLESORT(
A
A
A)
1 for
i
=
1
i=1
i=1 to
A
.
l
e
n
g
t
h
−
1
A.length-1
A.length−1
2 for
j
=
A
.
l
e
n
g
t
h
j=A.length
j=A.length downto
i
+
1
i+1
i+1
3 if
A
[
j
]
<
A
[
j
−
1
]
A[j]< A[j-1]
A[j]<A[j−1]
4 exchange
A
[
j
]
A[j]
A[j] with
A
[
j
−
1
]
A[j-1]
A[j−1]
a. 设
A
′
A'
A′ 表示 BUBBLESORT(
A
A
A) 的输出。为了证明 BUBBLESORT 是正确的,我们需要证明它可以终止,并且:
A
′
[
1
]
≤
A
′
[
2
]
≤
⋯
≤
A
′
[
n
]
(
2.3
)
A'[1]\leq A'[2]\leq\dots\leq A'[n] \qquad \qquad \qquad(2.3)
A′[1]≤A′[2]≤⋯≤A′[n](2.3)
其中
n
=
A
.
l
e
n
g
t
h
n=A.length
n=A.length。为了证明 BUBBLESORT 确实可以实现排序,还需要证明什么?
下面两个部分证明不等式(2.3)。
b. 对于第 2-4 行中的 for 循环,准确指出其循环不变式,并证明该循环不变式成立。 证明应使用本章介绍的循环不变式证明的结构。
c. 使用在(b)部分中证明的循环不变式的终止条件,为第1–4行中的 for 循环给出一个循环不变式,这可以用来证明不等式(2.3)。 证明应使用本章介绍的循环不变式证明的结构。
d. 冒泡排序的最坏情况运行时间是多少?比较它与插入排序的运行时间。
解: a. 还需要证明序列
A
′
A'
A′ 是序列
A
A
A 的重新排序。
b. 证:循环不变式:
A
[
j
]
A[j]
A[j] 是
A
[
j
.
.
n
]
A[j..n]
A[j..n] 中最小的元素。
初始: 在循环第一次迭代之前,
j
=
n
j=n
j=n,此时
A
[
j
.
.
n
]
A[j..n]
A[j..n] 中只有一个元素
A
[
n
]
A[n]
A[n],循环不变式为真。
保持: 若
A
[
j
−
1
]
>
A
[
j
]
A[j-1]>A[j]
A[j−1]>A[j],则
A
[
j
]
A[j]
A[j] 和
A
[
j
−
1
]
A[j-1]
A[j−1] 的值会交换,此时
A
[
j
−
1
]
<
A
[
i
]
A[j-1]<A[i]
A[j−1]<A[i];否则
A
[
j
]
A[j]
A[j] 和
A
[
j
−
1
]
A[j-1]
A[j−1] 保持不变,此时有
A
[
j
−
1
]
≤
A
[
j
]
A[j-1]\leq A[j]
A[j−1]≤A[j]。
A
[
j
−
1
]
≤
A
[
j
]
A[j-1]\leq A[j]
A[j−1]≤A[j] 恒成立,而
A
[
j
]
A[j]
A[j] 是
A
[
j
.
.
n
]
A[j..n]
A[j..n] 中最小的元素,所以,
A
[
j
−
1
]
A[j-1]
A[j−1] 是
A
[
j
−
1..
n
]
A[j-1..n]
A[j−1..n] 中最小的元素。在下一次迭代前,循环不变式为真。
终止: 当
j
=
i
+
1
j=i+1
j=i+1 时,循环终止。
A
[
i
]
A[i]
A[i] 是序列
A
[
i
.
.
n
]
A[i..n]
A[i..n] 中最小的元素。
综上,循环不变式成立。
c. 证:循环不变式:
A
[
1..
i
]
A[1..i]
A[1..i] 是排序好的。
初始: 在循环第一次迭代之前,序列
A
[
1..
i
]
A[1..i]
A[1..i] 只有一个元素
A
[
1
]
A[1]
A[1],循环不变式成立。
保持: 由(b)部分,知
A
[
i
]
A[i]
A[i] 是
A
[
i
.
.
n
]
A[i..n]
A[i..n] 中最小的元素,所以
A
[
i
+
1..
n
]
A[i+1..n]
A[i+1..n] 内的值均大于
A
[
i
]
A[i]
A[i],且
A
[
1..
i
]
A[1..i]
A[1..i] 是排序好的。因此,
A
[
1..
i
+
1
]
A[1..i+1]
A[1..i+1] 是排序好的。在下一次迭代前,循环不变式为真。
终止: 当
i
=
n
−
1
i=n-1
i=n−1 时,循环终止。
A
[
1..
n
]
A[1..n]
A[1..n] 是排序好的。
综上,循环不变式成立。不等式(2.3)成立。
d. 冒泡排序的最坏运行时间是
Θ
(
n
2
)
\Theta(n^2)
Θ(n2)。考虑常数因子,冒泡排序比插入排序运行得慢。
2-3 霍纳规则的正确性
下面的代码片段实现了用于计算多项式的霍纳规则(Horner’s rule)
P
(
x
)
=
∑
k
=
0
n
a
k
x
k
=
a
0
+
x
(
a
1
+
x
(
a
2
+
⋯
+
x
(
a
n
−
1
+
x
a
n
)
⋯
)
)
P(x)=\sum_{k=0}^{n}a_{k}x^{k}=a_0+x(a_1+x(a_2+\cdots+x(a_{n-1}+xa_n)\cdots))
P(x)=∑k=0nakxk=a0+x(a1+x(a2+⋯+x(an−1+xan)⋯)),
给定系数
a
0
,
a
1
,
…
,
a
n
a_0,a_1,\dots ,a_n
a0,a1,…,an 和
x
x
x 的值:
1
y
=
0
y=0
y=0
2 for
i
=
n
i=n
i=n downto 0
3
y
=
a
i
+
x
⋅
y
y=a_i+x\cdot y
y=ai+x⋅y
a. 这段实现霍纳规则的代码运行时间是多少?以
Θ
\Theta
Θ 形式表示。
b. 写出伪代码以实现朴素多项式求值算法(naive polynomial-evaluation algorithm),该算法从头开始计算多项式的每个项。该算法的运行时间是多少?与霍纳规则相比如何?
c. 考虑下面的循环不变式:
在第 2–3 行的 for 循环的每次迭代开始时,
y
=
∑
k
=
0
n
−
(
i
+
1
)
a
k
+
i
+
1
x
k
y=\sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k
y=∑k=0n−(i+1)ak+i+1xk。
不包含任何项的和等于 0。按照本章介绍的循环不变式证明的结构,使用该循环不变式证明,在终止时,有
y
=
∑
k
=
0
n
a
k
x
k
y=\sum_{k=0}^{n}a_{k}x^{k}
y=∑k=0nakxk。
d. 推断:给出的代码片段能够正确地计算以系数
a
0
,
a
1
,
…
,
a
n
a_0,a_1,\dots,a_n
a0,a1,…,an 为特征的多项式。
解: a. 这段代码运行时间是
Θ
(
n
)
\Theta(n)
Θ(n)。
b. 朴素多项式求值算法伪代码如下:
1
y
=
0
y=0
y=0
2
s
=
1
s=1
s=1
3 for
i
=
0
i=0
i=0 to
n
n
n
4
y
=
y
+
a
i
⋅
s
y=y+a_i\cdot s
y=y+ai⋅s
5
s
=
x
⋅
s
s=x\cdot s
s=x⋅s
该算法的运行时间为
Θ
(
n
)
\Theta(n)
Θ(n)。考虑常数因子,该算法比霍纳规则的实现代码慢。
c.
y
=
∑
k
=
0
n
−
(
i
+
1
)
a
k
+
i
+
1
x
k
=
a
i
+
1
+
a
i
+
2
⋅
x
+
⋯
+
a
n
⋅
x
n
−
(
i
+
1
)
y=\sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k=a_{i+1}+a_{i+2}\cdot x+\cdots+a_n\cdot x^{n-(i+1)}
y=∑k=0n−(i+1)ak+i+1xk=ai+1+ai+2⋅x+⋯+an⋅xn−(i+1)。
初始: 在循环第一次迭代前,
y
=
0
y=0
y=0,
i
=
n
i=n
i=n,此时多项式不包含任何项,
∑
k
=
0
n
−
(
i
+
1
)
a
k
+
i
+
1
x
k
\sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k
∑k=0n−(i+1)ak+i+1xk 等于 0。循环不变式为真。
保持: 由第 3 行代码得,在进行当前迭代时,
y
i
−
1
=
a
i
+
x
⋅
y
i
y_{i-1} = a_i + x\cdot y_i
yi−1=ai+x⋅yi。
假设上一次迭代时循环不变式为真,将
y
i
=
∑
k
=
0
n
−
(
i
+
1
)
a
k
+
i
+
1
x
k
y_i=\sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k
yi=∑k=0n−(i+1)ak+i+1xk 代入上式得,
y
i
−
1
=
a
i
+
x
⋅
∑
k
=
0
n
−
(
i
+
1
)
a
k
+
i
+
1
x
k
=
a
i
+
a
i
+
1
⋅
x
+
a
i
+
2
⋅
x
2
+
⋯
+
a
n
⋅
x
n
−
i
=
∑
k
=
0
n
−
i
a
k
+
i
x
k
=
∑
k
=
0
n
−
(
(
i
−
1
)
+
1
)
a
k
+
(
i
−
1
)
+
1
x
k
y_{i-1} = a_i+x\cdot \sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k \\= a_i + a_{i+1}\cdot x + a_{i+2}\cdot x^2 + \cdots + a_n\cdot x^{n-i} \\=\sum_{k=0}^{n-i}a_{k+i}x^k\\=\sum_{k=0}^{n-((i-1)+1)}a_{k+(i-1)+1}x^k
yi−1=ai+x⋅∑k=0n−(i+1)ak+i+1xk=ai+ai+1⋅x+ai+2⋅x2+⋯+an⋅xn−i=∑k=0n−iak+ixk=∑k=0n−((i−1)+1)ak+(i−1)+1xk。
所以,在下一次迭代之前,循环不变式为真。
终止: 当
i
=
0
i=0
i=0 时,循环终止。此时,有
y
=
∑
k
=
0
n
a
k
x
k
y=\sum_{k=0}^{n}a_{k}x^{k}
y=∑k=0nakxk。
d. 由部分(c)可以推断出此结论。
2-4 逆序对(Inversions)
设
A
[
1..
n
]
A[1..n]
A[1..n] 是一个包含
n
n
n 个不同数字的数组。如果
i
<
j
i<j
i<j 且
A
[
i
]
>
A
[
j
]
A[i]>A[j]
A[i]>A[j],那么
(
i
,
j
)
(i,j)
(i,j) 被称为
A
A
A 中的一个逆序对。
a. 列出数组
⟨
2
,
3
,
8
,
6
,
1
⟩
\langle 2,3,8,6,1\rangle
⟨2,3,8,6,1⟩ 中的 5 个逆序对。
b. 如果数组的元素取自集合
{
1
,
2
,
…
,
n
}
\{1,2,\dots,n\}
{1,2,…,n},那么怎样的数组中逆序对最多?包含的逆序对是多少?
c. 插入排序的运行时间与输入数组中的逆序对的数目之间有什么关系?说明你的理由。
d. 给出一个算法,确定
n
n
n 个元素的任何排列中的逆序对的数目,其最坏情况运行时间为
Θ
(
n
lg
n
)
\Theta(n \lg n)
Θ(nlgn)。(提示:修改合并排序。)
解: a. 逆序对:
(
1
,
5
)
(1,5)
(1,5),
(
2
,
5
)
(2,5)
(2,5),
(
3
,
4
)
(3,4)
(3,4),
(
3
,
5
)
(3,5)
(3,5),
(
4
,
5
)
(4,5)
(4,5)。
b. 数组
⟨
n
,
n
−
1
,
n
−
2
,
…
,
2
,
1
⟩
\langle n,n-1,n-2,\dots,2,1\rangle
⟨n,n−1,n−2,…,2,1⟩ 中逆序对的数目最多。逆序对数目有
n
(
n
−
1
)
/
2
n(n-1)/2
n(n−1)/2 个。
c. 输入数组中的逆序对的数目越多,插入排序的运行时间越长。
过程 INSERTION-SORT 的外部 for 循环中,
i
<
j
i<j
i<j 总是成立,
k
e
y
=
A
[
j
]
key=A[j]
key=A[j]。如果
A
[
i
]
>
k
e
y
A[i]>key
A[i]>key,那么
(
i
,
j
)
(i,j)
(i,j) 就是一个逆序对,在内部 while 循环(代码第 5-7 行)中可以看出,此时每次迭代需要移动元素一次,消除一个逆序对。所以,输入数组中的逆序对的数目越多,插入排序需要消除的逆序对越多,其内循环移动元素的次数就越多,插入排序的运行时间就越长。
d. 题目要求的算法如过程 INVERSIONS-NUM 所示。
INVERSIONS-MERGE(
A
A
A,
p
p
p,
q
q
q,
r
r
r)
1
n
1
=
q
−
p
+
1
n_1=q-p+1
n1=q−p+1
2
n
2
=
r
−
q
n_2=r-q
n2=r−q
3 let
L
[
1..
n
1
+
1
]
L[1..n_1+1]
L[1..n1+1] and
R
[
1..
n
2
+
1
]
R[1..n_2+1]
R[1..n2+1] be new arrays
4 for
i
=
1
i=1
i=1 to
n
1
n_1
n1
5
L
[
i
]
=
A
[
p
+
i
−
1
]
L[i]=A[p+i-1]
L[i]=A[p+i−1]
6 for
j
=
1
j=1
j=1 to
n
2
n_2
n2
7
R
[
j
]
=
A
[
q
+
j
]
R[j]=A[q+j]
R[j]=A[q+j]
8
L
[
n
1
+
1
]
=
∞
L[n_1+1]=\infty
L[n1+1]=∞
9
R
[
n
2
+
1
]
=
∞
R[n_2+1]=\infty
R[n2+1]=∞
10
i
=
1
i=1
i=1
11
j
=
1
j=1
j=1
12
n
u
m
=
0
num=0
num=0
13 for
k
=
p
k=p
k=p to
r
r
r
14 if
L
[
i
]
≤
R
[
j
]
L[i]\leq R[j]
L[i]≤R[j]
15
A
[
k
]
=
L
[
i
]
A[k]=L[i]
A[k]=L[i]
16
i
=
i
+
1
i=i+1
i=i+1
17 else
18
n
u
m
=
n
u
m
+
n
1
−
i
+
1
num=num+n_1-i+1
num=num+n1−i+1 // 因为
L
[
1..
n
1
]
L[1..n_1]
L[1..n1] 有序,所以
L
[
i
.
.
n
1
]
L[i..n_1]
L[i..n1] 中的值均大于
R
[
j
]
R[j]
R[j]
19
A
[
k
]
=
R
[
j
]
A[k]=R[j]
A[k]=R[j]
20
j
=
j
+
1
j=j+1
j=j+1
21 return
n
u
m
num
num
INVERSIONS-NUM(
A
A
A,
p
p
p,
r
r
r)
1
n
u
m
=
0
num=0
num=0
2 if
p
<
r
p<r
p<r
3
q
=
⌊
(
p
+
r
)
/
2
⌋
q=\lfloor (p+r)/2\rfloor
q=⌊(p+r)/2⌋
4
n
u
m
=
n
u
m
+
num=num+
num=num+ INVERSIONS-NUM(
A
A
A,
p
p
p,
q
q
q)
5
n
u
m
=
n
u
m
+
num=num+
num=num+ INVERSIONS-NUM(
A
A
A,
q
+
1
q+1
q+1,
r
r
r)
6
n
u
m
=
n
u
m
+
num=num+
num=num+ INVERSIONS-MERGE(
A
A
A,
p
p
p,
q
q
q,
r
r
r)
7 return
n
u
m
num
num
学习笔记目录:【算法导论】目录