Introduction_to_Algorithms_chap2

第二章 算法基础

课后练习

2.1-2 重写过程INSERTION-SORT,使之按非升序排序。

INSERTION-SORT(A):
1 for j = 2 to A.length
2     key = A[j]
3     i = j-1
4     while i > 0 and A[i] < key
5         A[i+1] = A[i]
6         i = i-1
7     A[i+1] = key



2.1-3 考虑以下查找问题

**输入:**n个数的一个序列A={a1, a2, …, an}和一个值v。

输出:下标i使得v=A[i]或者当v不在A中出现时,v为特殊值NIL。

写出线性查找的伪代码,它扫描整个序列来查找v。使用一个循环不变式来证明你的算法是正确的。确保你的循环不变式满足三条必要的性质。

FIND_V(A, v):
1 for i = 1 to A.length
2     if v == A[i]
3         return i
4 return NIL

证明循环不变式:

初始化:当j=1时,循环不变式成立,当v!=A[i]的时候,将会继续循环,否则返回下标。

保持:对于没一个for循环体,直到找到v=A[i]的情况,说明A[1…i-1]的值都不等于v,保持了循环不变式。

终止:当发现了A[i]==v时,返回i的值,函数终止;i = A.length+1,说明A中没有值等于v,返回NIL,函数终止,满足循环不变式。


2.1-4 考虑把两个n位二进制整数加起来的问题,这两个证书分别存储在两个n数组A和B中。这两个整数的和应按二进制形式存储在一个(n+1)元数组C中。写出伪代码。

ADD_BINARY(A, B, C): // A.length and B.length is same.
1  in = 0
2  for i = 1 to A.length
3      tmp = A[i]+B[i]+in
4      if tmp == 3
5          in = 1
6          C[i] = 1
7      else if tmp == 2
8          in = 1
9          C[i] = 0
10     else
11         in = 0
12         C[i] = tmp
13 C[i+1] = in


2.2-2 考虑排序存储在数组A中的n个数:首先找出A中的最小元素并将其与A[1]中的元素进行交换。接着,找出A中的次最小元素并将其与A[2]中的元素进行交换。对A中前n-1个元素按该方式继续。该算法称为选择算法,写出其伪代码。该算法维持的循环不变式是什么?为什么它只需要对前n-1个元素,而不是对所有n个元素运行?用Θ记号给出线性查找的平均情况和最坏运行时间。

SELECT_SORT(A): // A.length = n
1 for i = 1 to n-1
2     smallest = i
3     for j = i+1 to n
4         if A[j] < A[smallest]
5             smallest = j
6     swap(A[i], A[smallest])  // exchange A[i] with A[smallest]

这算法维持了循环不变式。在初始化的时候,A[1…i]即为A[1],A[1]为最小的值。每一次迭代之后,A[1…i]都保持了有序性。最后一次迭代之后,次最大值放在了i-1的位置,最大值也就放在了i的位置。该算法所用的时间为Θ(n^2)。


2.2-3 再次考虑线性查找问题(参见练习2.1-3)。假定要查找的元素可能地为数组中的任意元素,平均需要检查输入序列的多少元素?最坏情况又如何呢?用Θ记号给出线性查找的平均情况和最坏情况的运行时间。

平均需要检查的输入序列的一半元素,最坏情况将会检查全部的元素,所以平均检查n/2个元素,最坏检查n个元素。假设检查一个元素所用的时间为c,那么这两种情况所花的时间分别为nc/2和nc,用Θ记号表示都是Θ(n)。


2.2-4 我们可以如何修改几乎任意算法来使之具有良好的最好运行时间?

可以在算法的开始检查输入是否满足某些特殊情况,如果是的话,输出一个预先计算好的答案。但是一般而言,最佳运行时间不是一个衡量算法效率的好方法。


2.3-2 重写过程MERGE,使之不使用哨兵,而是一旦数组L或R的所有元素均被复制回A就立刻停止,然后把另外一个数组的剩余部分复制回A。

MERGE(A, p, q, r):
1  n1 = q - p + 1
2  n2 = r - q
3  let L[1..n1] and R[1..n2] be new arrays
4  for i = 1 to n1
5      L[i] = A[p + i - 1]
6  for j = 1 to n2
7      R[j] = A[q + j]
8  i = j = 1
9  for k = p to r
10     if j == n2 || L[i] <= R[j]
11         A[k] = L[i]
12         i = i + 1
13     else
14         A[k] = R[j]
15         j = j + 1


