算法导论(三版):第二章思考题

第二章:算法基础

2-1 Insertion sort on small arrays in merge sort

Although merge sort runs in θ(nlgn) worst-case time and insertion sort runs in θ(n^2) worst-case time, the constant factors in insertion sort can make it faster in practice for small problem sizes on many machines. Thus, it makes sense to coarsen the leaves of the recursion by using insertion sort within merge sort when subproblems become sufficiently small. Consider a modification to merge sort in which n=k sublists of length k are sorted using insertion sort and then merged using the standard merging mechanism, where k is a value to be determined.
a. Show that insertion sort can sort the n=k sublists, each of length k, in θ(nk) worst-case time.
b. Show how to merge the sublists in θ(n lg(n/k)) worst-case time.
c. Given that the modified algorithm runs in θ(nk + n lgn/k)) worst-case time, what is the largest value of k as a function of n for which the modified algorithm has the same running time as standard merge sort, in terms of ‚θ notation?
d. How should we choose k in practice?

答:
a.
插入排序过程如下:
这里写图片描述
在串行的情况下,排序一个长度为k的数组需要θ(k^2),n/k个的话执行执行θ(k^2 * n/k) = θ(nk)。

本书在此处还没有讲多线程算法,所以暂时没有用多线程的思维来分析这道题。如果这道题用多线程的角度来分析的话,对n/k个长度为k的数组排序,那么总共的时间复杂度只有θ(k^2)而已,这里因为n/k个数组可以并发执行。

b.
这个问题需要参考下面这幅图:
算法导论(三版):第二章思考题_第1张图片
这是标准的归并算法的merge的过程。最后求出标准归并算法的时间复杂度时,是用层高 乘以 每层的代价。
其实本题所求代价也用层高 乘以 每层的代价,只是层高没有那么高,因为当到子数组的规模到了n/k的时候,此时的层高应为log(n/k + 1)。故总代价为:lg(n/k+1) * n ,即为:θ(nlg(n/k))

c.
我解答的过程是首先感受代了几个值感受了一下,

  • 当 k = 1 时,θ(nk + n lg(n/k)) = θ(n+ n lgn) = θ(n lgn),此时和归并算法的时间复杂度一样,但是k取得是最小值。
  • 当 k = n/2 时,θ(nk + n lg(n/k)) = θ((n^2)/2+ n lg2)= θ(n^2),此时为θ(n^2),大于了归并算法。

很容易发现关键就是k和lgn之间的大小关系,则让 k = θ(lgn) (题中要求用θ符号),则可得时间复杂度:θ(nk + n lg(n/k)) = θ(n lgn + n lg (n/lgn)) =θ(n lgn)(其中 lg(n/lgn) < lgn)。
故最终答案:k = θ(lgn)

参考:stack overflow

其实这道题我是解出来了的。但是我不知道这个答案要什么形式。现在回想起来就比较傻了,因为书说了要用θ记号来表示。我不应该强迫自己的

d.
解答可以参考:stack overflow
一段论述比较重要:
k的取值应该恰好是使 插入算法 比 归并算法 快的最大值。而这个值显然是只和插入算法和归并算法的常量相关,不与输入规模n相关。

还有一段引用比较重要:
According to the Algorithms (4th edition) book written by Robert Sedgewick and Kevin Wayne

Switching to insertion sort for small subarrays (length 15 or less, say) will improve the running time of a typical mergesort implementation by 10 to 15 percent.

2-2 Correctness of bubblesort

Bubblesort is a popular, but inefficient, sorting algorithm. It works by repeatedly swapping adjacent elements that are out of order.
算法导论(三版):第二章思考题_第2张图片

a. Let A’ denote the output of BUBBLESORT(A). To prove that BUBBLESORT is correct, we need to prove that it terminates and that
这里写图片描述
where n = A.length. In order to show that BUBBLESORT actually sorts, what else do we need to prove?
The next two parts will prove inequality (2.3).

