Introduction to Algorithms 算法导论 第2章 算法入门 学习笔记及习题解答

2.1 插入排序

插入排序解决的问题:
    输入:n个数构成的序列
    输出:排序输入序列,满足a1' ≤ a2' ≤ ... ≤ an'

伪码:
    INSERTION-SORT(A)
      for j <- 2 to length[A]
        do key <- A[j]
            i <- j - 1
            while i > 0 and A[i] > key
                do A[i+1] <- A[i]
                    i <- i - 1
            A[i+1] = key

C代码:(C的数组下标从0开始,而伪码中从1开始)
void insertion_sort(int *arr, size_t size)
{
    int i, j, key;

    assert(NULL != arr);

    for (j = 1; j < size; ++j) {
        key = arr[j];

        for (i = j - 1; i >= 0 && arr[i] > key; --i)
            arr[i+1] = arr[i];

        arr[i+1] = key;
    }
}

正确性分析:

    说明:
        插入算法在执行循环之前,A[1,j-1]是已排好序的,而每次循环都保持这个情况,因此当j从2到length[A]循环完成后,整个数组已排序。
    初始:
        j=2,A[1,j-1]只包含一个元素A[1],因此是已排序的;
    保持:
        对于任意j(2 ≤ j ≤ length[A]),保存A[j]到key
        内部循环总是从A[j-1], A[j-2], ..., A[1]序列中依次查找所有大于A[j]的值并依次后移。
        当循环结束时,A[i+1,j]中存放所有大于key的值,而A[1,i-1]中存放所有不大于key的值,且A[i]是空闲的。
        存放key到A[i]。
        因此每次循环后,A[1,j]是有序的。
    退出:
        j > length[A]时循环结束,此时A[1,length[A]]全部有序。

数学归纳法证明:
    当j=2时,A[1,j-1]只包含一个元素A[1],因此肯定是有序的。
    假定对于任意j=m(2 ≤ m < length[A]),循环完成后有A[1,j]有序
    则当j=m+1时:
        A[1,m]肯定有序。
        保存A[m+1]到key。
        内层循环结束时,A[1,i]子数组中所有元素不大于key,而A[i+2,m+1]子数组中所有元素大于key,A[i+1]不持有有效数据。
        保存key到A[i+1]。
        综上可知:A[1,m+1]此时是有序的。

    因此对于所有的j(2 ≤ j ≤ length[A]),在任意时刻,有A[1,j]有序。
    当循环迭代到j=length[A]算法结束时,数组A已排序。

测试程序:
    可以通过如下程序测试插入排序算法的正确性。其中辅助函数print_arr、init_arr可能在以后多次用到:
#include 
#include 
#include 
#include 

void insertion_sort(int *arr, size_t size)
{
	int key;
	int i, j;

	assert(NULL != arr);

	for (j = 1; j < size; ++j) {
		key = arr[j];
		for (i = j - 1; i >= 0 && arr[i] > key; --i)
			arr[i+1] = arr[i];
		arr[i+1] = key;
	}
}

void print_arr(const int *arr, size_t size, const char *info)
{
	int i;

	assert(NULL != arr);

	printf("%s: ", info);
	for (i = 0; i < size; ++i)
		printf("%d ", arr[i]);
	printf("\n");
}

void init_arr(int *arr, size_t size)
{
	int i;

	assert(NULL != arr);

	srand((unsigned int)time(NULL));

	for (i = 0; i < size; ++i)
		arr[i] = rand()%100;
}

int main()
{
	int arr[10];

	init_arr(arr, 10);
	print_arr(arr, 10, "before");
	insertion_sort(arr, 10);
	print_arr(arr, 10, "after");

	return 0;
}


习题:

2.1-1 以图2-2为模型,说明INSERTION-SORT在数组A=<31,41,59,26,41,58>上的执行过程。
如下:
    31,    41(j), 59,    26,    41,    58
    31(j), 41,    59(j), 26,    41,    58
    26,    31,    41,    59(j), 41,    58
    26,    31,    41,    41,    59(j), 58
    26,    31,    41,    41,    58,    59(j)
标注为x(j)的元素表示此时正在以j执行循环。

2.1-2 重写过程INSERTION-SORT,使之按非升序(而不是按非降序)排序。
只需要更改一行:
更改
    while i > 0 and A[i] > key

    while i > 0 and A[i] < key
即可。

