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

2.1 插入排序

插入排序解决的问题:
    输入:n个数构成的序列<a1, a2, ..., an>
    输出:排序输入序列<a1, a2, ..., an>为<a1', a2', ..., an'>,满足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 <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

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=<a1,a2,…,an>和一个值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个不同数的数组。如果i<j且A[i]>A[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<j且L[i] < R[j],则递增统计计数count。合并排序完成后count值为逆序对的数目。


  • 4
    点赞
  • 3
    收藏
  • 打赏
    打赏
  • 3
    评论
中文名: 算法导论 原名: Introduction to Algorithms 作者: Thomas H. Cormen Ronald L. Rivest Charles E. Leiserson Clifford Stein 资源格式: PDF 版本: 文字版 出版社: The MIT Press书号: 978-0262033848发行时间: 2009年09月30日 地区: 美国 语言: 英文 简介: 内容介绍: Editorial Reviews Review "In light of the explosive growth in the amount of data and the diversity of computing applications, efficient algorithms are needed now more than ever. This beautifully written, thoughtfully organized book is the definitive introductory book on the design and analysis of algorithms. The first half offers an effective method to teach and study algorithms; the second half then engages more advanced readers and curious students with compelling material on both the possibilities and the challenges in this fascinating field." —Shang-Hua Teng, University of Southern California "Introduction to Algorithms, the 'bible' of the field, is a comprehensive textbook covering the full spectrum of modern algorithms: from the fastest algorithms and data structures to polynomial-time algorithms for seemingly intractable problems, from classical algorithms in graph theory to special algorithms for string matching, computational geometry, and number theory. The revised third edition notably adds a chapter on van Emde Boas trees, one of the most useful data structures, and on multithreaded algorithms, a topic of increasing importance." —Daniel Spielman, Department of Computer Science, Yale University "As an educator and researcher in the field of algorithms for over two decades, I can unequivocally say that the Cormen book is the best textbook that I have ever seen on this subject. It offers an incisive, encyclopedic, and modern treatment of algorithms, and our department will continue to use it for teaching at both the graduate and undergraduate levels, as well as a reliable research reference." —Gabriel Robins, Department of Computer Science, University of Virginia Product Description Some books on algorithms are rigorous but incomplete; others cover masses of material but lack rigor. Introduction to Algorithms uniquely combines rigor and comprehensiveness. The book covers a broad range of algorithms in depth, yet makes their design and analysis accessible to all levels of readers. Each chapter is relatively self-contained and can be used as a unit of study. The algorithms are described in English and in a pseudocode designed to be readable by anyone who has done a little programming. The explanations have been kept elementary without sacrificing depth of coverage or mathematical rigor. The first edition became a widely used text in universities worldwide as well as the standard reference for professionals. The second edition featured new chapters on the role of algorithms, probabilistic analysis and randomized algorithms, and linear programming. The third edition has been revised and updated throughout. It includes two completely new chapters, on van Emde Boas trees and multithreaded algorithms, and substantial additions to the chapter on recurrences (now called "Divide-and-Conquer"). It features improved treatment of dynamic programming and greedy algorithms and a new notion of edge-based flow in the material on flow networks. Many new exercises and problems have been added for this edition. As of the third edition, this textbook is published exclusively by the MIT Press. About the Author Thomas H. Cormen is Professor of Computer Science and former Director of the Institute for Writing and Rhetoric at Dartmouth College. Charles E. Leiserson is Professor of Computer Science and Engineering at the Massachusetts Institute of Technology. Ronald L. Rivest is Andrew and Erna Viterbi Professor of Electrical Engineering and Computer Science at the Massachusetts Institute of Technology. Clifford Stein is Professor of Industrial Engineering and Operations Research at Columbia University. 目录: Introduction 3 1 The Role of Algorithms in Computing 5 1.1 Algorithms 5 1.2 Algorithms as a technology 11 2 Getting Started 16 2.1 Insertion sort 16 2.2 Analyzing algorithms 23 2.3 Designing algorithms 29 3 Growth of Functions 43 3.1 Asymptotic notation 43 3.2 Standard notations and common functions 53 4 Divide-and-Conquer 65 4.1 The maximum-subarray problem 68 4.2 Strassen's algorithm for matrix multiplication 75 4.3 The substitution method for solving recurrences 83 4.4 The recursion-tree method for solving recurrences 88 4.5 The master method for solving recurrences 93 4.6 Proof of the master theorem 97 5 Probabilistic Analysis and Randomized Algorithms 114 5.1 The hiring problem 114 5.2 Indicator random variables 118 5.3 Randomized algorithms 122 5.4 Probabilistic analysis and further uses of indicator random variables 130 II Sorting and Order Statistics Introduction 147 6 Heapsort 151 6.1 Heaps 151 6.2 Maintaining the heap property 154 6.3 Building a heap 156 6.4 The heapsort algorithm 159 6.5 Priority queues 162 7 Quicksort 170 7.1 Description of quicksort 170 7.2 Performance of quicksort 174 7.3 A randomized version of quicksort 179 7.4 Analysis of quicksort 180 8 Sorting in Linear Time 191 8.1 Lower bounds for sorting 191 8.2 Counting sort 194 8.3 Radix sort 197 8.4 Bucket sort 200 9 Medians and Order Statistics 213 9.1 Minimum and maximum 214 9.2 Selection in expected linear time 215 9.3 Selection in worst-case linear time 220 III Data Structures Introduction 229 10 Elementary Data Structures 232 10.1 Stacks and queues 232 10.2 Linked lists 236 10.3 Implementing pointers and objects 241 10.4 Representing rooted trees 246 11 Hash Tables 253 11.1 Direct-address tables 254 11.2 Hash tables 256 11.3 Hash functions 262 11.4 Open addressing 269 11.5 Perfect hashing 277 12 Binary Search Trees 286 12.1 What is a binary search tree? 286 12.2 Querying a binary search tree 289 12.3 Insertion and deletion 294 12.4 Randomly built binary search trees 299 13 Red-Black Trees 308 13.1 Properties of red-black trees 308 13.2 Rotations 312 13.3 Insertion 315 13.4 Deletion 323 14 Augmenting Data Structures 339 14.1 Dynamic order statistics 339 14.2 How to augment a data structure 345 14.3 Interval trees 348 IV Advanced Design and Analysis Techniques Introduction 357 15 Dynamic Programming 359 15.1 Rod cutting 360 15.2 Matrix-chain multiplication 370 15.3 Elements of dynamic programming 378 15.4 Longest common subsequence 390 15.5 Optimal binary search trees 397 16 Greedy Algorithms 414 16.1 An activity-selection problem 415 16.2 Elements of the greedy strategy 423 16.3 Huffman codes 428 16.4 Matroids and greedy methods 437 16.5 A task-scheduling problem as a matroid 443 17 Amortized Analysis 451 17.1 Aggregate analysis 452 17.2 The accounting method 456 17.3 The potential method 459 17.4 Dynamic tables 463 V Advanced Data Structures Introduction 481 18 B-Trees 484 18.1 Definition of B-trees 488 18.2 Basic operations on B-trees 491 18.3 Deleting a key from a B-tree 499 19 Fibonacci Heaps 505 19.1 Structure of Fibonacci heaps 507 19.2 Mergeable-heap operations 510 19.3 Decreasing a key and deleting a node 518 19.4 Bounding the maximum degree 523 20 van Emde Boas Trees 531 20.1 Preliminary approaches 532 20.2 A recursive structure 536 20.3 The van Emde Boas tree 545 21 Data Structures for Disjoint Sets 561 21.1 Disjoint-set operations 561 21.2 Linked-list representation of disjoint sets 564 21.3 Disjoint-set forests 568 21.4 Analysis of union by rank with path compression 573 VI Graph Algorithms Introduction 587 22 Elementary Graph Algorithms 589 22.1 Representations of graphs 589 22.2 Breadth-first search 594 22.3 Depth-first search 603 22.4 Topological sort 612 22.5 Strongly connected components 615 23 Minimum Spanning Trees 624 23.1 Growing a minimum spanning tree 625 23.2 The algorithms of Kruskal and Prim 631 24 Single-Source Shortest Paths 643 24.1 The Bellman-Ford algorithm 651 24.2 Single-source shortest paths in directed acyclic graphs 655 24.3 Dijkstra's algorithm 658 24.4 Difference constraints and shortest paths 664 24.5 Proofs of shortest-paths properties 671 25 All-Pairs Shortest Paths 684 25.1 Shortest paths and matrix multiplication 686 25.2 The Floyd-Warshall algorithm 693 25.3 Johnson's algorithm for sparse graphs 700 26 Maximum Flow 708 26.1 Flow networks 709 26.2 The Ford-Fulkerson method 714 26.3 Maximum bipartite matching 732 26.4 Push-relabel algorithms 736 26.5 The relabel-to-front algorithm 748 VII Selected Topics Introduction 769 27 Multithreaded Algorithms Sample Chapter - Download PDF (317 KB) 772 27.1 The basics of dynamic multithreading 774 27.2 Multithreaded matrix multiplication 792 27.3 Multithreaded merge sort 797 28 Matrix Operations 813 28.1 Solving systems of linear equations 813 28.2 Inverting matrices 827 28.3 Symmetric positive-definite matrices and least-squares approximation 832 29 Linear Programming 843 29.1 Standard and slack forms 850 29.2 Formulating problems as linear programs 859 29.3 The simplex algorithm 864 29.4 Duality 879 29.5 The initial basic feasible solution 886 30 Polynomials and the FFT 898 30.1 Representing polynomials 900 30.2 The DFT and FFT 906 30.3 Efficient FFT implementations 915 31 Number-Theoretic Algorithms 926 31.1 Elementary number-theoretic notions 927 31.2 Greatest common divisor 933 31.3 Modular arithmetic 939 31.4 Solving modular linear equations 946 31.5 The Chinese remainder theorem 950 31.6 Powers of an element 954 31.7 The RSA public-key cryptosystem 958 31.8 Primality testing 965 31.9 Integer factorization 975 32 String Matching 985 32.1 The naive string-matching algorithm 988 32.2 The Rabin-Karp algorithm 990 32.3 String matching with finite automata 995 32.4 The Knuth-Morris-Pratt algorithm 1002 33 Computational Geometry 1014 33.1 Line-segment properties 1015 33.2 Determining whether any pair of segments intersects 1021 33.3 Finding the convex hull 1029 33.4 Finding the closest pair of points 1039 34 NP-Completeness 1048 34.1 Polynomial time 1053 34.2 Polynomial-time verification 1061 34.3 NP-completeness and reducibility 1067 34.4 NP-completeness proofs 1078 34.5 NP-complete problems 1086 35 Approximation Algorithms 1106 35.1 The vertex-cover problem 1108 35.2 The traveling-salesman problem 1111 35.3 The set-covering problem 1117 35.4 Randomization and linear programming 1123 35.5 The subset-sum problem 1128 VIII Appendix: Mathematical Background Introduction 1143 A Summations 1145 A.1 Summation formulas and properties 1145 A.2 Bounding summations 1149 B Sets, Etc. 1158 B.1 Sets 1158 B.2 Relations 1163 B.3 Functions 1166 B.4 Graphs 1168 B.5 Trees 1173 C Counting and Probability 1183 C.1 Counting 1183 C.2 Probability 1189 C.3 Discrete random variables 1196 C.4 The geometric and binomial distributions 1201 C.5 The tails of the binomial distribution 1208 D Matrices 1217 D.1 Matrices and matrix operations 1217 D.2 Basic matrix properties 122

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 3

打赏作者

cppgp

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值