b. State precisely a loop invariant for the for loop in lines 2–4, and prove that this loop invariant holds. Your proof should use the structure of the loop invariant proof presented in this chapter.

c. Using the termination condition of the loop invariant proved in part (b), state a loop invariant for the for loop in lines 1–4 that will allow you to prove inequality (2.3). Your proof should use the structure of the loop invariant proof presented in this chapter.

d. What is the worst-case running time of bubblesort? How does it compare to the
running time of insertion sort?

答:
a.
从输出来讲,除了证明有序,还需要证明的就是输出数组的元素和输入数组的元素是相同的。但是由于这个算法是原址排序算法,所以我认为除了2-3式没有需要证明的了。
对于一个排序算法,从P9(中文)的描述也可以看出来。

b.
对于2-4行,循环不定式:
在2-4行每一次循环迭代开始时,下标 j 的元素比下标大于 j 的元素的值小

  • 初始化:初始时,j = A.length,没有下标比下标比j大于的元素。故初始化成立。
  • 保持:在每一次for循环的时候,都会将A[j] 与 A[j - 1]比较,并且将两者较小元素放在A[j - 1]的位置。那么在下一次循环开始的时候,j 其实是上一次循环的 j - 1,而在上一次循环已经较小元素放置在了j - 1的位置,那么本次循环的j的位置的元素肯定肯定比小标大于的j的位置的元素小。
  • 终止:什么导致了终止?当 j < i+1时,循环终止。也就是当 j = i 时,循环终止,循环不定式依然成立。这是因为上一循环的j = i + 1 时,已经将 j 与 j - 1 (也就是 i 与 i +1)中较小的元素放在了 j (也就是 i )的位置。

综上,2-4行的for循环能够为我们提供如上所述的循环不定式。

c.
对于1-4行,循环不定式:
在1-4行每一次循环迭代开始时,数组下标为i-1的元素,即为该数组第 i - 1 小的元素。也就说,第 i - 1 小的元素已经位于其整个数组排序后的最终为:第 i - 1位

  • 初始化:当i = 1时,i - 1 = 0。没有这个下标,也就没有这个元素。故初始化成立(这样说确实比较勉强,但保持和终止能很好表示)。
  • 保持:当第 i 次循环开始后,根据 2-2.b 的循环不定式 和 终止的描述, 当内循环(第 2 - 4行)执行终止后 (j = i),下标 为 i 的元素比下标大于 i 的元素的值小。注意伪代码的下标是从1开始的,那么可以说此时 下标为 i 的元素 为 第 i 小的元素。当下一次循环开始后,i - 1 即可为上一个循环终止时的 i , 故循环不定式成立。
  • 终止:什么导致了循环终止?当 i >A.length - 1时,也就是i=A.length时,循环终止。此时 根据循环不定式可知, A.length - 1 的下标 的元素就是数组第 A.length - 1 小的元素。此外可知,下标小于 A.length的元素全部都处于正确的位置,那么下标为A.length位置的元素是最后一个元素,而且此时也只有一个位置,所以该元素也处于正确的位置。此时,整个数组有序排列。

书上初始化的定义,必须是第一次for循环赋值,但是没有开始执行for的语句之前。这样就搞得初始化的语句显得很奇怪。但是保持和终止却能表示的非常清楚。我姑且容忍了这个做法。另外,也有可能我没有想出一个正确的循环不定式。

d.
看图说话:
算法导论(三版):第二章思考题_第3张图片
当第四句每次都执行的时候将会出现最坏的情况。根据第三行,发现只要都输入数组是逆序的时候,第四行每次都执行。
下面来分析每句执行的情况:
1. 由 1 至 A.length, 共 n 次
2. 由于第二行要执行n - 1个循环,且每个循环第二行执行的次数不一样。当 i=1 时,第二行执行 n 次,当 i = A.length - 1 ,第二行执行 2 次。故可以知第二行执行的次数:

                  n
2 + 3 + ... + n = Σi
                  i=2