2.1-3 考虑下面的查找问题:
    输入:一列数A=和一个值V。
    输出:下标i,使得V=A[i],或者当V不再A中出现时为NIL。
写出针对这个问题的线性查找的伪代码,它顺序地扫描整个序列以查找V。利用循环不变式证明算法的正确性。确保所给出的循环不变式满足三个必要的性质。
FIND-LINEAR(A, v, i)
    i <- NIL
    j <- 1
    while j ≤ length[A] and A[j] != v
        do j <- j + 1
    if j ≤ length[A]
        then i <- NIL
int find_linear(const int *arr, size_t size, int v)
{
    int i;

    for (i = 0; i < size && arr[i] != v; ++i);

    if (i == size) i = -1;    /* -1 means NIL */

    return (int)i;
}

证明:
    初始:
        i=NIL,初始化为未查找到。

    保持:
        从j=1到length[A],依次判断,如果A[j]等于v,则结束循环。

    退出:
        循环结束时,判断j是否有效
        如果j在有效范围(小于等于length[A]),则证明查找成功,因此设置i为正确的索引。
        否则,循环因为越界结束,v没有找到,i依然为NIL。

2.1-4 有两个各存放在数组A和B中的n位二进制整数,考虑它们的相加问题。两个整数的和以二进制形式存放在具有(n+1)个元素的数组C中。请给出这个问题的形式化描述,并写出伪代码。

存储说明:最高位存在数组的第一个元素中,最低位存在数组的最后一个元素中。
    因为A/B各有n个元素而C有n+1个元素,因此对于任意i(1 ≤ i ≤ n),A[i]、B[i]和C[i+1]总在相同位(权数)。如下:
    位 (n+1)   (n)  (n-1)  ...   (2)     (1)
    A         A[0]  A[1]   ...  A[n-1]  A[n]
    B         B[0]  B[1]   ...  B[n-1]  B[n]
    C  C[0]   C[1]  C[2]   ...  C[n]    C[n+1]
伪码:
BINARY-SUM(A, B, C, n)
    for i <- 1 to n+1
        do C[i] <- 0

    for i <- n to 1
        do if A[i] + B[i] + C[i+1] >= 2
                then C[i] <- 1
            C[i+1] <- (C[i+1] + A[i] + B[i])%2

C代码:
void binary_sum(const int *arr_a, const int *arr_b, int *arr_c, size_t n)
{
    int i;

    assert(NULL != arr_a && NULL != arr_b && NULL != arr_c);

    for (i = 0; i < n+1; ++i)
        arr_c[i] = 0;

    if (0 == n) return;

    for (i = n-1; i >= 0; --i) {
        if (arr_a[i] + arr_b[i] + arr_c[i+1] >= 2)
            arr_c[i] = 1;

        arr_c[i+1] = (arr_a[i] + arr_b[i] + arr_c[i+1])%2;
    }
}

形式化描述:
    初始C数组为0。从n到1反向遍历(因为数组第一个元素存储最高为、最后一个元素存储最低位),每次都考虑加法是否需要进位,并设置该位的结果。当循环结束时,C中保存有正确的加法结果。
    如果最终的结果依然为n位(最高为没有进位),则C[1]依然为0,对结果没有影响。

证明:
    初始:
        C中各元素为0,加法尚未开始,因此肯定正确。
    保持:
        对于每一位,当A[i]+b[i]+C[i+1]>=2时,表示需要进位,而此时C[i]肯定为0,因此直接设置为1即可。
        对于C[i+1],如果上次运算有进位,则C[i+1]初始值为1,因此需要考虑A[i]+b[i]+C[i+1]三个位置的值,运算结果为(C[i+1] + A[i] + B[i])%2
    退出:

        循环结束时,A和B中的所有位已经处理过了,如果最高为有进位,则存储在C[1]中,否则C[1]保持为0,因此结果是正确的。


2.2 算法分析
算法分析:
    预测算法运行所需资源。
    资源通常是指算法运行时间。
    实际中需要考虑的其他资源包括:内存、通信带宽、硬件等。

实现模型:
    通用处理器;
    随机存取RAM;
    均等的指令成本,例如当k较小时(小于等于处理器位宽),2^k可认为在常数时间内完成(移位操作)。

插入排序分析:
    T(n) = Θ(n^2)

最坏/好、平均/期望算法运行时间:
    最坏/好:算法上/下界(阈值)
    平均/期望:随机化分析结果

