算法导论第2章习题解析

2.1 插入排序

2.1-1: 略.

2.1-2 :将伪代码第5行 while i > 0 and A[i] > key 修改为 while i > 0 and A[i] < key 即可.

2.1-3 :代码如下所示:

int Linear_Find(int a[],int length,int key){
    for(int i = 0;i < length;i++){
        if(a[i] == key)
            return i;       
    }
    return -1;
}

很显然,这里的循环不变式为: a[0..i1]ea[0..i1],ekey
初始化:首先证明在第一次迭代之前(当 i = 0 时),循环不变式成立。当 i=0 时, a[0..i1] 中不存在任何元素,循环不变式显然是成立的。
保持:在循环体中,如果 a[i]=key, 循环结束,否则继续迭代。在下一次迭代增加 i 将保持循环不变式。
终止:导致 for 循环终止的条件是 a[i]=key 或者 i=length 。这两种情况都符合循环不变式满足的性质。
综上,算法正确。

2.1-4:代码如下所示:

void Binary_Add(int a[],int b[],int c[],int length){
    auto r = 0;
    auto q = 0;
    for(auto i = length-1;i >=0;i--){
        c[i+1] = (a[i]+b[i]+q)%2;
        q = (a[i]+b[i]+q)/2;
    }
    c[0] = q;
}

这里需要的注意的一点是,用于保存结果的数组c的元素个数要比a,b中的元素个数多一个,才能容纳因为进位产生的二进制数。

2.2 分析算法

2.2-1: Θ(n3)

2.2-2: 代码如下所示:

void Selection_Sort(int a[],int length){
    for(auto j = 0;j < length - 1;j++){
        auto k = j;
        for(auto i = j+1;i< length;i++){
            if(a[k]>a[i]){
                k = i;
            }
        }
        auto temp = a[j];
        a[j] = a[k];
        a[k] = temp;
    }
}

这里,其循环不变式为a[0..j],每次迭代之前,a[0..j]都已是按从小到大排好了序的。当j = length - 2,即当j已迭代至倒数第二个元素时,经过循环体后,a[length - 1] >= a[length -2],因此只需要对前n-1个元素运行即可。最好情况(已按从小到大排好了序)是 Θ(n) ,最坏情况(倒序)是 Θ(n2) .

2.2-3: 假定要查找的元素等可能的为数组中的任意元素,假设元素个数为 n,则每一个元素在位置 i 出现的概率的概率为 1 / n ,E = 1*(1 / n) + 2*(1 / n) + ……+ n*(1 / n) = (n+1) / 2。最坏情况下需要查询 n 次。所以在平均情况和最坏情况下运行时间都是 Θ(n)

2.2-4:略。

2.3 设计算法

2.3-1 :略。

2.3-2:代码如下所示:

void Merge(int a[],int p,int q,int r){
    int n1 = q - p + 1;
    int n2 = r - q;

    int* L = (int*)malloc((n1)*sizeof(int));
    int* R = (int*)malloc((n2)*sizeof(int));

    for(int i = 0; i < n1; i++){
        L[i] = a[p+i];
    }

    for(int j = 0; j < n2; j++){
        R[j] = a[q+j+1];
    } 

    int m = 0;
    int n = 0;

    while(m < n1 && n < n2){
        if(L[m] < R[n]){
            a[p++] = L[m++];
        }
        else{
            a[p++] = R[n++];
        }
    }

    if(m == n1){
        while(n < n2){
            a[p++] = R[n++];
        }
    }

    if(n == n2){
        while(m < n1){
            a[p++] = L[m++];
        }
    }
}

2.3-3: 略。

2.3-4:代码如下所示:

#include<stdio.h>

void Insert(int a[],int p){
    int key = a[p];
    int i = p - 1;
    while(i>=0 && a[i] > key){
        a[i+1] = a[i];
        i--;
    }
    a[i+1] = key;
}

//插入排序的递归版本
//T(n) = c(n = 1)
//T(n) = T(n-1) + c*n (n >=1)
void InsertSort_Cursor(int a[],int p){
    if(p >= 1){
        InsertSort_Cursor(a,p-1);
        Insert(a,p);
    }
}

//打印数组函数
void PrintArray(int a[],int length){
    for(auto i =0; i < length;i++){
        printf("%d ",a[i]);
    }
    printf("\n");
}

int main(){
    int a[] = {31,41,59,26,41,58};
    printf("Before Sorting....\n");
    PrintArray(a,6);
    InsertSort_Cursor(a,5);
    printf("After Sorting....\n");
    PrintArray(a,6);
    getchar();
    return 0;
}