3. 第三行在每次第二行循环的时候的都少一次,故有:

n
Σ(i - 1)
i=2

4.由于在最坏的情况下,每次第四行都执行,所以与第三行执行的次数一样。

设第i行,的代价都Ci,故有:

          n            n
C1 n + C2 Σi + (C3+C4)Σ(i-1)
         i=2          i=2

与插入算法的类似公式比较:
算法导论(三版):第二章思考题_第4张图片

我们会发现插入算法多了(C2+C4+C8)(n-1)的部分。感觉上插入算法的时间复杂度更长。但其实这个问题是未知的,这是因为我们不知道两个公式中Ci的具体大小。

此外,很容易求解冒泡排序的时间复杂度(以θ形式),和书中求解插入算法几乎一样,见中文版P15页。

2-3 Correctness of Horner’s rule

The following code fragment implements Horner’s rule for evaluating a polynomial
这里写图片描述
given the coefficients a0, a1,…..,an and a value for x:
这里写图片描述
a.
In terms of ‚θ-notation, what is the running time of this code fragment for Horner’s rule?

b.
Write pseudocode to implement the naive polynomial-evaluation algorithm that
computes each term of the polynomial from scratch. What is the running time
of this algorithm? How does it compare to Horner’s rule?

c.
Consider the following loop invariant:
At the start of each iteration of the for loop of lines 2–3,
这里写图片描述
Interpret a summation with no terms as equaling 0. Following the structure of the loop invariant proof presented in this chapter, use this loop invariant to show that, at termination,这里写图片描述

d.
Conclude by arguing that the given code fragment correctly evaluates a polynomial characterized by the coefficients a0,a1,…,an.

答:
a.
只有一个循环,故为θ(n)。
b.

#!/usr/bin/env python
# -*- coding: utf8 -*-
"""
brief:算法导论的思考题2-3.b
author:[email protected]
"""
import doctest

def polynomial_evaluation(a,x):
    """
    朴素地多项式 求值算法
    1*1 + 5 * 2 + 3 * 4 + 2 * 8 = 1 + 10 + 12 + 16 = 23 + 16 = 39
    Example
    -------
    >>> polynomial_evaluation('1 5 3 2',2)
    39
    """

    a=a.split()
    _sum = 0
    last_x_i = 1 #x的i方
    for i in xrange(0,len(a)):
        if i != 0:
            x_i = x * last_x_i
        else:
            x_i = last_x_i
        _sum = _sum + int(a[i]) * x_i
        last_x_i = x_i
    return _sum

def main():
    doctest.testmod()


if __name__ == '__main__' :
    main()

很容易看出也是θ(n),但是很显然朴素的求值的常量因子会大一些。这是因为,除了第一次循环以外,必定会有的赋值:

x_i = x * last_x_i
和
_sum = _sum + int(a[i]) * x_i

显然比书中给的霍纳规则的伪代码的常量因子大。所以朴素的算法更性能更差。
c.
已经给出了循环不定式,下面是证明:
初始化:初始时,i = n,此时,n - (n+1) = -1 ,而k的初始化为0,那么0 到-1,就没有。而此时伪代码中的y确实为0,所以初始化成立(比较勉强)。

保持:当i = n -1 时,n - (n - 1 + 1 ) = 0

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

可知,y = a[n]。检查伪代码,当i = n-1时,已经完成了i = n的循环。所以执行了一次 y = a[i] + x * y,推算一下即可得:y = a[n] 。此时循环不定式成立。
终止:何时终止?当 n < 0 终止,此时i = -1。那么循环不定式中的式子有:

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

。检查伪代码,可知 当 i = -1 时,已经完成了当i=0的循环,当i=0时的循环:y = a[0] + x * y 。式子中的a[0],恰好是循环不定式展开的第一项。因为y是一层一层的保留下,推理可以,此时的y=a[1]x^1 + a[2] x^2 + … + a[n]*x^n。故可以知道此时循环不定式中的式子成立。
并且可以知道,当i = -1 终止时,有

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