增长指数:
    只统计增长最快项,因为当n增大时,增长最快项是关键。
    实际项目中,还需要考虑输入规模和实现的复杂度、可维护性等。
    衡量方法Θ

习题:
2.2-1 用Θ形式表示函数n^3/1000-100n^2-100n+3。
Θ(n^3)

2.2-2 考虑对数组A中的n个数进行排序的问题:首先找出A中的最小元素,并将其与A[1]中元素进行交换。接着,找出A中的次最小元素,并将其与A[2]中的元素进行交换。对A中头n-1个元素继续这一过程。写出这个算法的伪代码,该算法称为选择排序(selection)。对这个算法来说,循环不变式是什么?为什么它仅需要在头n-1个元素上运行,而不是在所有n个元素上运行?以Θ形式写出选择排序的最佳和最坏情况下的运行时间。

伪码:
SELECTION-SORT(A)
    for i <- 1 to length[A]-1
        do min <- i
            for j <- i+1 to length[A]
                do if A[j] < A[min]
                    then min <-j
            exchange(A[i], A[min])
C代码:
void selection_sort(int *arr, size_t size)
{
    int i, j, min;

    assert(NULL != arr);

    for (i = 0; i < size; ++i) {
        min = i;
        for (j = i + 1; j < size; ++j) {
            if (arr[j] < arr[min])
                min = j;
        }
        /* exchanges */
        if (min != i) {
            arr[i] = arr[i]^arr[min];
            arr[min] = arr[min]^arr[i];
            arr[i] = arr[i]^arr[min];
        }
    }
}
循环不变式:
    初始:
        i=1,A[1,i]只包含一个元素即A[1],因此A[1,i]有序。
    保持:
        每次i迭加时,总是查找A[i, length[A]]中的最小元素,然后和A[i]交换,因此循环后,A[i]大于等于A[1,i-1]中的任何元素,且小于A[i+1,length[A]]中的任何元素。
        因此迭代完成后,A[1,i]依然是有序的。
    退出:
        当i=length[A]-1时退出。此时A[1,i]所有元素均小于A[length[A]]且有序,因此数组A已排序。

对于选择排序,当前n-1个元素已排序时,剩下的一个元素一定是最大的一个,因此不再需要循环判断。

最佳和最坏运行时间相等,都是Θ(n^2)

2.2-3 再次考察线性查找问题(见练习2.1-3)。在平均情况下,需要检查输入序列中的多少个元素?假定待查找的元素是数组中任何一个元素的可能性是相等的。在最坏情况下又是怎么样?用Θ形式表示的话,线性查找的平均情况和最坏情况运行时间怎样?对你的答案加以说明。

平均需要查找n/2个元素;
最坏需要查找n个元素;
Θ形式的平均和最坏相同,都是Θ(n)。

2.2-4 应如何修该任何一个算法,才能使之具有较好的最佳运行时间?
针对最大概率的输入定制优化一个算法,可获得最佳运行时间。
比如在输入规模很小时,首要考虑算法的常数系数。输入规模很大时,则优先考虑最高项(即Θ结果)。

2.3 算法设计

2.3.1 分治法(Divede-and-Conquer)
递归的把原问题划分成n个较小规模的结构与原问题相似的子问题,递归地解决这些子问题,然后合并结果,就得到原问题的解。
分治模式在每一层上都有三个步骤:分解、解决、合并。

合并排序完全按照上述模式操作,如下:
分解:将n个元素分成各含n/2个问题的子序列。
解决:用合并排序法对子序列递归地排序。
合并:合并两个已排序的子序列以得到排序结果。

在对子序列排序时,其长度为1时认为是已排序的,递归结束。
合并过程如下:
A为待排序数组,n=r-p+1是需要合并的子序列。用∞作为哨兵,它大于任意元素。

伪码:
MERGE(A, p, q, r)
    n1 <- q - p +1
    n2 <- r - q

    create arrays L[1..n1+1] and R[1..n2+1]

    for j <- 1 to n1
        do L[i] <- A[p+i-1]
    for j <- 1 to n2
        do R[j] <- A[q+j]
    L[n1+1] <- ∞
    R[n2+1] <- ∞

    i <- 1
    j <- 1
    for k <- p to r
        do if L[i] ≤ R[j]
            then A[k] <- L[i]
                i <- i + 1
            else A[j] <- R[j]
                j <- j + 1
MERGE-SORT(A, p, r)
    if p < r
        then q <- ⌊(p+r)/2⌋
            MERGE-SORT(A, p, q)
            MERGE-SORT(A, a+1, r)
            MERGE(A, p, q, r)
