第二章:算法基础
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.
这个问题需要参考下面这幅图:
这是标准的归并算法的merge的过程。最后求出标准归并算法的时间复杂度时,是用层高 乘以 每层的代价。
其实本题所求代价也用层高 乘以 每层的代价,只是层高没有那么高,因为当到子数组的规模到了n/k的时候,此时的层高应为log(n/k + 1)。故总代价为:lg(n/k+1) * n ,即为:θ(nlg(n/k))
c.
我解答的过程是首先感受代了几个值感受了一下,
很容易发现关键就是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.
Bubblesort is a popular, but inefficient, sorting algorithm. It works by repeatedly swapping adjacent elements that are out of order.
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 的元素的值小
综上,2-4行的for循环能够为我们提供如上所述的循环不定式。
c.
对于1-4行,循环不定式:
在1-4行每一次循环迭代开始时,数组下标为i-1的元素,即为该数组第 i - 1 小的元素。也就说,第 i - 1 小的元素已经位于其整个数组排序后的最终为:第 i - 1位
书上初始化的定义,必须是第一次for循环赋值,但是没有开始执行for的语句之前。这样就搞得初始化的语句显得很奇怪。但是保持和终止却能表示的非常清楚。我姑且容忍了这个做法。另外,也有可能我没有想出一个正确的循环不定式。
d.
看图说话:
当第四句每次都执行的时候将会出现最坏的情况。根据第三行,发现只要都输入数组是逆序的时候,第四行每次都执行。
下面来分析每句执行的情况:
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
我们会发现插入算法多了(C2+C4+C8)(n-1)的部分。感觉上插入算法的时间复杂度更长。但其实这个问题是未知的,这是因为我们不知道两个公式中Ci的具体大小。
此外,很容易求解冒泡排序的时间复杂度(以θ形式),和书中求解插入算法几乎一样,见中文版P15页。
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
可其霍纳规则确实能够正确计算出多项式的值。
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}。
显然可以知道这个数量是
(n-1)
Σi
i=1
c.
插入算法的运行时间和逆序对是线性关系。
可以结合例子和代码来分析:
比如:{2,3,8,6,1}
可以看出来,伪代码中除了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在原数组和排序数组的下标没变。以后一定要严格证明算法啊。差点就上当了。