d.
根据循环不定式,在终止时证明:

    n
y = Σ a[k+i+1] x^k
    k=0   

可其霍纳规则确实能够正确计算出多项式的值。

2-4 Inversions

Let A[1…n] be an array of n distinct numbers. If i < j and A[i] > A[j] , then the pair (i, j) is called an inversion of A.
a. List the five inversions of the array {2,3,8,6,1}.
b. What array with elements from the set {1,2,…,n} has the most inversions? How many does it have?
c. What is the relationship between the running time of insertion sort and the number of inversions in the input array? Justify your answer.
d. Give an algorithm that determines the number of inversions in any permutation on n elements in θ(n lgn) worst-case time. (Hint: Modify merge sort.)
答:
a.
对于:{2,3,8,6,1}
其逆序对:(2,1),(3,1),(8,1),(6,1),(8,6)
b.
很显然当数组逆序的排序的时候,逆序对最多。
比如:{8,6,3,2,1}。

  • 对于8而已,有4个和8组合的逆序对:(8,6),(8,3),(8,2),(8,1)
  • 对于6而已,有3个(6,3),(6,2),(6,1)
  • 对于3而已,有2个(3,2),(2,1)
  • 对于2而已,有1个(2,1)

显然可以知道这个数量是

(n-1)
 Σi 
i=1

c.
插入算法的运行时间和逆序对是线性关系。
可以结合例子和代码来分析:
比如:{2,3,8,6,1}

算法导论(三版):第二章思考题_第5张图片

可以看出来,伪代码中除了6,7句以外,都是肯定会一定会执行的。什么时候第6,7句执行呢?当A[i]>key时,注意key = A[j]。此时,即是一个逆序对:(A[i],key)。所以逆序对越多,执行6,7行的次数越多。故插入算法的运行时间和逆序对是线性关系。

d.
因为提示了用归并排序,所以再模拟归并的过程{2,3,8,6,1} 。并且我发现只需要一丢丢的改变即可计算出逆序对。

         -----------
        | 1,2,3,6,8 |
         -----------
         /         \                  逆序对+2 *** (3)
    -----           -------
   | 2,3 |         | 1,6,8 |
    -----           -------
  /       \        /       \          逆序对+2 *** (2) 
 ---     ---      ---      -----
| 2 |   | 3 |    | 8 |    | 1,6 |
 ---     ---      ---      -----
                  /       /      \    逆序对:+1 *** (1)
                 ---     ---     ---
                | 8 |   | 6 |   | 1 |
                 ---     ---     ---

如上图所示:总共加了5次。逆序对后面的***(1) 只是一个为了方便解析的序号。L数组和R数组分别指合并时左边的数组和右边的数组。
(1). 当R数组的1首先被放入合并后的数组时,L数组中有一个元素。故逆序对+1
(2). 当R数组的1首先被放入合并后的数组时,L数组中有一个元素。故逆序对+1。再将R数组的6放入合并后的数组时,L数组中有一个元素。逆序对再+1。总共+2
(3). 当R数组的1首先被放入合并后的数组时,L数组中有两个元素。故逆序对+2。
算法过程叙述完成。下面代码如下:

错误思路:
1. 归并算法排序整个数组,得到排序数组
2. 将原数组和排序数组比较:
{2,3,8,6,1}
{1,2,3,6,8}
3. 遍历排序数组,插入其在原数组的位置。再用元素在原数组的下标减去排序数组的下标,如果差值大于0,则计为逆序个数。
比如1,原数组下标4 减去 排序数组的下标0,得到差值4。
比如2,原数组下标0 减去 排序数组的下标1,得到差值-1(不计为逆序个数)
但是计算完之后发现错误。
那么这个思路是错误的。因为6在原数组和排序数组的下标没变。以后一定要严格证明算法啊。差点就上当了。

你可能感兴趣的:(算法)