2.3-5:代码如下所示:

#include<stdio.h>

int BinarySearch(int key,int a[],int low,int high){
    if(low > high) return -1;
    int mid = low + (high - low) / 2;
    if(key < a[mid]) return BinarySearch(key,a,low,mid - 1);
    else if(key > a[mid]) return BinarySearch(key,a,mid+1,high);
    else return mid;  
}

int main(){
    int a[] = {1,2,3,4,5,6,7,8,9};
    printf("%d\n",BinarySearch(9,a,0,8));
    getchar();
    return 0;
}

根据递归式:T(n) = T(n / 2) + c (n > 1);T(n) = c (n = 1) 可证明耗时为:(lgn + 1)*c,可表示为 Θ(lgn) .

2.3-6:代码如下所示:

#include<stdio.h>

int BinarySearch(int key,int a[],int low,int high){
    if(low > high) return low;
    int mid = low + (high - low) / 2;
    if(key < a[mid]) return BinarySearch(key,a,low,mid - 1);
    else if(key > a[mid]) return BinarySearch(key,a,mid+1,high);
    else return mid;  
}

void Insertion_Sort_BS(int a[],int length){
    for(int j = 1; j < length; j++){
        int key = a[j];
        int i = j - 1;
        int low = BinarySearch(key,a,0,i);
        if(a[low] < key){
           low++;
        }
        while(i >= low){
            a[i+1] = a[i];
            i--;
        }
        a[i+1] = key;
    }
}

void PrintArray(int a[],int length){
    for(auto i =0; i < length;i++){
        printf("%d ",a[i]);
    }
    printf("\n");
}

int main(){
    int a[] = {8, 7, 6, 5, 4, 3, 2, 1};
    Insertion_Sort_BS(a,8);
    PrintArray(a,8);
    getchar();
    return 0;
}

其伪代码如下所示:

                                    代价            次数
for j = 2 to A.length               c1             n
    key = A[j]                      c2             n-1
    i = j - 1;                      c3             n-1
    low = Binary(key,a,0,i)         c4*lg(n-1)     n-1
    if(a[low] < key)                c5             n-1
        low ++                      c6             n-1
    while(i >= low)                 c7             (i1 到 n-1 的求和式)  
        a[i+1] = a[i]               c8             (i1 到 n-2 的求和式)
        i--                         c9             (i1 到 n-2 的求和式)
    a[i+1] = key                    c10            n-1

从上面的伪代码分析,不难看出在while循环中会产生n的2次方项,那么其运行时间必然是 Θ(n2) ,因此不能使用二分查找来把排序的最坏情况总运行时间改进到 Θ(nlgn) .

2.3-7:该代码可描述如下:
(1) 先用合并排序算法对 S 中的元素按从小到大的顺序排序,其产生的运行时间为 Θ(nlgn) .
(2) 迭代 S 中的元素e,在循环体中使用二分查找算法查找元素 x-e,运行时间也为 Θ(nlgn) .
因为(1)和(2)是顺序运行的,所以总的运行时间还是为 Θ(nlgn) .

思考题:

2-1: 直接上代码:

#include<stdio.h>
#include<limits.h>
#include<stdlib.h>

#define K 4

void Insertion_Sort(int a[],int low,int high){
    for(auto j = low;j<high;j++){
        auto i = j+1;
        auto key = a[i];
        while(i > low && a[i-1] > key){
            a[i] = a[i - 1];
            i--;
        }
        a[i] = key;
    }
}

void Merge(int a[],int p,int q,int r){
    int n1 = q - p + 1;
    int n2 = r - q;

    int* L = (int*)malloc((n1+1)*sizeof(int));
    int* R = (int*)malloc((n2+1)*sizeof(int));

    for(int i = 0; i < n1; i++){
        L[i] = a[p+i];
    }

    for(int j = 0; j < n2; j++){
        R[j] = a[q+j+1];
    }

    L[n1] = INT_MAX;
    R[n2] = INT_MAX;

    int m = 0;
    int n = 0;

    for(int h = p; h <= r; h++){
        if(L[m] < R[n]){
            a[h] = L[m];
            m++;
        }
        else{
            a[h] = R[n];
            n++;
        }
    }
}
void Merge_Sort(int a[],int p,int r){
    int q = (p + r) / 2;
    if(q - p + 1 <= K){
        Insertion_Sort(a, p, q);
    }
    else{
        Merge_Sort1(a, p, q);
    }
    if(r - q <= K){
        Insertion_Sort(a, q+1, r);
    }
    else{
        Merge_Sort1(a, q+1,r);
    }
    Merge(a,p,q,r);
}

