您的位置:首页 > 编程语言 > Go语言

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

2011-12-29 15:42 1011 查看
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

B         B[0]  B[1]   ...  B[n-1]  B

C  C[0]   C[1]  C[2]   ...  C
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'
                     (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
成立。因此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


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 - 1a)这一段实现霍纳规则的代码的渐近运行时间是什么?

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
= 0,迭代开始时,循环后有y
= a


保持:

    对于任意 0 ≤ i ≤ n,循环后有:

        y[i] = a[i] + y[i+1] * x = a[i] + (a[i+1] * x + a[i+2] * x + ... + a
* x^(n-(i+1))) * x

             = a[i] + a[i+1] * x + a[i+2] * x^2 + ... + a
* x^(n-i)

终止:

    i小于0时终止,此时有 y[0] = a[0] + a[1] * x + a[2] * x^2 + a
* 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
* 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
* x^(n-i)

    因此有:

        y'[i] = y[i+1] = a[i+1] + a[i+2] * x + ... + a
* 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值为逆序对的数目。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 merge null c n2 arrays