C代码:
int merge(int *arr, size_t p, size_t q, size_t r)
{
    int i, j, k, n1 = q - p + 1, n2 = r - q, *arr_l, *arr_r;

    assert(NULL != arr);
    assert(p <= q && q < r);

    arr_l = malloc(sizeof(int)*n1);
    arr_r = malloc(sizeof(int)*n2);
    if (NULL == arr_l || NULL == arr_r) {
        /* to free a null pointer is a safe operation */
        free(arr_l);
        free(arr_r);
        return -ENOMEM;
    }

    /* we don't use dummy node but to check l and r sub-array */
    for (i = 0; i < n1; ++i)
        arr_l[i] = arr[p+i];
    for (j = 0; j < n2; ++j)
        arr_r[j] = arr[q+j+1];

    for (i = 0, j = 0, k = p; i < n1 && j < n2; ++k) {
        if (arr_l[i] < arr_r[j]) {
            arr[k] = arr_l[i];
            ++i;
        } else {
            arr[k] = arr_r[j];
            ++j;
        }
    }

    /* append unused nodes */
    while (i < n1) arr[k++] = arr_l[i++];
    while (j < n2) arr[k++] = arr_r[j++];

    free(arr_l);
    free(arr_r);

    return 0;
}

int merge_sort(int *arr, size_t p, size_t r)
{
    if (p < r) {
        size_t q = (p+r)/2;

        if (0 == merge_sort(arr, p ,q) && 0 == merge_sort(arr, q+1, r))
            return merge(arr, p, q, r);

        return -ENOMEM;
    }

    return 0;
}

C代码需要优化的位置:
C函数merge_sort执行成功时返回0,失败返回-ENOMEM。为了提高程序效率,可以由外部提供辅助空间,这样merge_sort总是成功。更改以后的接口可能是:
    void merge_sort(int *arr, size_t p, size_t r, int *arr_helper);
    void merge(int *arr, size_t p, size_t q, size_t r, int *arr_helper);
其中arr_helper辅助数组最小需要和arr同等大小。
在merge函数内部,设置arr_l=arr_helper, arr_r=arr_helper+n1即可。

循环不变式:
    初始化:
        k=p,因此A[p, k-1]是空的,包含0个元素。
        又i=j=1,L[i]和R[j]都在各自的数组中,尚未复制到A。
    保持:
        每次复制时,选择L[i] ≤ R[j],则L[i]是L和R中尚未复制的最小元素,复制它到A[k],然后递增k和i,因此A[p,k]中包含最小的k-p+1个元素且有序。
        当L中元素全部复制完时,遇到哨兵元素∞,而它比R[j]中任何哨兵以外的元素斗大,因此此后总是复制R[j]的元素并递增j。
        当L[i] > R[j]时情况类似。
    终止:
        此时k=r-p+1,子数组A[p..k-1]已排序。L和R中的所有元素(共n1+n2=r-p+1个)全部拷贝到了A[p,k-1]中。

2.3.2 分治法分析
记分解(Divide)时间为D(n),合并(Conquer)为C(n),则:
    n = 1:
        T(n) = Θ(1)
    n > 1:
        T(n) = aT(n/b) + D(n) + C(n)

合并排序算法分析:
    分解:仅仅需要计算q=(r-p)/2,因此 D(n) = Θ(1)
    解决:递归分解为n/2个元素的子数组,因此时间为2T(n/2)。
    合并:MERGE的时间复杂度为Θ(n),因此 C(n) = Θ(n)

    因此有:
        n = 1:
            T(n) = Θ(1)
        n > 1:
            T(n) = 2T(n/2) + Θ(n)

    第四章介绍的主定理对上式做了详细证明。其结果是:
        T(n) = Θ(nlgn) (lgn表示log以2为底的n)

    因为nlgn的增长速度比n^2要慢很多,因此当输入规模足够大时,合并排序比插入排序的性能要好。

-----------------------------

练习

2.3-1 以图2-4为模型,说明合并排序在输入数组A=〈3,41,52,26,38,57,9,49〉上的执行过程。

 ^                                                ^
 |         3  9  26  38  49  51  52  57           |
 |           /                      \             |
 |     3  26  51  52           9  38  49  57      |
 |     /          \            /          \       |
 |  3    41    26    52    38    57     9    49   |
 |  /    \      /    \      /    \      /    \    |
 | 3     41    52    26    38    57    9     49   |

