算法导论第二章习题笔记
文章目录
- 算法导论第二章习题笔记
- 习题
- 2.1-2:重写插入排序过程,使之按非升序(而不是非降序)排序。
- 2.1-3 :查找问题:
- 2.1-4:考虑把两个n位二进制整数加起来的问题,这两个整数分别存储在两个n元数组A和B中。这两个整数的和应按二进制形式存储在一个(n+1)元数组C中。请给出该问题的形式化描述,并写出伪代码。
- 2.2-1:用θ记号表示函数$n^3/1000-100n^2-100n+3$。
- 2.2-2:考虑排序存储在数组A中n的个数:首先找出A中的最小元素并将其与A[1]中的元素进行交换。接着,找出A中的次最小元素并将其与A[2]中的元素进行交换。对A中前n-1个元素按该方式继续。该算法称为**选择算法**,写出其伪代码。该算法的循环不变式是什么?为什么它只需要对前n-1个元素,而不是对所有n个元素进行?用O()方式给出选择排序的最好情况与最坏情况运行时间。
- 2.2-3:再次考虑线性查找问题(参见练习2.1-3)。假定要查找的元素等可能地为数组中的任意元素,平均需要检查输入序列的多少元素?最坏情况又如何?用O()方式给出平均情况和最坏情况运行时间。证明答案。
- 2.2-4:应如何修改任何一个算法,才能使之具有良好的最好运行时间情况。
- 2.3-2:重写过程MERGE,使之不用哨兵,而是一旦数组L或R的所有元素均被复制回A就立刻停止,然后把另一个数组的剩余部分复制回A。
- 2.3-3:使用数学归纳法证明:当 $n$ 刚好是2的幂时,以下递归式的解是$T(n) = nlgn$。
- 2.3-4:我们可以把插入排序表示为如下的一个递归过程。为了排序$A[1..n]$,我们递归地排序$A[1..n-1]$,然后把$A[n]$插入已排序的数组$A[1..n-1]$。为插入排序的这个递归版本的最坏情况运行时间写一个递归式。
- 2.3-5:回顾查找问题(2.1-3),注意到,如果序列$A$已排好序,就可以将该序列的中点与 $v$ 进行比较。根据比较的结果,原序列中有一半就可以不用再做进一步的考虑了。二分查找算法重复这个过程,每次都将序列剩余部分的规模减半。为二分查找写出迭代或递归的伪代码。证明:二分查找的最坏情况运行时间为$\Theta(lg\ n)$。
- 2.3.6:利用二分查找将插入排序的最坏情况总运行时间改进到$\Theta(nlg\ n)$。
- 2.3-7描述一个运行时间为$\Theta(n\ lg\ n)$的算法,给定$n$个整数的集合$S$和另一个整数$x$,该算法能确定$S$中是否存在两个其和刚好为$x$的元素。
- 思考题
习题
2.1-2:重写插入排序过程,使之按非升序(而不是非降序)排序。
For j=2 to A.length
Key = A[j]
i = j – 1
while i > 0 and A[i] < key
A[i+1] = A[i]
i = i – 1
A[i + 1] = key
2.1-3 :查找问题:
输入:n个数的一个序列A=<a1, a2, …, an>和一个值v。
输出:下标i使得v = A[i] 或当v不在A中出现时。V为特殊值NIL。
写出线性查找的伪代码。用循环不变时来证明算法的正确性。
For i=1 to A.length
if A[i] == v
return i
return NIL
循环不变式:子数组(已遍历的数组)中没有与v的值相等的。
初始化:子数组为空,必定没有与v相等的,循环不变式成立。
保持:每次迭代都会对子数组中新的数的值进行判断。如果与v相等,则循环终止。如果不等,则A[1,2,…i] 这个子数组中没有与v相等的,继续循环,循环不变式保持。
终止:当i > A.length时或A[i] == v时循环终止。但此时子数组(下标小于i的数字)中是没有与v相等的数的,算法成立。
2.1-4:考虑把两个n位二进制整数加起来的问题,这两个整数分别存储在两个n元数组A和B中。这两个整数的和应按二进制形式存储在一个(n+1)元数组C中。请给出该问题的形式化描述,并写出伪代码。
C = [0,0,……0] (C.length = A.length + 1)
For i=1 to A.length:
if A[i] + B[i] + C[i] == 0:
C[i] = 0
else if A[i] + B[i] + C[i] == 1:
C[i] = 1
else if A[i] + B[i] + C[i] == 2:
C[i] = 0
C[i+1] = 1
else if A[i] + B[i] + C[i] == 2:
C[i] = 1
C[i+1] = 1
C = C.reverse
2.2-1:用θ记号表示函数 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中n的个数:首先找出A中的最小元素并将其与A[1]中的元素进行交换。接着,找出A中的次最小元素并将其与A[2]中的元素进行交换。对A中前n-1个元素按该方式继续。该算法称为选择算法,写出其伪代码。该算法的循环不变式是什么?为什么它只需要对前n-1个元素,而不是对所有n个元素进行?用O()方式给出选择排序的最好情况与最坏情况运行时间。
for i=1 to A.length - 1:
min_num = inf
min_index = i
for j=i to A.length:
if A[j] < min:
min = A[j]
min_index = j
temp = A[i]
A[i] = min
A[min_index] = temp
该算法的循环不变式是A[1,2,……i]这个子数组是已排序的。
因为当该算法对前n-1个元素执行完毕时,最后一个元素一定是比前n-1个元素大的,否则它将被替换进前n-1个元素中。故它只需要对前n-1个元素执行该算法。
最好情况为该数组已经排序完成,则只需要遍历整个数组,而不需要进行任何的交换操作。而两次循环的时间为O(n2)。
同理,最坏情况也需要完成两次循环,额外的情况只是几次交换操作,是常数级的,因此最坏情况运行时间也为O(n2)。
2.2-3:再次考虑线性查找问题(参见练习2.1-3)。假定要查找的元素等可能地为数组中的任意元素,平均需要检查输入序列的多少元素?最坏情况又如何?用O()方式给出平均情况和最坏情况运行时间。证明答案。
平均需要检查输入序列的一半元素。最坏情况需要检查输入序列的全部元素。两者的运行时间均为O(n)。
平均情况需要检查输入序列的一半元素,为 $ \frac{1}{2}n$ 。而最坏情况为 n n n。由于O()方式不在意常数因子,因此两种情况的O()方式的运行时间均为O(n)。
2.2-4:应如何修改任何一个算法,才能使之具有良好的最好运行时间情况。
应当把某些可想到的最好情况写在算法的开头,就可以得到良好的最好运行时间。如一些边界条件。
2.3-2:重写过程MERGE,使之不用哨兵,而是一旦数组L或R的所有元素均被复制回A就立刻停止,然后把另一个数组的剩余部分复制回A。
MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1..n1+1] and R[1..n2+1] 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 = j = 1
for k = p to r
if L[i] << R[j]
A[k] = L[i]
if i = n1:
A[k + 1 .. n1 + n2] = R[j .. n2] //将R中剩下部分复制回A
else i = i + 1
else
A[k] = R[j]
if j = n2:
A[k + 1 .. n1 + n2] = L[i .. n1] //将L中剩下部分复制回A
else j = j + 1
2.3-3:使用数学归纳法证明:当 n n n 刚好是2的幂时,以下递归式的解是 T ( n ) = n l g n T(n) = nlgn T(n)=nlgn。
若 若 T ( n ) = { 2 若 n = 2 2 T ( n / 2 ) + n 若 n = 2 k , k > 1 若若T(n) = \begin{cases}2& {若n=2}\\2T(n/2)+n&{若n=2_k,k>1}\end{cases} 若若T(n)={22T(n/2)+n若n=2若n=2k,k>1
当 n = 2 n=2 n=2时, T ( 2 ) = 2 ∗ l g 2 = 2 T(2)=2*lg2=2 T(2)=2∗lg2=2,得证。
当 n = 4 n=4 n=4时, T ( 4 ) = 4 ∗ l g 4 = 8 T(4)=4*lg4=8 T(4)=4∗lg4=8, T ( 4 ) = 2 T ( 2 ) + 4 = 2 ∗ 2 + 4 = 8 T(4) = 2T(2)+4 = 2*2+4=8 T(4)=2T(2)+4=2∗2+4=8,得证。
当 n = k n=k n=k时, T ( k ) = 2 T ( k / 2 ) + k = … = k / 2 ∗ T ( 2 ) + k ( l g k − 1 ) = k + k ( l g k − 1 ) = k l g k T(k)=2T(k/2)+k=…=k/2*T(2) + k(lgk-1)=k + k(lgk-1)=klgk T(k)=2T(k/2)+k=…=k/2∗T(2)+k(lgk−1)=k+k(lgk−1)=klgk,得证。
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]。为插入排序的这个递归版本的最坏情况运行时间写一个递归式。
最坏运行时间为 T ( n ) = Θ ( n 2 ) T(n)=\Theta(n^2) T(n)=Θ(n2)。
T ( n ) = { Θ ( 1 ) i f n = 1 T ( n − 1 ) + Θ ( n ) i f n > 1 T(n)=\begin{cases}\Theta(1)&if\ n=1\\T(n-1) + \Theta(n) &if\ n>1\end{cases} T(n)={Θ(1)T(n−1)+Θ(n)if n=1if n>1
2.3-5:回顾查找问题(2.1-3),注意到,如果序列 A A A已排好序,就可以将该序列的中点与 v v v 进行比较。根据比较的结果,原序列中有一半就可以不用再做进一步的考虑了。二分查找算法重复这个过程,每次都将序列剩余部分的规模减半。为二分查找写出迭代或递归的伪代码。证明:二分查找的最坏情况运行时间为 Θ ( l g n ) \Theta(lg\ n) Θ(lg n)。
binary search(A, v, p, q)
if q > p
r = (q - p) // 2
if v > A[r]
binary search(A, v, r, q)
if v < A[r]
binary search(A, v, p, r)
if v == A[r]
return r
if q == p
if A[q] == v
return q
else return NIL
T ( n ) = { 1 i f n = 1 T ( n / 2 ) + 1 i f n > 2 T(n)=\begin{cases}1&if\ n=1\\T(n/2) +1 &if\ n>2\end{cases} T(n)={1T(n/2)+1if n=1if n>2
证明:
T ( n ) = T ( n / 2 ) + 1 = T ( n / 4 ) + 3 = . . . = 1 + l g n = Θ ( l g n ) T(n)=T(n/2) + 1=T(n/4)+3=...=1+lg\ n = \Theta(lg\ n) T(n)=T(n/2)+1=T(n/4)+3=...=1+lg n=Θ(lg n)
2.3.6:利用二分查找将插入排序的最坏情况总运行时间改进到 Θ ( n l g n ) \Theta(nlg\ n) Θ(nlg n)。
是可以的,只需要将插入排序中查找该对应位置的循环改成二分查找即可。伪代码如下。
binary search(A, v, p, q)
if q > p
r = (q - p) // 2
if v > A[r]
binary search(A, v, r, q)
if v < A[r]
binary search(A, v, p, r)
if v == A[r]
return r
if q == p + 1
return p
2.3-7描述一个运行时间为 Θ ( n l g n ) \Theta(n\ lg\ n) Θ(n lg n)的算法,给定 n n n个整数的集合 S S S和另一个整数 x x x,该算法能确定 S S S中是否存在两个其和刚好为 x x x的元素。
可以看看leetcode上的第一题两数之和,与这道题其实是一样的。
使用归并排序将集合 S S S 排列好,时间为 Θ ( n l g n ) \Theta(n\ lg\ n) Θ(n lg n)然后遍历一边集合 S S S,将每个元素都记录到一个hash表中。再遍历一次集合,查看hash表中是否有与该值相加为 x x x的元素。因为查找hash表的时间为 Θ ( 1 ) \Theta(1) Θ(1),因此两次遍历的时间也只为 Θ ( n ) \Theta(n) Θ(n),小于 Θ ( n l g n ) \Theta(n\ lg\ n) Θ(n lg n),因此运行时间为 Θ ( n l g n ) \Theta(n\ lg\ n) Θ(n lg n)。
思考题
2.1在归并排序中对小数组采用插入排序。
a.证明:插入排序最坏情况可以在 Θ ( n k ) \Theta(nk) Θ(nk)时间内排序每个长度为 k k k的 n / k n/k n/k个子表。
每个子表的时间为 Θ ( n 2 ) \Theta(n^2) Θ(n2),则总时长为 Θ ( k 2 ) ∗ n / k = T h e t a ( n k ) \Theta(k^2)*n/k=Theta(nk) Θ(k2)∗n/k=Theta(nk)。
b.表明在最坏情况下如何在 Θ ( n l g ( n / k ) ) \Theta(n\ lg(n/k)) Θ(n lg(n/k))时间内合并这些子表。
每层 n n n个元素,共需要复制 n n n个元素。共 n / k n/k n/k个子表,共需要合并 l g ( n / k ) lg(n/k) lg(n/k)次。则为 n ∗ l g ( n / k ) = Θ ( n l g ( n / k ) ) n*lg(n/k)=\Theta(n\ lg(n/k)) n∗lg(n/k)=Θ(n lg(n/k))。
c.如果已知修改后的合并排序算法的最坏运行时间为 Θ ( n k + n l g ( n / k ) ) \Theta(nk+n\ lg(n/k)) Θ(nk+n lg(n/k)),要使修改后的算法与标准的归并排序具有相同的运行时间,作为一个n的函数,用 Θ \Theta Θ表示, k k k的最大值是什么。
令 n k + n l g ( n / k ) = n l g n nk+n\ lg(n/k)=n\ lg\ n nk+n lg(n/k)=n lg n,有 k + l g ( n / k ) = l g n k+lg(n/k)=lg\ n k+lg(n/k)=lg n,忽略 l g ( n / k ) lg(n/k) lg(n/k),得到结果为 k = l g n k=lg\ n k=lg n。
d.在实践中,我们应该如何选择k。
根据实践中的 n n n来决定,只需要在当前 k k k下,插入排序比归并排序快即可。
2.2冒泡排序的正确性。
a.证明冒泡排序的正确性,除了证明它将终止且为非降序,还需要证明什么?
还需要证明,最终序列和原序列的构成是相同的。
b.证明冒泡排序的循环不变式。
循环不变式:每次迭代中, A [ j ] = m i n { A [ k ] : j ≤ k ≤ n } A[j]=min\{A[k]:j\le k\le n\} A[j]=min{A[k]:j≤k≤n},并且子数组是原数组的一部分。
初始化: 开始时, j = n j=n j=n, A [ j . . n ] A[j..n] A[j..n]中只有 A [ n ] A[n] A[n]一个元素,显然成立。
保持: A [ j ] A[j] A[j]是A [ j . . n ] [j..n] [j..n]中的最小值,那么,由 i f if if判断语句我们会发现, A [ j − 1 ] A[j−1] A[j−1]大于 A [ j ] A[j] A[j],所以肯定会交换,这样一来 A [ j ] A[j] A[j] 就是 A [ j − 1.. n ] A[j−1..n] A[j−1..n]的最小值,由此下去,我们就会正确的把后一部分的最小值移动到前面已经排好的子序列里面,加上前面排好的 A [ 1.. i + 1 ] A[1..i+1] A[1..i+1] 是原数组的子数组,后面未排序的 A [ i + 2.. n ] A[i+2..n] A[i+2..n]也是原数组的子数组,不变式成立。
终止: 终止时 , i = n , j = n ,i=n,j=n ,i=n,j=n,如果排序完成, A [ j ] = m i n { A [ k ] : j ≤ k ≤ n } A[j]=min\{A[k]:j≤k≤n\} A[j]=min{A[k]:j≤k≤n}显然成立,因为 A [ j . . n ] A[j..n] A[j..n]中只有 A [ n ] A[n] A[n]。
c.给出一个循环不变式,并且用它来证明 ( ) ( A ′ [ 1 ] ≤ A ′ [ 2 ] ≤ A ′ [ 3 ] ≤ . . . A ′ [ n ] ) ()(A′[1]≤A′[2]≤A′[3]≤...A′[n]) ()(A′[1]≤A′[2]≤A′[3]≤...A′[n])。
**循环不变式:**子数组 A [ 1.. i − 1 ] A[1..i−1] A[1..i−1]里是原始数组中前 i − 1 i-1 i−1个最小数据,并且已经排好顺序,并且 A [ i . . n ] A[i..n] A[i..n]中是原始数组中除 A [ 1.. i − 1 ] A[1..i−1] A[1..i−1]外的部分。
证明同上。
d.冒泡排序的最坏情况运行时间是多少?与插入排序的运行时间比,其性能如何。
冒泡排序的最坏情况运行时间为 Θ ( n 2 ) \Theta(n^2) Θ(n2)。与插入排序同为 Θ ( n 2 ) \Theta(n^2) Θ(n2)。
2.3(霍纳(Horner)规则的正确性) 给定系数 , a 0 , a 1 , ⋅ ⋅ ⋅ , a n ,a_0,a_1,···,a_n ,a0,a1,⋅⋅⋅,an和 x x x的值,代码片段
y = 0
for i = n downto 0
y = a[i] + 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^{n}_{k=0}a_kx^k=a_0+x(a_1+x(a_2+···+x(a_n-1+xa_n) ··· )) P(x)=∑k=0nakxk=a0+x(a1+x(a2+⋅⋅⋅+x(an−1+xan)⋅⋅⋅))
的霍纳规则。
a.借助 Θ \Theta Θ记号,实现霍纳规则的以上代码的运行时间是多少?
单循环。运行时间为 Θ ( n ) \Theta(n) Θ(n)。
b.编写伪代码来实现朴素的多项式求值算法,该算法从头开始计算多项式的每个项。该算法的运行时间是多少?与霍纳规则相比,其性能如何?
y = 0
for i = 0 to n
y = y + a[i] * x^i
双循环(x的i次幂相当于一次循环),运行时间为 Θ ( n 2 ) \Theta(n^2) Θ(n2)。霍纳规则性能更优。
c.考虑以下循环不变式:
在第2~3行for循环每次迭代的开始有: y = ∑ k = 0 n − ( i + 1 ) a k + i + 1 x k y=\sum^{n-(i+1)}_{k=0}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^{n}_{k=0}a_kx^k y=∑k=0nakxk。
循环不变式证明如下:
**初始化:**循环开始前, y = 0 , i = n , k = 0 , − 1 y=0,i=n,k=0,-1 y=0,i=n,k=0,−1,有 y = a n = a n x 0 y=a_n=a_nx^0 y=an=anx0
**保持:**对任意 ( 0 ≤ i < n ) (0\leq i<n) (0≤i<n),有 y ( i ) = a i + x ∗ y ( i + 1 ) = ∑ k = 0 n − ( i + 1 ) a k + i + 1 x k y(i)=a_i+x*y(i+1)=\sum^{n-(i+1)}_{k=0} a_{k+i+1}x^k y(i)=ai+x∗y(i+1)=∑k=0n−(i+1)ak+i+1xk
终止: i = − 1 i=-1 i=−1时,有 y = ∑ k = 0 n a k x k y=\sum^{n}_{k=0}a_kx^k y=∑k=0nakxk
综上,得证。
d.最后证明上面给出的代码片段将正确地求由系数 a 0 , a 1 , ⋅ ⋅ ⋅ , a n a_0,a_1,···,a_n a0,a1,⋅⋅⋅,an刻画的多项式的值。
同上。
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的一个逆序对(inversion)。
a.列出 〈 2 , 3 , 8 , 6 , 1 〉 〈2,3,8,6,1〉 〈2,3,8,6,1〉的5个逆序。
〈 2 , 1 〉 , 〈 3 , 1 〉 , 〈 8 , 1 〉 , 〈 6 , 1 〉 , 〈 8 , 6 〉 〈2,1〉,〈3,1〉,〈8,1〉,〈6,1〉,〈8,6〉 〈2,1〉,〈3,1〉,〈8,1〉,〈6,1〉,〈8,6〉
b.由集合 1 , 2 , ⋅ ⋅ ⋅ , n {1,2,···,n} 1,2,⋅⋅⋅,n中的元素构成的什么数组具有最多的逆序对?它有多少逆序对?
从大到小排列的数组: { n , n − 1 , ⋅ ⋅ ⋅ , 1 } {n,n-1,···,1} {n,n−1,⋅⋅⋅,1},有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)对逆序对。
c.插入排序的运行时间与输入数组中逆序对的数量之间是什么关系?证明你的回答。
无逆序对时,为最优情况,运行时间为 Θ ( n ) \Theta(n) Θ(n)。
逆序对最多时,为最差情况,运行时间为 。 Θ ( n 2 ) 。 。\Theta(n^2)。 。Θ(n2)。
逆序对数量与交换次数相等。
d.给出一个确定在 n n n个元素的任何排列中逆序对数量的算法,最坏情况需要 Θ ( n l g n ) \Theta(nlg\ n) Θ(nlg n)时间。(提示:修改归并排序。)
修改归并排序使,当左序列指针指向的元素大于右序列的指针指向的元素,计数就自增。
原理也很简单,逆序列就是左边比右边大的数据对,而归并排序正是把这样的数据对调整过来的过程,其实不仅仅是这样,这里可以思考另一个过程,如果我们把归并换成快排,快排的每次排序也是把左大右小的数据对调整过来,实际上基于比较的排序都是这样,但是快排是错误的比如序列{4,3,2,1} {4,3,2,1}就明显不对,其实问题就出在快排每趟排序会破坏原序列的整体关系,比如第一趟排序过后会变成{1,3,2,4} {1,3,2,4} 这里破坏了1和2,3的逆序关系,而归并排序恰恰有从局部开始,不破坏整体关系的优点
所以归并排序的正确性就在于:1 正好是调整逆序数对的过程,2 不破坏数列的整体关系
MERGER(A,p,q,r)
A1 = A[p..q]
A2 = A[q+1..r]
i=1,j=1
count = 0
for k=p to r
if i > A1.length
A[k] = A2[j]
j++
else if j>A2.length
A[k] = A1[i]
i++
else if A1[i] >A2[j]
count = count + A.length - i + 1
A[k] = A2[j]
j++
else
A[k] = A1[I]
i++
return count