2-1
(在归并排序中队小数组采用插入排序) 虽然归并排序的最坏情况运行时间为 Θ ( n 2 ) \Theta(n^2) Θ(n2),但是插入排序中的常量因子可能使得它在 n n n较小时,在许多机器上实际运行得更快。因此,在归并排序中当子问题变得足够小时,采用插入排序使得递归的叶变粗是有意义的。考虑对归并排序的一种修改,其中使用插入排序来排序长度为 k k k的 n / k n/k n/k个子表,然后使用标准的合并机制来合并这些子表,这里 k k k是一个特定的值。
- 证明:插入排序最坏情况可以在 Θ ( n k ) \Theta(nk) Θ(nk)时间内排序每个长度为 k k k的 n / k n/k n/k个子表。
- 表明在最坏情况下如何在 Θ ( n l o g 2 ( n / k ) ) \Theta(nlog_2(n/k)) Θ(nlog2(n/k))时间内合并这些子表。
- 假定修改后的算法的最坏情况运行时间为 Θ ( n k + n l o g 2 ( n / k ) ) \Theta(nk+nlog_2(n/k)) Θ(nk+nlog2(n/k)),要使修改后的算法与标准的归并排序有相同的运行时间,作为 n n n的一个函数,借助 Θ \Theta Θ记号, k k k的最大值是多少?
- 在实践中,我们应该如何选择 k k k?
答:
- 在最坏情况下,使用插入排序排序一个长度为 k k k的列表的运行时间是 Θ ( k 2 ) \Theta(k^2) Θ(k2)。因此,在最坏情况下,排序 n / k n/k n/k个子列表(每个子列表的长度为 k k k)的运行时间是 Θ ( k 2 ⋅ n / k ) = Θ ( n k ) \Theta(k^2 \cdot n/k)=\Theta(nk) Θ(k2⋅n/k)=Θ(nk)。
- 我们有 n / k n/k n/k个子列表每一个子列表的长度是 k k k。为了合并这 n / k n/k n/k个已排序的子列表成为一个长度为 n n n的单一已排序的列表,我们必须每次合并2个子列表。如果两两为一组的子列表合并完了,就继续再两两一组。这需要花费我们 l o g 2 ( n / k ) log_2(n/k) log2(n/k)步,并且每一步需要比较 n n n个元素。所以,在最坏情况下,合并这些子列表所花费的时间是 Θ ( n ⋅ l o g 2 ( n k ) ) \Theta(n \cdot log_2(\frac{n}{k})) Θ(n⋅log2(kn))。
- 当
Θ
(
n
k
+
n
⋅
l
o
g
2
(
n
k
)
)
=
Θ
(
n
⋅
l
o
g
2
n
)
\Theta(nk+n\cdot log_2(\frac{n}{k})) = \Theta(n\cdot log_2n)
Θ(nk+n⋅log2(kn))=Θ(n⋅log2n)时,修改后的算法具有普通合并排序的算法复杂度。假定
k
=
Θ
(
l
o
g
2
n
)
k = \Theta(log_2n)
k=Θ(log2n),
Θ ( n k + n ⋅ l o g 2 ( n k ) ) = Θ ( n k + n ⋅ l o g 2 n − n ⋅ l o g 2 k ) \Theta(nk + n\cdot log_2(\frac{n}{k})) = \Theta(nk + n\cdot log_2n - n\cdot log_2k) Θ(nk+n⋅log2(kn))=Θ(nk+n⋅log2n−n⋅log2k)
Θ ( n ⋅ l o g 2 n + n ⋅ l o g 2 n − n ⋅ l o g 2 ( l o g 2 n ) ) \Theta(n\cdot log_2n + n\cdot log_2n - n\cdot log_2(log_2n)) Θ(n⋅log2n+n⋅log2n−n⋅log2(log2n))
= Θ ( 2 n ⋅ l o g 2 n − n ⋅ l o g 2 ( l o g 2 n ) ) =\Theta(2n\cdot log_2n - n\cdot log_2(log_2n)) =Θ(2n⋅log2n−n⋅log2(log2n))
= Θ ( n ⋅ l o g 2 n ) =\Theta(n\cdot log_2n) =Θ(n⋅log2n)。 - 将 k k k选定为当插入排序比合并排序更快时,子列表的最大长度。
2-2
(冒泡排序的正确性)冒泡排序是一种流行但低效的排序算法,它的作用是反复交换相邻的未按次序排列的元素。
BUBBLESORT(A)
for i = 1 to A.length - 1
for j = A.length downto i + 1
if A[j] < A[j - 1]
exchange A[j] with A[j - 1]
- 假设
A
′
A'
A′表示BUBBLESORT(A)的输出。为了证明BUBBLESORT正确,我们必须证明它将终止并且有:
A ′ [ 1 ] ⩽ A ′ [ 2 ] ⩽ A ′ [ 3 ] ⩽ ⋯ ⩽ A ′ [ n ] A'[1]\leqslant A'[2]\leqslant A'[3]\leqslant \cdots\leqslant A'[n] A′[1]⩽A′[2]⩽A′[3]⩽⋯⩽A′[n]
其中 n = A . l e n g t h n = A.length n=A.length。为了证明BUBBLESORT确实完成了排序,我们还需要证明什么?
下面两部分将证明不等式。 - 为第2~4行的for循环精确地说明一个循环不变式,并证明该循环不变式成立。你地证明应该使用本章给出地循环不变式证明地结构。
- 使用(2)部分证明的循环不变式的终止条件,为第1~4行的for循环说明一个循环不变式,该不变式将使你能证明不等式。你的证明应该使用本章给出的循环不变式证明的结构。
- 冒泡排序的最坏情况运行时间是多少?与插入排序的运行时间相比,其性能如何?
答:
- A ′ A' A′是由 A A A中的元素组成,且是按顺序排列的。
- 证明过程需要完善
- 循环不变式为第1-4行的for循环在第 i i i次循环之前, A [ 1.. i − 1 ] A[1..i - 1] A[1..i−1]已排好序,且 A [ 1 ] A[1] A[1]为数组 A A A最小的元素,数组 A [ 2 ] A[2] A[2]为次小的元素,等等。数组 A A A剩余的元素 A [ i . . A . l e n g t h ] A[i..A.length] A[i..A.length]即为未排序的元素。初始化:在第1次循环之前, i i i的值是1,数组 A [ 1.. i − 1 ] A[1..i-1] A[1..i−1]为空,因为在初始化的时候所有元素均未排序,故循环不变式成立。保持:在第 i i i次循环开始之前,数组 A [ 1.. i − 1 ] A[1..i-1] A[1..i−1]中的依次从小到大排列,且是数组 A A A中最小的 i − 1 i-1 i−1个元素。在第 i i i次循环中,数组 A [ i + 1.. A . l e n g t h ] A[i + 1..A.length] A[i+1..A.length]中最小的元素会逐步冒泡“上升”到数组 A A A的第 i i i个位置。所以,当前循环结束后,第 i i i个位置的位置即为数组 A A A中“第 i i i小”的元素。因此,循环不变式保持成立。终止:循环终止条件为 i = = A . l e n g t h i == A.length i==A.length,此时 A [ 1.. i − 1 ] A[1..i-1] A[1..i−1]中的元素即为数组 A A A中的所有元素除最大的元素之外的所有元素,且已按从小到大的顺序排列,因 A [ A . l e n g t h ] A[A.length] A[A.length]就是数组 A A A中的仅剩的元素,也是最大元素,所以数组 A A A中的所有元素均已被排序。
- 冒泡排序的结果是数组 A A A中的元素以从小到大的顺序排列。最坏情况是,原始情况下数组 A A A以从大到小的顺序排列。这个时候,对于第2行的for循环的每次迭代,第3行的的判断均成立,故第4行的交换均执行。所以,对于第1行的for循环的 i i i,第4行的执行次数为 A . l e n g t h − i A.length - i A.length−i,假设交换数组元素所需的运行时间为c,则冒泡排序总共所需运行时间为 ∑ 1 n − 1 c ( n − i ) = c n ( n − 1 ) 2 \sum _{1}^{n-1}c(n - i) = \frac{cn(n-1)}{2} ∑1n−1c(n−i)=2cn(n−1)。故,冒泡排序的最坏情况运行时间是 Θ ( n 2 ) \Theta(n^2) Θ(n2),与插入排序相比,所需的运行时间是一样的。
2-3
(霍纳(Horner)规则的正确性)给定系数 a 0 , a 1 , ⋯ , a n a_0,a_1,\cdots,a_n a0,a1,⋯,an和 x x x的值,代码片段
y = 0
for i = n downto 0
y = ai + x*y
实现了用于求职多项式
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_kx^k = a+0 + x(a_1 + x(a_2 + \cdots + x(a_{n-1} + xa_n)\cdots))
P(x)=k=0∑nakxk=a+0+x(a1+x(a2+⋯+x(an−1+xan)⋯))
的霍纳规则。
- 借助 Θ \Theta Θ记号,实现霍纳规则的以上代码片段的运行时间是多少?
- 编写伪代码来实现朴素的多项式求值算法,该算法从头开始计算多项式的每个项。该算法的运行时间是多少?与霍纳规则相比,其性能如何?
- 考虑以下循环不变式:
在第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=0∑n−(i+1)ak+i+1xk
把没有项的和式解释为等于0。准找本章中给出的循环不变式证明的结构,使用该循环不变式来证明终止时有 y = ∑ k = 0 n a k x k y = \sum_{k = 0}^{n}a_kx^k y=∑k=0nakxk。 - 最后证明上面给出的代码片段将正确地求由系数 a 0 , a 1 , ⋯ , a n a_0,a_1,\cdots,a_n a0,a1,⋯,an刻画地多项式的值。
答:
- 实现霍纳规则的以上代码片段的运行时间是 Θ ( n ) \Theta(n) Θ(n)。
- 朴素的多项式求值算法:
sum = 0
pow = 1
for 0 downto n
sum += ai * pow
pow *= x
该算法的运行时间是 Θ ( n ) \Theta(n) Θ(n)。该算法的性能霍纳规则是一样的。
-
第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。初始化:当第一次循环之前,和式中没有项,故y = 0。保持:在第 i i i次迭代之后,我们有
y = a i + x ∑ k = 0 n − ( i + 1 ) x k y = a_i + x\sum_{k = 0}^{n - (i + 1)}x^k y=ai+xk=0∑n−(i+1)xk
= a i x 0 + x ∑ k = 0 n − i − 1 a k + i + 1 x k + 1 = a_ix^0 + x\sum_{k = 0}^{n - i - 1}a_{k + i + 1}x^{k + 1} =aix0+xk=0∑n−i−1ak+i+1xk+1
= a i x 0 + ∑ k = 1 n − i a k + i x k = a_ix^0 + \sum_{k = 1}{n - i}a_{k + i}x^k =aix0+k=1∑n−iak+ixk
= ∑ k = 0 n − i a k + i x k = \sum_{k = 0}^{n - i}a_{k + i}x^k =k=0∑n−iak+ixk
终止:当 i = − 1 i = -1 i=−1时,循环结束。此时
y = ∑ k = 0 n − i − 1 a k + i + 1 x k = ∑ k = 0 n a k x k y = \sum_{k = 0}^{n - i - 1}a_{k + i + 1}x^k = \sum_{k = 0}^na_kx^k y=k=0∑n−i−1ak+i+1xk=k=0∑nakxk。 -
循环的不变量是一个等于一个具有给定系数的多项式的和。
2-4
(逆序对)假设 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的一个逆序对(inversion)。
- 列出数组 < 2 , 3 , 8 , 6 , 1 > <2, 3, 8, 6, 1> <2,3,8,6,1>的5个逆序对。
- 有集合 1 , 2 , ⋯ , n {1, 2, \cdots, n} 1,2,⋯,n中的元素构成的什么数组具有最多的逆序对?她有多少逆序对?
- 插入排序的运行时间与输入数组中逆序对的数量之间是什么关系?证明你的回答。
- 给出一个确定在 n n n个元素的任何排列中逆序对数量的算法,最坏情况需要 Θ ( n ⋅ l o g 2 n ) \Theta(n\cdot log_2n) Θ(n⋅log2n)时间。(提示:修改该归并算法。)
答:
- < 1 , 5 > 、 < 2 , 5 > 、 < 3 , 4 > 、 < 3 , 5 > 、 < 4 , 5 > <1, 5>、<2, 5>、<3, 4>、<3, 5>、<4, 5> <1,5>、<2,5>、<3,4>、<3,5>、<4,5>。
- 如果集合 1 , 2 , ⋯ , n {1, 2, \cdots, n} 1,2,⋯,n中的元素是以降序排列的,那么组成的逆序对最多。它有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)个逆序对。
- 如果输入数组中的逆序对数量越多,则插入排序的运行时间就越大。对于数组中的的逆序对 < i , j > <i, j> <i,j>,在应用插入排序的时候,为了保证数组按正序排列,必定会交换逆序对 < i , j > <i, j> <i,j>。每一个逆序对会花费时间 t t t,那么在排序数组的时候,总共花费的时间就是逆序对的数目 m ⋅ t m\cdot t m⋅t。所以,插入排序的运行时间与输入中逆序对的数量之间呈正比关系。
INVERSION-MERGE(A, p, q, r)
result = 0
for m = p to to q
for n = q + 1 to r
if A[m] < A[n]
break
result++
n1 = q - p + 1
n2 = r - q
let L[1 .. n1] and R[1 .. n2] be new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
i = 1
j = 1
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
return result
INVERSION-SORT(A, p, r)
result = 0
if p < r
q = (p + r) / 2
result += INVERSION-SORT(A, p, q)
result += INVERSION-SOFT(A, q + 1, r)
result += INVERSION - MERGE(A, p, q, r)
return result