2.3-4 我们可以把插入排序表示如下的一个递归过程。为了排序A[1..n],我们递归地排序A[1..n-1],然后把A[n]插入已排好序的数组A[1..n-1]。为插入排序的这个递归版本的最坏情况运行时间写一个递归式。

T(n) = 1, n = 2;
T(n) = T(n-1) + n, n = 2^k, k > 1

经过计算之后得到:T(n) = (n+1)n/2,写成Θ形式为Θ(n^2)。


2.3-5 回顾查找问题(参见练习2.1-3),注意到,如果序列A已排好序,就可以将该序列的中点与v进行比较。根据比较的结果,原序列中有一半就可以不用再做进一步的考虑了。二分查找算法重复这个过程,每次都将序列剩余部分的规模减半。为二分查找写出迭代或递归的伪代码。证明:二分查找的最坏情况的运行时间为Θ(lgn)。

伪代码(递归版本):

FIND_V_BANIRY(A, v, start, ending):
1 if start > ending
2     return NIL
3 mid = (ending + start)/2
4 if v == A[mid]
5     return mid
6 else if v < A[mid]
7     return FIND_V_BANIRY(A, v, start, mid-1)
8 else return FIND_V_BANIRY(A, v, mid+1, ending)

伪代码(非递归版本):

FIND_V_BANIRY(A, v, start, ending):
1 while start < ending
2    mid = (start + ending)/2
3    if v == A[mid]
4        return mid
5    else if v < A[mid]
6        ending = mid - 1
7    else start = mid + 1
8 return NIL

最坏运行时间分析:

T(n) = 1, start == ending
T(n) = T(n/2) + 1, start < ending

将递归是T(n) = T(n/2) + 1写为递归树的形式,树高为lgn + 1,对于每一层所花的时间开销都是1,所以二分查找的最坏运行时间为Θ(lgn)。


2.3-7 描述一个运行时间为Θ(n lgn)的算法,给定n个整数的集合S和另外一个整数x,该算法能确定S中是否存在两个其和刚好为x的元素。

解题思路:

1)对集合S进行排序

2)从集合S中选择一个元素S(i),计算x与S(i)的差值y=x-S(i)。在集合S中查找除S(i)之外的元素中是否存在y,如果存在,则返回。

3)检查是否全部元素已遍历,如果没有跳到第2步。

接下来确定该思路的复杂度。第1步使用归并排序来排序,时间复杂度为Θ(nlg(n));二分查找的时间复杂度为Θ(lg(n)),第2、3步需要遍历的次数为n,因此第2、3步的时间复杂度为Θ(nlg(n)),因此总的时间复杂度为Θ(nlg(n)),符合题目的要求。

伪代码:

FIND_SUM_IS_X(S, x):
1 MERGE_SORT(S, 1, S.length)
2 for i = 1 to S.length
3     y = x - S[i]
4     j = FIND_V_BANIRY(S, y, start, ending, i)
5     if j != NIL
6         return true
7 return false

FIND_V_BANIRY(S, y, start, ending, i):
1  while start < ending
2     mid = (start + ending)/2
3     if mid == i
4         if y <= S[mid - 1]
5             ending = mid - 1
6         else start = mid + 1
7     else if y == S[mid]
8         return mid
9     else if y < S[mid]
10        ending = mid - 1
11    else start = mid + 1
12 return NIL

C语言实现: 代码详见github


思考题

2-1 (在归并排序中对小数组采用插入排序) 虽然归并排序的最坏情况运行时间为Θ(nlgn),而插入排序的最坏情况运行时间为Θ(n^2),但是插入排序的常量因子可能使得它在n较小时,在许多机器上实际运行得更快。因此,在归并排序中当子问题变得足够小时,采用插入排序来是递归的叶变粗是有意义的。考虑对归并排序的一种修改,其中使用插入排序来排序长度为k的n/k个子表,然后使用标准的合并机制来合并这些子表,这里k是一个待定的值。

