Solution to CLRS Chapter 2

来源:互联网 发布:expect详解sql 编辑:程序博客网 时间:2024/06/05 19:34

2.1-1
j=2 : 31,41,59,26,41,58 (31 > 41 false)
j=3 : 31,41,59,26,41,58 (41 > 59 false)
j=4 : 26,31,41,59,41,58 (i = 0)
j=5 : 26,31,41,41,59,58 (41 > 41 false)
j=6 : 26,31,41,41,58,59 (41 > 58 false)

2.1-2

void D-INSERTION-SORT        for j = 2 : N                key = A[j];                i = j - 1;                while i > 0 && A[i] < key                        A[i + 1] = A[i];                        i = i - 1                end                A[i + 1] = key

2.1-3

LINEAR-SSEARCH (A, key)        for i = 1 : A.length                if A[i] == key                        return i;        return NIL

Loop invariant
Initialization:
i = 1 之前,数组为空,不可能找到,故而没有对invariant构成violation

Maintenance:
每次开始循环前,之前的i - 1个数一定是不存在和key相等的情况(否则一定已经返回),那么和key相等的元素,只能是在剩下的n - i + 1个元素中,这个invariant每次都成立。

Termination:
如果找到了和key相等的元素,那么就正确返回,如果循环结束后依旧未能找到,那么key一定不存在于A[]中,此时程序返回NIL,正确

2.1-4
Statement: n-bit binary addition
Input: 2 sequences of n-bit binary number
Output: the sum sequence of a + b

Pseudocode:

N-BIT-BINARY-ADDITION (A, B)        N = A.length        new Array C[N+1];        carry = 0 //store the carry number        for i = 1 : N                C[i] = (A[i] + B[i] + carry) % 2                if A[i] + B[i] + carry >= 2                        carry = 1;                else                        carry = 0        C[N + 1] = carry        return C

2.2-1

2.2-2

SELECTION-SORT(A)        for i = 1 : A.length - 1                        k = i    //after the j-loop, A[k] will be the smallest in A[i...A.length]                for j = i + 1 : A.length - 1                        if A[k] > A[j]                             k = j                swap(A[i], A[k])

Loop invariant: 开始第i次循环前,A[1…i-1]必然是有序的

找N-1次最小之后,剩下的第N个元素必然是最大值,没有必要继续操作

因为,不论元素如何分布,两个循环执行的次数并不会发生改变,所以SELECTION-SORT的复杂度是稳定的

2.2-3
假设输入时随机的,每个元素出现在各位置是等可能的,那么这个数学期望应该是:

Worst-case 1, best-case n

2.2-3
这题的意思是:有什么通用的方法,可以用来改进所有的算法,使得它们都有一个best-case running time
我一开始没想到,后来借鉴了网上的答案:
将特定的case进行预处理计算,然后对每一次的Input,先检查,这个Input是否符合best-case,如果符合就直接输出答案,如果不符合,那就调用正常的算法过程。

个人认为这题是挺有价值的。很多算法的performance和distribution of Input是密切相关的,这题的思路是优化best-case performance,是对一种对少数Input进行特判的处理方式,这本书后面的Randomization是将Input的分布进行随机化。
这题提供给我算法设计的一个概念,就是算法设计时,Input分布也是一个可优化的考虑因素。

2.3-1
3,41,52,26 | 38,57,9,49
3,41 | 52,26 | 38,57 | 9,49
3 | 41 | 52 | 26 | 38 | 57 | 9 | 49
3,41 | 26,52 | 38,57 | 9,49
3,26,41,52 | 9,28,49,57
3,9,26,28,41,49,52,57

2.3-2
这个算法的实现不难,但是相对于原先的设置了sentinel的算法,winthout sentinel 在代码形式上,复杂了很多,需要考虑两个数组中,可能有一个没有完全复制的情况。
这题给我的启示,就是设置sentinel这种方法的巧妙,在一些以比较为基础的循环中,可以广泛采用!

2.3-3
很简单的数学归纳法,注意归纳的是power of 2的情况,所以假设了n成立后,下一个成立的应该是2n,直接用给的递归式子代入化简一下就得到结果了。

2.3-4

RECURSIVE-INSERTION-SORT(A, N)        RECURSIVE-INSERTION-SORT(A, N-1)        key = A[N]        i = N - 1        while i > 0 && key < A[i]                A[i + 1] = A[i]                i = i - 1        A[i + 1] = key

2.3-5

BINARY-SEARCH(A, key)        left = 1        right = A.length        while left <= right                mid = (left + right) / 2                if A[mid] == key                        return mid                else if A[mid] > key                        right = mid - 1                else                        left = mid + 1        return right

n个元素,折半k次后,成为1个元素,则这个折半次数为k = log(n),显然这就是worstcase的复杂度,相当可观,不过这是在序列有序的前提下。二分查找可以作为一种优秀的优化,植根于其他的算法中,比如dp等。

2.3-6
这题问的是:二分查找是否能够用来优化insertion-sort使得复杂度下降到nlogn。
一开始我感觉这好像是没有问题的,但是后来发现似乎这是不对的,因为insertion-sort的开销,并不完全的来自于“查找”,还包括“移动元素,腾出位置”同样也是一个O(n)的因子,所以即使用了二分查找,优化了搜索部分的复杂度,移动元素的复杂度依旧是不可以避免的!