2.3-2 改写MERGE过程,使之不使用哨兵元素,而是在一旦数组L或R中的所有元素都被复制回数组A后,就立即停止,再将另一个数组中余下的元素复制回数组A中。
MERGEA(A, p, q, r)
    n1 <- q - p +1
    n2 <- r - q

    create arrays L[1..n1] and R[1..n2]

    for j <- 1 to n1
        do L[i] <- A[p+i-1]
    for j <- 1 to n2
        do R[j] <- A[q+j]

    i <- 1
    j <- 1
    k <- p
    while i ≤ n1 and j ≤ n2
        do if L[i] ≤ R[j]
            then A[k] <- L[i]
                i <- i + 1
            else A[j] <- R[j]
                j <- j + 1
        k <- k + 1

    if i ≤ n1
        then while i ≤ n1
            do A[k] <- L[i]
                k <- k + 1
                i <- i + 1
        else while j ≤ n2
            do A[k] <- R[j]
                k <- k + 1
                j <- j + 1

2.3-3 利用数学归纳法证明:当n是2的整数次幂时,递归式

                |-- 2   n=2时
    T(n) = |
                |-- 2T(n/2) + n 如果n=2^k, k>1
的解为 T(n) = nlgn
证明:
    n = 2时,
        T(n) = nlgn = 2lg2 = 2 成立。

    假定当n=2^k时成立,有:
        T(n) = 2^klg(2^k) = (2^k) * k

    则当n=2^(k+1)时,有:
        T(n) = 2T(n/2) + n
            = 2T(2^(k+1)/2) + 2^(k+1)
            = 2T(2^k) + 2^(k+1)
            = 2 * (2^k) * k + 2^(k+1)
            = (2^(k+1)) * k + 2^(k+1)
            = (2^(k+1)) * (k+1)
            = (2^(k+1)) * lg(2^(k+1))
            = nlgn

    因此,对于所有的n=2^k, k>=1,递归式均成立。

    证毕。

2.3-4 插入排序可以如下改写成一个递归过程:为排序A[1..n],先递归地排序A[1..n-1],然后再将A[n]插入到已排序的数组A[1..n-1]中去。对于插入排序的这一递归版本,为它的运行时间写一个递归式。
INSERTION-SORT-RECURSIVE(A, n, k)
    if k > 1
        then INSERTION-SORT-RECURSIVE(A, n, k-1)

    key <- A[k]
    for i <- k-1 to 1
        do if A[i] > key
            then A[i+1] <- A[i]
        A[i+1] <- key

2.3-5 回顾一下练习2.1-3中提出的查找问题,注意如果序列A是已排序的,就可以将该序列的中点与v进行比较。根据比较的结果,原序列中有一半就可以不用再做进一步的考虑了。二分查找(binary search)就是一个不断重复这一查找过程的算法,它每次都将序列余下的部分分成两半,并只对其中的一半做进一步的查找。写出二分查找算法的伪代码,可以是迭代的,也可以是递归的。说明二分查找算法的最坏情况运行时间为什么是Θ(lgn)。
迭代版本:

BINARY-SEARCH-ITERATE(A, v)
    low <- 1
    high <- length[A]
    while low ≤ high
        do mid <- ⌊(low + high)/2⌋
            if A[mid] = v
                then return mid
                elif A[mid] < v
                    then low <- mid + 1
                    else high <- mid - 1
    return NIL

注意,当查找到时,设置low=high+1,因此下次循环检测时循环结束,并且idx已被设置为正确值。
递归版本:

BINARY-SEARCH-RECURSIVE(A, v, low, high)
    if low < high
        then return NIL

    mid <- ⌊(low + high)/2⌋

    if A[mid] = v
        then return mid
        elif A[mid] < v
            then return BINARY-SEARCH-RECURSIVE(A, v, mid+1, high)
            else return BINARY-SEARCH-RECURSIVE(A, v, low, mid-1)
可知,二分查找时间复杂度满足
    T(n) = 2T(n/2) + Θ(1)
因此
    T(n) = Θ(lgn)

2.3-6 观察一下2.1节中给出的INSERTION-SORT过程,在第5~7行的while循环中,采用了一种线性查找策略,在已排序的子数组A[1..j- 1]中(反向)扫描。是否可以改用二分查找策略(见练习2.3.5),来将插入排序的总体最坏情况运行时间改善至Θ(nlgn)?