a. 证明:插入排序最坏情况可以在Θ(nk)时间内排序每个长度为k的n/k个子表。
b. 表明在最坏情况下如何在Θ(nlg(n/k))时间内合并这些子表。
c. 假定修改后的算法的最坏情况运行时间为Θ(nk+nlg(n/k)),要使修改之后的算法与标准的归并排序具有相同的运行时间,作为n的一个函数,借助Θ记号,k的最大值是什么?
d. 在实践中,我们应该如何选择k?

函数的等价树结构如下

a. 在叶子层中有(n/k)个需要进行插入排序的小数组,最坏情况下插入排序所需要的时间为ak^2+bk+c,所以插入排序总共需要的时间为akn+bn+c/k,表示为Θ形式为Θ(kn)。

b. 树的高度为lg(n/k)+1,合并每一层(叶子层除外)所要的时间都是cn,叶子层花费的时间为所以Θ(kn),所以总共花费的时间是cnlg(n/k)+Θ(kn),所以合并所花的时间即为Θ(nlg(n/k))。

c. 问题等价于求解 Θ(nk + nlg(n/k)) = Θ(nlgn)。其最大渐近值为lgn,所以k最大值为lgn。

d. 在实际情况中,在k的合法取值范围内分别计算不同k的值,得到最适合的k值。


2-2(冒泡排序算法的正确性)冒泡排序算法是一种流行但低效的排序算法,它重复地交换相邻的两个反序元素。

BUBBLESORT(A)
1 for i = 1 to A.length-1
2     for j = A.length downto i + 1
3         if A[j] < A[j-1]
4             exchange A[j] with A[j-1]
a) 设A′表示BUBBLESORT(A)的输出。为了证明BUBBLESORT是正确的,我们必须证明它将终止并且有:
A'[1] ≤ A'[2] ≤ ... ≤ A'[n]                     (2.3)
其中n=A.length。为了证明BUBBLESORT的确能实现排序的效果,还需要证明什么?下面两个部分将证明不等式(2.3)。
b)为第2~4行的for循环精确地说明一个循环不变式,并证明该循环不变式成立。在证明中应采用本章中给出的循环不变式证明结构。
c)利用(b)部分中证明的循环不变式的终止条件,为第1~4行中的for循环给出一个循环不变式,它可以用来证明不等式(2.3)。你的证明应采用本章中给出的循环不变式的证明结构。
d)冒泡排序算法的最坏情况运行时间是什么?比较它与插入排序的运行时间。

解答:

a) A中所有的元素都在A’中,或者说A中的任意元素在A’中存在且唯一(一一对应)。

b) 冒泡排序中,需要证明子数组A[j-1..n]的最小元素为A[j-1]。 初始、保持、终止三元素描述如下:

初始:j=n,子数组为A[j-1..n]=A[n-1..n]有两个元素。在循环内部,通过条件交换语句,可以保证A[n-1] < A[n]成立。因此A[j-1]是A[j-1..n]中的最小元素。

保持:每次迭代开始时,A[j]是A[j..n]中的最小元素。在迭代操作中,当A[j] < A[j-1]时交换,因此总有A[j-1] < A[j]。可知,本次迭代操作完成后,A[j-1]一定是A[j-1..n]中的最小元素。

终止:j=i+1时退出,因此结束时,A[i]一定是A[i..n]中的最小元素。

c) 描述如下:

初始:i=1,是A中的第一个元素,因此内部循环完成后,可以保证A[1]中保存A[1..n]的最小元素。

保持:每次递增i时,执行内部循环,因此A[i]中保存A[i..n]中的最小元素。可知每次内部循环完成后,都有 A[1] ≤ A[2] ≤ … ≤ A[i]。

终止:i=length[A]时终止,此时有 A[1] ≤ A[2] ≤ … ≤ A[n]。

d)冒泡排序最坏和最好运行时间均为Θ(n^2);

插入排序的最坏运行时间为Θ(n^2),但是最好运行时间为Θ(n);

排序前A所有元素已经有序时,插入排序达到最好运行时间。

2.3( 霍纳规则的正确性)给定系数a0, a1, …, an以及x的值,代码片段:

1 y = 0
2 for i = n downto 0
3     y = ai + x*y

实现了用于计算多项式