void PrintArray(int a[],int length){
    for(auto i =0; i < length;i++){
        printf("%d ",a[i]);
    }
    printf("\n");
}
int main(){
    int a[] = {1,2,3,13,12,23,45,67,7,8,9,10,43,4,24,56,78,95,100,23,45,67,78,89,32,43,54,65,87,98,123,234,17,14,16,18,19,20,43,36,37,78,90,21,31,41,54,65,76,81,91,21,25,26,27,28,29,31,43,54,65,21,32,45};           
    Merge_Sort(a,0,63);
    PrintArray(a,64);   
    getchar();     
    return 0;
}

a. 证明: 插入排序最坏情况可以在 Θ(nk) 时间内排序每个长度为 k 的 n / k 个子表。

我们知道插入排序的耗时为 T(n)=an2+bn+c 的形式 ,这里有 k/n 个插入操作,那么耗时为 Θ((k/n)T(n))=Θ(a(nk)+kb+ck/n)=Θ(nk) .

b. 表明在最坏情况下如何在 Θ(nlg(n/k)) 时间内合并这些子表。

我们先写出上述代码耗时的递归式:

T(n)={Θ(k2)2T(n/2)+Θ(n),n=kn>k

上述递归式中,当 n>k 时, T(n)=2T(n/2)+Θ(n) ,其中 Θ(n) 用于合并耗时。在最坏情况下,需要执行多少次合并步骤呢?

如果我们画出其递归树的话,那么递归树的最底层的每个叶子节点为 Θ(k2) ,即分解为 n/k 个长度为 k 的子问题时产生的排序耗时,而上面的每一层耗时都为Θ(n) ,只要找出层数,我们就可以知道合并步骤产生的总耗时。

那么怎么去找这个层数?我的方法是举一个实际的例子。假如 n=64,k=4 ,那么递归树的层数为 4 ,即 level=lg(n/k) . 所以在最坏情况下合并步骤的耗时为 lg(n/k)Θ(n)=Θ(nlg(n/k))

c. 假定修改后的算法的最坏运行时间为 Θ(nk+nlg(n/k)) ,要使修改后的算法与标准的归并排序具有相同的运行时间,作为 n 的一个函数,借助Θ 记号, k 的最大值是什么?

首先,由上述a,b两部分,我们知道修改后的算法其实由两部分组成:排序 n/k 个子问题以及递归地合并这些子问题,前者产生的耗时为 Θ(nk) 而后者产生的耗时为 Θ(nlg(n/k)) 。所以算法总运行时间即为: Θ(nk+nlg(n/k)))

明白了这个式子的由来,我们再来分析问题。我们知道标准的合并排序的耗时为 Θ(nlg(n)) 。假如要求: Θ(nk+nlg(n/k))=Θ(nlg(n)) 即: Θ(nk+nlg(n)nlg(k))=Θ(nlg(n)) k=lg(n) 时,上式的左边即为: Θ(nlg(n)+nlg(n)nlg(lg(n)))=Θ(nlg(n)nlg(lg(n))) ,由于 n(lg(lgn)) 很小,这里可以略去。所以, Θ(nlg(n)nlg(lg(n)))=Θ(nlg(n))

从上面的分析可知,k 的最大值为 lg(k)

d. 在实践中,我们该如何选择 k?
参见习题 1.2-2

2-2. 冒泡排序的正确性

我不喜欢伪代码,所以老规矩先上完整代码:

#include<stdio.h>

void PrintArray(int a[],int length){
    for(auto i =0; i < length;i++){
        printf("%d ",a[i]);
    }
    printf("\n");
}


void Bubble_Sort(int a[], int length){
    for(int i = 0; i < length - 1; i++){
        for(int j = length; j > i; j--){
            if(a[j-1] > a[j]){
                int temp = a[j];
                a[j] = a[j-1];
                a[j - 1] = temp;
            }
        }
    }
}

int main(){
    int a[] = {31,41,59,26,41,58};
    printf("Before Sorting....\n");
    PrintArray(a,6);
    Bubble_Sort(a,6);
    printf("After Sorting....\n");
    PrintArray(a,6);
    getchar();
    return 0;
}

a. 我们还需要证明 A’ 的元素是由 A 中的元素组成。

