2.3-6:观察一下2.1节中给出的INSERTION-SORT过程,在第5~7行的while循环中,采用了一种线性查找策略,在已排序的子数组A[1..j-1]中(反向)扫描。是否可以改为二分查找策略(见练习2.3-5),来将插入排序的总体最坏情况运行时间改善至Θ(nlgn)?
(2.3-6
Observe that the while loop of lines 5–7 of the INSERTION-SORT procedure in
Section 2.1 uses a linear search to scan (backward) through the sorted subarray
A[1..j-1]. Can we use a binary search (see Exercise 2.3-5) instead to improve
the overall worst-case running time of insertion sort to Θ(nlgn))
首先引入一个二分查找策略(与2.3-5的Binary Search略有不同)
BINARY(A,p,r,v)
5 for j←p to r
6 if A[j]> v
7 return j
8 return NIL
然后是二分查找的递归过程
BINARY-SEARCH(A,p,r,v)
10 if p=0 and r=0 and A[0] v
11 return 0
12 if p<r
13
14 if A[q] v
15 BINARY-SEARCH(A,p,q,v)
16 return BINARY(A,p,q,v)
17 else BINARY-SEARCH(A,q+1,r,v)
18 return BINARY(A,q+1,r,v)
10 return NIL
利用了二分查找策略的插入排序:
BINARYINSERTION-SORT(A)
1 for j 2 to length[A]
2 do key A[j]
3 i j-1
4 k BINARY-SEARCH(A,0,i,key)
5 if k!= NIL
6 for s i downto k
7 A[s+1] A[s]
8 A[k] key
此算法的在最坏情况下的运行时间是
该算法的C#实现代码:
private static int BinarySearchForInsertionSort<T>(T[] Input, int p, int r, T v) where T : IComparable<T>
{
int q;
if (p == 0 && r == 0 && Input[0].CompareTo(v)>0)
return 0;
if (p < r)
{
q = (p + r) / 2;
if (Input[q].CompareTo(v) > 0)
{
BinarySearchForInsertionSort(Input, p, q, v);
return BinaryForInsertionSort(Input, p, q, v);
}
else
{
BinarySearchForInsertionSort(Input, q+1, r, v);
return BinaryForInsertionSort(Input, q+1, r, v);
}
}
return -1;
}
private static int BinaryForInsertionSort<T>(T[] Input, int p, int r, T v) where T : IComparable<T>
{
for (int j = p; j <= r; j++)
if (Input[j].CompareTo(v) > 0)
return j;
return -1;
}
public static void BinaryInsertionSort<T>(T[] Input) where T : IComparable<T>
{
T key;
int i, k;
for (int j = 1; j < Input.Length; j++)
{
key = Input[j];
i = j - 1;
k = BinarySearchForInsertionSort(Input, 0, i, key);
if (k != -1)
{
for (int s = i; s>=k ; s--)
Input[s + 1] = Input[s];
Input[k] = key;
}
}
}
*2.3-7:请给出一个运行时间为Θ(nlgn)的算法,使之能在给定一个由n个整数构成的集合 和另一个整数 时,判断出 中是否存在有两个其和等于 的元素。
(2.3-7 ?
Describe a ‚.n lg n/-time algorithm that, given a set S of n integers and another integer x, determines whether or not there exist two elements in S whose sum is exactly x.)
利用2.3-5中的BINARY-SEARCH(A,v)和2.3-6中的BINARYINSERTION-SORT(S)算法
ISEXISTSUM(S,x)
1 BINARYINSERTION-SORT(S)
2 for j←)1 to n
3 k BINARY-SEARCH(S,x-S[j])
4 if k!=NIL
5 return TRUE
6 else return FALSE
该算法的运行时间为: Θ(nlgn)
思考题
2-1:在合并排序中对小数组采用插入排序
尽管合并排序的最坏情况运行时间为Θ(nlgn),插入排序的最坏情况运行时间为Θ(n^2),但插入排序中的常数因子使得它在n较小时,运行得要更快一些。因此,在合并排序算法中,当子问题足够小时,采用插入排序就比较合适了。考虑对合并排序做这样的修改,即采用插入排序策略,对n/k个长度为k的子列表进行排序,然后,再用标准的合并机制将它们合并起来,此处k是一个特定的值。
a) 证明最坏情况下,n/k个子列表(每一个子列表的长度为k)可以用插入排序在Θ(nk)时间内完成排序。
b) 证明这些子列表可以在Θ(nlg(n/k))最坏情况时间内完成合并。
c) 如果已知修改后的合并排序算法的最坏情况运行时间为Θ(nk+nlg(n/k)),要使修改后的算法具有与标准合并排序算法一样的渐进运行时间,k的最大渐进值(即 形式)是什么(以n的函数形式表示)?
d) 在实践中,k的值应该如何选取?
(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 Θ(nlg(n/k)) worst-case time.
c. Given that the modified algorithm runs inΘ(nk+nlg(n/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^2*n/k)= Θ(nk)
b.每一层代价都是Θ(n),共lg(n/k)+1层,因此相乘得Θ(nlg(n/k))
c.k=lgn
d.在满足插入排序比合并排序更快的情况下,k取最大值。
ANSWER TO 2-1 在合并排序中对小数组采用插入排序
由题意可知,程序的设计如下所示:
[cpp] view plaincopyprint?
1. #include "stdafx.h"
2. #include <string.h>
3. #include <iostream>
4. using namespace std;
5. void Merge(int* arr, int head, int mid, int tail);
6. void InsertSort(int* arr, int k, int head, int tail) // 进行k个大小的数组插入排序
7. { if (head + k - 1< tail) { int mid = (head + tail) / 2;
8. InsertSort(arr, k, head, mid); InsertSort(arr, k, mid + 1, tail);
9. Merge(arr, head, mid, tail); }
10. else { int key, i, j;
11. for (i = head + 1; i <= tail; i++)
12. { key = arr[i];
13. for (j = i - 1; j >= head && key < arr[j]; j--)
14. arr[j+1] = arr[j]; arr[j + 1] = key; } } }
15. void Merge(int* arr, int head, int mid, int tail)
16. {
17. int n1 = mid - head + 1; int n2 = tail - mid;
18. int* arr1 = new int[n1 + 1]; int* arr2 = new int[n2 + 1];
19. int i, j, k;
20. for(int i = 0; i < n1; i++)
21. arr1[i] = arr[head + i];
22. for(int i = 0; i < n2; i++)
23. arr2[i] = arr[mid + 1 + i];
24. arr1[n1] = 1000000000; //哨兵
25. arr2[n2] = arr1[n1];
26. i = j = 0; k = head;
27. while(k <= tail) {
28. if (arr1[i] <= arr2[j]) { arr[k] = arr1[i];
29. i++; k++; }
30. else {
31. arr[k] = arr2[j];
32. j++; k++;
33. } }
34. delete[] arr1; delete[] arr2; }
#include "stdafx.h"
#include <string.h>
#include <iostream>
using namespace std;
void Merge(int* arr, int head, int mid, int tail);
void InsertSort(int* arr, int k, int head, int tail) // 进行k个大小的数组插入排序
{
if (head + k - 1< tail) {
int mid = (head + tail) / 2;
InsertSort(arr, k, head, mid);
InsertSort(arr, k, mid + 1, tail);
Merge(arr, head, mid, tail);
}
else {
int key, i, j;
for (i = head + 1; i <= tail; i++)
{
key = arr[i];
for (j = i - 1; j >= head && key < arr[j]; j--)
arr[j+1] = arr[j];
arr[j + 1] = key;
}
}
}
void Merge(int* arr, int head, int mid, int tail)
{
int n1 = mid - head + 1;
int n2 = tail - mid;
int* arr1 = new int[n1 + 1];
int* arr2 = new int[n2 + 1];
int i, j, k;
for(int i = 0; i < n1; i++)
arr1[i] = arr[head + i];
for(int i = 0; i < n2; i++)
arr2[i] = arr[mid + 1 + i];
arr1[n1] = 1000000000; //哨兵
arr2[n2] = arr1[n1];
i = j = 0;
k = head;
while(k <= tail) {
if (arr1[i] <= arr2[j]) {
arr[k] = arr1[i];
i++;
k++;
}
else {
arr[k] = arr2[j];
j++;
k++;
}
}
delete[] arr1;
delete[] arr2;
}
测试后通过。关键代码是InsertSort函数中的“if (head + k - 1< tail)”,这句决定了可以进行多大范围的插入排序。
那么a).每次插入排序最坏情况下显然为O(n^2),共进行了n/k次插入排序,显然是在O(nk)时间内完成;
b).由于有n/k个子列表,那么共有log(n/k)+1层。每一层的代价是O(n),因此总共的时间复杂度为O(nlog(n/k));
c).标准的合并算法时间复杂度为O(nlog(n)),要使修改后的算法具有与标准合并排序算法一样的渐进运行时间,k的最大渐进值是logn,原来时间复杂度为O(nk + nlog(n/k)),现在变为了O(nlogn + nlog(n/logn)),忽略nlog(n/logn)的影响,这样即为O(nlogn);d).在满足插入排序比合并排序更快的前提下,k取最大值。
2-2:冒泡排序算法的正确性
冒泡排序(bubblesort)算法是一种流行的排序算法,它重复地交换相邻两个反序元素。
BUBBLESORT(A)
1 for i←1 to length[A]
2 do for j←length[A] downto i+1
3 do if A[j]< A[j-1]
4 then exchange A[j]←→ A[j-1]
a) 设A’表示BULLESORT(A)的输出,为了证明BUBBLESORT是正确的,需要证明它能够终止,并且有: A’[1]<=A[2]<=..<=A’[n]
其中n=length[A]。为了证明BUBBLESORT的确能实现排序的效果,还需要证明什么?
下面两个部分将证明不等式(2.3)。
b) 对第2~4行中的for循环,给出一个准确的循环不变式,并证明该循环不变式是成立的。在证明中采用本章中给出的循环不变式证明结构。
c) 利用在b)部分证明的循环不变式的终止条件,为第1~4行中的for循环给出一个循环不变式,它可以用来证明不等式(2.3)。你的证明因采用本章中给出的循环不变式的证明结构。
d) 冒泡排序算法的最坏情况运行时间是什么?比较它与插入排序的运行时间。
(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.
BUBBLESORT(A)
1 for i←1 to length[A]
2 do for j←length[A] downto i+1
3 do if A[j]< A[j-1]
4 then exchange A[j]←→ A[j-1]
a. Let A0 denote the output of BUBBLESORT(A). To prove that BUBBLESORT is
correct, we need to prove that it terminates and that
A’[1]<=A[2]<=..<=A’[n]
where n=length[A]. 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. A’中的元素全部来自于A中变换后的元素。
b.
初始化:j=n,子数组为A[j..n]即A[n..n],此中仅有一个元素因此是已排序的。
保持:如果A[j..n]是已排序的,按计算过程知A[j] A[j+1] … A[n],当插入元素A[j-1]时,如果A[j] A[j-1]则互换A[j]、A[j-1],否则A[j-1]直接插入A[j..n]的最前,因此A[j-1..n]也是已排序的。
终止:j=i时循环结束,此时A[i..n]是已排序的。与外层循环条件一直,所以算法正确。
c.
初始化:i=1时,子数组A[1..i-1]是空的,因此在第一轮迭代前成立。
保持:假设子数组A[1..i-1]已排序,则之中元素是A[1..n]中最小的i-1个元素,按b证明的循环不变式,知插入A[i]元素后的子数组A[1..i]是A[1..n]中最小的i个元素,并且A[1..i]亦是已排序的。
终止:当i=n+1时循环终止,此时已处理的子数组是A[1..n],A[1..n]是已排序的,这个数组就是要排序的数组。因此算法正确。
d.Σ(n-i)+Σ(n-i-1)=Θ(n^2),与插入排序相同
2-3:霍纳规则的正确性
以下的代码片段实现了用于计算多项式
代码片段见PDF
的霍纳规则(Horner’s Rule)。
给定系数a0,a1,…an以及x的值,有
1 y←0
2 i←n
3 while i>=0
4 do y←i+x*y
5 i←i-1
a) 这一段实现霍纳规则的代码的渐进运行时间是什么?
b) 写出伪代码以实现朴素多项式求值(native polynomial-evaluation)算法,它从头开始计算多项式的每一个项。这个算法的运行时间是多少?它与实现霍纳规则的代码段的运行时间相比怎样?
c) 证明一下给出的是针对第3~5行中while循环的一个循环不变式:
在第3~5行中while循环每一轮迭代的开始,有:公式略
不包含任何项的和视为等于0。你的证明应遵循本章中给出的循环不变式的证明结构,并应证明在终止时,有:公式略(请见PDF)
d) 最后证明以上给出的代码片段能够正确的计算由系数a0,a1,…,an.
()
1. // 霍纳规则求多项式的值
2. // number:数组尾元素所在标号
3. unsigned long horner(word *p, word number,word x)
4. { unsigned long result = 0;
5. word count = number;
6. while(count != 0)
7. { result = *(p+count) + x * result;
8. count--; }
9. result += *p;
10. return result; }
ANSWER TO 2-3 霍纳规则的正确性
a)霍纳规则的代码渐进运行时间是n?;
b)朴素多项式?伪代码是:
[cpp] view plaincopyprint?
1. void Ploynomial()
2. {
3. int t;
4. sum = a[0];
5. for (i = 1; i < n; i++)
6. {
7. sum += a[i]*x;
8. x *= x;
9. }
10. }
void Ploynomial()
{
int t;
sum = a[0];
for (i = 1; i < n; i++)
{
sum += a[i]*x;
x *= x;
}
}
那么运行时间应该是2n吧?与上述相比是2倍时间长度;
c)起始:因为y初始状态为0,i为n,因此在第一轮迭代的开始时有,这时y正好为0;
保持:利用数学归纳法,若i = k+1时,迭代前为,此时成立;那么对于下一轮迭代,i=k,迭代前有,此时,必然成立。终止:终止时i=-1,那么此时,与原多项式相同,证明成立
2-4:逆序对
设A[1..n]是一个包含n个不同数的数组。如果在i<j的情况下,有A[i]>A[j],则(i,j)就称为A中的一个逆序对(inversion)。
a) 列出数组<2,3,8,6,1>的5个逆序。
b) 如果数组的元素取自集合{1,2,…,n},那么,怎样的数组含有最多的逆序对?它包含多少个逆序对?
c) 插入排序的运行时间与输入数组中逆序对的数量之间有怎样的关系?说明你的理由。
d) 给出一个算法,它能用Θ(lgn)间,确定n个元素的任何排列中逆序对的数目。(提示:修改合并排序)
(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
a. on n elements in Θ(lgn)worst-case time. (Hint: Modify merge sort.)
#include "stdafx.h"
int count = 0;
void Merge(int *arr, int head, int mid, int tail)
{ int n1 = mid - head + 1; int n2 = tail - mid;
int* arr1 = new int[n1 + 1]; int* arr2 = new int[n2 + 1]; int i, j, k;
for (i = 0; i < n1; i++)
arr1[i] = arr[head + i];
for (i = 0; i < n2; i++)
arr2[i] = arr[mid + i + 1]; arr1[n1] = arr2[n2] = 1000000000; i = j = 0; k = head;
while (k <= tail) {
if (arr1[i] < arr2[j]) {
arr[k] = arr1[i]; i++; }
else { arr[k] = arr2[j];
count++; j++; } k++; }
delete [] arr1; delete [] arr2; }
void MergeSort(int* arr, int head, int tail)
{ if (head < tail)
{ int mid = (head + tail) / 2;
MergeSort(arr, head, mid);
MergeSort(arr, mid + 1, tail);
Merge(arr, head, mid, tail); } }
int _tmain(int argc, _TCHAR* argv[])
{ int a[5] = {2,3,8,6,1};
MergeSort(a, 0, 4); printf("%d", count);
return 0;}
a.(2,1),(3,1),(8,6),(8,1),(6,1)
b.{n,n-1,n-2,…,1}有最多的逆序对。共n*(n-1)/2
c.逆序对越多,说明运行情况越坏,所以逆序对的数量与插入排序的运行效率成反比。
d.修改MERGE过程的最后一个FOR循环即可。