P(x) = a0 + x(a1 + x(a2 + ... + x(an-1 + xan)...))

的霍纳规则(Horner’s rule)。

a)借助Θ记号,实现霍纳规则的代码片段的运行时间是多少?
b)写出伪代码以实现朴素多项式求值(naive polynomial-evaluation)算法,该算法从头开始计算多项式的每一项。该算法的运行时间是多少?与霍纳规则相比,其性能如何?
c)考虑以下循环不变式:

在第2~3行for循环每次迭代的开始有

y = ai+1 * x + ai+2 * x^2 + ... + ai+1+k * x^k

把没有项的和式解释为等于0。遵照本章中给出的循环不变式证明的结构,使用该循环不变式来证明终止时有

y = a0 + a1*x + a2*x2 + ... + an*x^n
d)最后证明以上给出的代码片段能够正确地计算由系数a0, a1, …, an刻划的多项式的值。

解答:

a) Θ(n)

b)

NAIVE_POLYNOMIAL_EVALUATION(A, x):
1 y = 0
2 for i = 0 to A.length
3     v = 1
4     for j = 1 to i
5         v = v * x
6     y = y + A[i] * v

所花费的时间为Θ(n^2),性能上低于霍纳规则。

c)

由于k从0到n-(i+1),因此有:

y = Σ ak+i+1 * x^k
  = ak+i+1 + ak+i+2 * x + ... + an * x^(n-(i+1))

霍纳规则代码段循环不变式证明如下:

初始:i=n,y[n] = 0,迭代开始时,循环后有y[n] = a[n]。

保持:对于任意 0 ≤ i ≤ n,循环后有:

y[i] = a[i] + y[i+1] * x = a[i] + (a[i+1] * x + a[i+2] * x + ... + a[n] * x^(n-(i+1))) * x
     = a[i] + a[i+1] * x + a[i+2] * x^2 + ... + a[n] * x^(n-i)

终止:i小于0时终止,此时有 y[0] = a[0] + a[1] * x + a[2] * x^2 + a[n] * x^n

证明和y = Σ a[k+i+1] * x^k的关系:

k 从0到n-(i+1),等价于 0 ≤ k ≤ n-(i+1)。因此

y = Σ a[k+i+1] * x^k
  = a[i+1] + a[i+2] * x + ... + a[n-(i+1)+i+1] * x^(n-i)
  = a[i+1] + a[i+2] * x + ... + a[n] * x^(n-i)

由于i+1循环之后和i循环之前的值相等,用y’[i]表示i循环之前的值,则有:

y'[i] = y[i+1]

霍纳规则循环不变式的结果表明:

y[i] = a[i] + a[i+1] * x + a[i+2] * x^2 + ... + a[n] * x^(n-i)

因此有:

y'[i] = y[i+1] = a[i+1] + a[i+2] * x + ... + a[n] * x^(n-(i+1))

令k=n-(i+1),则n=k+i+1,所以:

y'[i] = a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^(k+i+1-(i+1))
    = a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^k

用y表示y’[i],则有:

y = a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^k
  = Σ a[k+i+1] * x^k

其中 k从0到n-(i+1)

证毕。

2.4 (逆序对) 设A[1..n]是一个包含n个不同数的数组。如果i

a)列出数组〈2,3,8,6,1〉的5个逆序。
b)由集合{1, 2, …, n}中的元素构成的什么数组具有最多的逆序对?它有多少逆序对?
c)插入排序的运行时间与输入数组中逆序对的数量之间有怎样的关系?证明你的答案。
d)给出一个算法,它能用Θ(nlgn)的最坏情况运行时间,确定n个元素的任何排列中逆序对的数量。(提示:修改合并排序)

解答:

a) (2,1),(3,1),(8,6),(8,1),(6,1)

b) 倒序数组{n, …, 2, 1},它总共有n(n-1)/2个逆序对。

c)

逆序对增加时,插入排序时间增加。

没有逆序对时,插入排序时间最少,为Θ(n)。

逆序对最多时,插入排序时间最多,为Θ(n^2)。

d)

解题思路:初始化count为0,对数组执行合并排序,在合并(MERGE)操作中,只要L[i] > R[i],那么R[i]和L[i…n1-1]都是逆序对

C语言实现: 代码详见github

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值