★2.3-7
要求给出nlogn的算法来判断:一个序列中是否存在两个序列的和,为给定的数字x
这题由于之前接触过,所以很快就想起来可以做一个转换,用一次O(N)的遍历,对每一个元素A[i],用二分查找看
x-A[i]是否存在于序列中!记得当初对这个算法十分佩服,只可惜已经遇到过这个问题后,思维已成定势,没有能自己想一个优秀的算法。

Problems
2-1 Insertion sort on small arrays in merge sort
关于这个优化的思想内涵,我认为原文说的很完备,这里直接转述:
“Although merge sort runs in theta(nlg n) worst-case time and insertion sort runs in theta(n^2) worst-case time,the constant factors in insertion sort can make it faster in practice for small problem size 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 the merges using the standard merging mechanism,where k is a value to be determined.”

(a)
insertion sort 时间复杂度是 theta(n^2) 的,所以处理规模为k的问题,需要theta(k^2),由于有n/k个这样的问题,复杂度是 n/k*theta(k^2) = theta(nk) proved!

(b)
这题怎么求启示题目描述里面已经有提示了-“coarsen the leaves”。merge sort原来的叶子节点,代表的是序列中的单个元素,现在用了insertion sort对small case进行优化后,叶子节点变成了“长度为k的序列”,形象的理解,就是叶子变粗了,这样,整棵树的高度就减少了,每次将n折半,直到将n变成k,
即,高度为lg(n/k),结合之前对merge sort树每层都是c*n,那么总的复杂度是

(c)
k = lg(n) , if k = f(n) > lg(n), 复杂度则是theta(nf(n))

(d)
只要选择insertion sort比merge sort高效的那个分段点就可以了

2-2 Correctness of bubblesort
冒泡排序,稳定,但效率不高,学C的经典入门题,貌似是我接触的第一个算法!
(a)
这题一开始感觉有点奇怪,对于一个排序算法而言,它要正确,就必须把原来的数组,变成一个单调的数组(或增或减),这一点是显而易见的,但这题问,除此之外,还需要什么,才能说一个排序算法正确?
答案是:Ouput序列,必须由Input序列中的元素构成,具体举个例子,
经过一个排序算法后,0 -1 1变成了 1 2 3,后者确实有序,但是其中的元素根本不是input中给的,所以不是一个正确的排序算法,也就是说,在本质上,排序算法只是一种对元素相对位置的变换,不能改变任何有关元素本身的性质。

(b)
问题:研究内层循环的loop invariant。
Initialization:
第i次开始j循环的时候,前i-1个元素一定是有序的,这个性质是由外层的loop invariant保证的。
Maintenance:
j循环中所作的交换,一定能够讲剩下元素中最小的元素,放在第i的位置,这样,保证在下一次循环的时候,前i个数是有序的
Termination:
中止后,由于上面的性质都没有改变,所以n个数是有序的

(c)
同上

(d)
worst-case running time is
相比insertion sort,它的best-case performance 不如insertion sort,worst-case相当,但是bubblesort中,swap操作是一个比较耗时的操作,所以总体来说,insertion sort更加优秀。

2-3 Correctness of Horner’s rule
貌似这个就是那个秦九韶算法,一种用来求多项式的值的linear算法(传统的暴力法是quadratic的)
这个算法的思想很强大,我记得在另一个地方也见到过,就是求所有区间[i,j]的和的两倍的和,传统的暴力法需要theta(n^2)的复杂度,但是可以通过同样的方式,降低到theta(n):
pre = pre * a[i];
ans += pre * 2 + a[i];
pre += a[i];
(a)

(b)
naive polynomial-evalution 算法,就是把每一个值老老实实的计算出来,所以在计算a^k次方的时候,需要用一个长度为k的循环,这样总的时间复杂度就是n(n+1)/2,,比起Horner’s rule,慢了非常非常多

(c)
这个算法的loop invariant他直接给出了,那么只要验证就可以了,没什么难度

(d)
就是(c)的结论。

2-4 Inversions
求逆序数,这个问题有两种解法,一种就是用改装过的merge sort,另一种是用树状数组。这个问题虽然说不上是很常见,但是有一些问题可以转换成逆序数的问题,比如:有一个二部图,每一个部分有n个元素,每一个元素向对面未连线的元素连一根线,求相交的点数。这个就可以转换成逆序数对的问题。
(a)
5个逆序对:(2,1) (3,1) (8,6) (8,1) (6,1)

(b)
显然 {n, n-1, n-2, … , 3, 2, 1}这个序列的逆序数最多,共n(n-1)/2。因为和k构成逆序,最多有k-1种,所以把k放在n-k+1的位置,是对k这个资源的最大利用而不构成浪费。

(c)
如果存在一对逆序数(A[i], A[j]),那么在insertion sort中,已定会至少移动j-i次,把A[j]放到A[i]的前面,两者de关系应该就是如此。

(d)

int A[MAX_N], L[MAX_N], R[MAX_N];long long count = 0;void merge (int l, int mid, int r) {        int n1 = mid - l + 1;        int n2 = r - mid;        for (int i = 1; i <= n1; ++i)                L[i] = A[l+i-1];        for (int j = 1; j <= n2; ++j)                R[j] = A[mid+j];        L[n1+1] = INF;        R[n2+1] = INF;        for (int i = 1, j = 1, k = l; k <= r; ++k) {                if (L[i] < R[j])                        A[k] = L[i++];                else {                        A[k] = R[j++];                        count += n1 - i + 1;                }        }}void mergeSort (int l, int r) {        if (l < r) {                int mid = (l + r) / 2;                mergeSort(l, mid);                mergeSort(mid+1, r);                merge(l, mid, r);        }}
0 0