b. 第 2 到 4 行的 for 循环的循环不变式为 A[j..n].
在每次迭代之前,A[j..n] 中的元素就是上一次迭代产生的A[j..n]中的元素,但A[j]却是A[j..n]中最小的元素。

初始化:在第一次迭代之前 j = n,A[j..n]中只有一个元素A[n]组成,实际上就是A[n]中原来的元素,而且A[j]是最小的元素。这表明第一次循环迭代之前循环不变式成立。

保持:在循环体中,比较A[j]和A[j - 1]的大小,使A[j - 1]成为较小的那个元素,同时子数组长度增加1,其第一个元素是子数组中最小的一个。

终止:当 j = i + 1 时,比较A[i] 和 A[i+1]的大小,使A[j - 1]成为较小的那个元素,同时子数组长度增加1,其第一个元素是子数组中最小的一个,同时循环终止。

c. 1 到 4 行的 for 循环的循环不变式为:A[1..i - 1],每次迭代之前,A[1..i-1]已排好序,并且都小于A[i..n].

初始化:在第一次迭代之前,A[1..i-1]为空,自然满足循环不变式的要求。

保持: 从 b 部分,我们知道,内循环之后,A[i]是A[i..n]中的最小的数,而在外循环开始时,A[1..i-1]中的元素都比A[i..n]中的元素小,并且A[1..i-1]已排好了序。因此外循环之后,A[1..i]中的元素都比A[i+1..n]中的元素小,并且已排好了序。

终止:当 i = A.length 时,循环终止,A[1..n]中的元素已排好序。

d. 显然,冒泡排序的最坏情况运行时间为 Θ(n2) ,虽然插入排序的最坏情况运行时间也为 Θ(n2) ,但冒泡排序的内循环中交换两个元素要比插入排序中移动元素的步骤多,所以插入排序要比冒泡排序的性能要好一些。

2-3:a. 很显然霍纳规则求解多项式的算法运行时间是 Θ(n)
b. 朴素的方式求解多项式代码如下:

int polynomial_value(int A[], int x, int n){
    int y = A[0];
    for(int i = 1; i <= n; i++){
        int h = x;
        for(int j =1; j < i;j++){
            h*= x;
        }
        y+=A[i]*h;
    }
    return y;
}

int main(){
    int A[] = {1, 2, 3, 4};
    int value = polynomial_value(A,1,3);
    return 1;
}

上述普通的算法的具有嵌套for循环结构,因此,其算法的运行时间为 Θ(n2)

c. 略
d. 略

2- 4 :
a. (2,1),(8,1),(6,1),(8,6),(3,1)。

b. 倒序数组组成的逆序对最多,如果一个数组中有n个按照倒序从大到小的元素,那么其逆序对的个数为 n(n1)/2

c. 假如用 y 表示数组中的逆序对的个数,那么插入排序的运行时间 T(n)=Θ(y)=Θ(n2) .

d. 在合并时,如果 L[m] > R[n] ,那么L数组中从 m 开始到结束(不包括哨兵元素)的元素都会大于 R 数组中从 m 开始到结束(不包括哨兵元素)的元素,根据这个特点我们就可以写出代码,代码如下所示:

#include<stdlib.h>
#include<limits.h>
#include<stdio.h>

int cnt = 0;

void Merge(int a[],int p,int q,int r){
    int n1 = q - p + 1;
    int n2 = r - q;

    int* L = (int*)malloc((n1+1)*sizeof(int));
    int* R = (int*)malloc((n2+1)*sizeof(int));

    for(int i = 0; i < n1; i++){
        L[i] = a[p+i];
    }

    for(int j = 0; j < n2; j++){
        R[j] = a[q+j+1];
    }

    L[n1] = INT_MAX;
    R[n2] = INT_MAX;

    int m = 0;
    int n = 0;

    for(int k = p; k <= r; k++){
        if(L[m] <= R[n]){
            a[k] = L[m];
            m++;
        }
        else{
            a[k] = R[n];
            cnt += q - p - m + 1;
            n++;
        }
    }
}

void Merge_Sort(int a[],int p,int r){
    if(p < r){
        int q = (p + r) / 2;
        Merge_Sort(a, p, q);
        Merge_Sort(a, q + 1, r);
        Merge(a, p, q, r);
    }
}

void PrintArray(int a[],int length){
    for(auto i =0; i < length;i++){
        printf("%d ",a[i]);
    }
    printf("\n");
}

int main(){
    int a[] = {5, 2, 4, 7, 1, 3, 2, 6};
    Merge_Sort(a,0,7); 
    getchar();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值