完成了一定程度上的优化,但依然不满足Θ(nlgn),因为需要移动元素的平均个数依然是n/2,因此还是Θ(n^2)。

*2.3-7 请给出一个运行时间为Θ(nlgn)的算法,使之能在给定一个由n个整数构成的集合S和另一个整数x时,判断出S中是否存在有两个其和等于x的元素。
考虑到目前只学习了合并排序和二分查找,因此用这两种方法实现解决该问题,如下:
SEARCH-SUM(S, x)
    MERGE-SORT(S, 0, length[S])
    for i <- 0 to length[S]
        do v <- x - S[i]
            if BINARY-SEARCH-ITERATE(S, v) != NIL
                then return true

    return false
分析:
MERGE-SORT时间复杂度为Θ(nlgn),循环执行n次,每次BINARY-SEARCH-ITERATE的时间复杂度为Θ(lgn),所以循环体的时间复杂度是Θ(nlgn)。
因此,SEARCH-SUM的时间复杂度为Θ(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的值应该如何选取?
解答:
a) 每个子列表长度为k,因此每个子列表插入排序的时间为Θ(k^2)。共有n/k个子列表,故总的时间为Θ(k^2)*(n/k) = Θ(nk)
b) 此时共需合并n/k次(因为分解了n/k次),每次合并的时间为Θ(n),故所有合并的时间为Θ(nlg(n/k))
c) 问题等价于求解 Θ(nk + nlg(n/k)) = Θ(nlgn)。其最大渐近值为lgn。
d) 这是个实验问题,应该在k的合法范围内测试可能的k,用T-INSERTION-SORT(k)表示k个元素的插入排序时间,T-MERGE-SORT(k)表示k个元素的合并排序时间。该问题等价于测试求解T-INSERTION-SORT(k)/T-MERGE-SORT(k)比值最小的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′表示BUBBLESORT(A)的输出。为了证明BUBBLESORT是正确的,需要证明它能够终止,并且有:
    A'[1] ≤ A'[2] ≤ ... ≤ A'[n]                     (2.3)
   其中n=length[A]。为了证明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 霍纳规则的正确性
以下的代码片段实现了用于计算多项式
    P(x) = a0 + x(a1 + x(a2 + ... + x(an-1 + xan)...))
的霍纳规则(Horner's rule)。
给定系数a0, a1, ..., an-1, an以及x的值,有:

    y <- 0
    i <- n
    while i ≥ 0
        do y <- ai + x*y
            i <- i - 1
a)这一段实现霍纳规则的代码的渐近运行时间是什么?
b)写出伪代码以实现朴素多项式求值(naive polynomial-evaluation)算法,它从头开始计算多项式的每一个项。这个算法的运行时间是多少?它与实现霍纳规则的代码段的运行时间相比怎样?
c)证明以下给出的是针对第3~5行中while循环的一个循环不变式:
在第3~5行中while循环每一轮迭代的开始,有:
    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) 朴素多项式求值:
NATIVE-POLYNOMIAL-EVALUATION(A, x)
    y <- 0

    for i <- 0 to length[A]
        v <- 1
        do for j <- 1 to i
            do v <- v*x
            y <- y + A[i]*v
    return y
算法运行时间为Θ(n^2)。而霍纳规则代码段时间复杂度为Θ(n)。因此,该算法的时间耗费是霍纳规则代码段的Θ(n)倍。

c)
由于0从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个不同数的数组。如果iA[j],则(i,j)就称为A中的一个逆序对(inversion)。
a)列出数组〈2,3,8,6,1〉的5个逆序。
b)如果数组的元素取自集合{1, 2, ..., n},那么,怎样的数组含有最多的逆序对?它包含多少个逆序对?
c)插入排序的运行时间与输入数组中逆序对的数量之间有怎样的关系?说明你的理由。
d)给出一个算法,它能用Θ(nlgn)的最坏情况运行时间,确定n个元素的任何排列中逆序对的数目。(提示:修改合并排序)
解答:
a)
2,1
2,1
8,6
8,1
6,1
b)
数组从大到小有序排列时,逆序对最多,为n(n-1)/2个。
c)
逆序对增加时,插入排序时间增加。
没有逆序对时,插入排序时间最少,为Θ(n)。
逆序对最多时,插入排序时间最多,为Θ(n^2)。
d)
初始化count为0,对数组执行合并排序,在合并(MERGE)操作中,只要i

你可能感兴趣的:(Algorithms)