算法导论总结---第一部分,1-5章

本文详细阐述了分治法的概念及其在算法排序(如插入排序、合并排序)、查找(二分查找)及求解特定数学问题(如Fibonacci数列)的应用。同时,文章深入探讨了时间复杂度分析方法,包括主定理、代换法和递归树法,并通过实例展示了如何运用这些方法来评估不同算法的时间效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

欢迎大家挑错

第一部分、算法基础    

这一部分主要说了分治法和算法的时间分析。

一开始先介绍了插入法,这个方法就像我们玩扑克牌的时候,选一张排,插入前面已经排好的序列里面。为了实现这个算法,我们就要有个抽象的思维,假设前面的i个数字已经排好,那么我们就要把i+1位置上的数字和前i的逐个比较,找到i+1应该插入的位置。如果是用数组的话,那就意味着还要移位。算法时间复杂度是O(n^2)c代码如下:

    for(i=1;i<n;++i){

        int key = a[i];

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

            a[j+1]= a[j];

        a[j+1]= key;

    }

接着引入合并排序(mergesort),这个算法用的就是分治的思想,分而治之。把一个问题,分解成几个个子问题,对子问题求解完之后,再对它们的结果合并,如此一来,就解决了问题。对于排序这个例子,我们想象一下如果要分治的话要怎么分呢?既然是要分成子问题,把子问题解决掉之后合并解决大问题,那就是说我分完子问题之后,分开的地方都排好序了,然后把他们合并。怎么合并呢?既然子问题排好了,那么我们就可以每次比较子问题,选出它们最小的那个。在这里我们假设分成两个,那么可以想象成我有两堆牌,他们排好序了,然后每次比较牌面的两张,谁小就放进新的一堆那里。C代码如下:

void merge_sort(int*a,int left,int right)

{

    if (left<right) {

        int mid = (left+right)>>1;

        merge_sort(a,left,mid);

        merge_sort(a,mid+1,right);

        merge(a,left,right);

    }

}

void merge(int *a,int left,int right)

{//b这个临时数组,把[leftmid][mid+1,right]这两部分合并到b那里,然 //后再把b覆盖到aleftright这一段

    int i=left,j=mid+1,k=0;

    int *b = (int*)malloc(sizeof(int)*(right-left+1));

    while(i<=mid&& j<=right)

        if(a[i]>a[j])

            b[k++]= a[i++];

        else

            b[k++]= a[j++];

        //其中一堆还有剩余的话,就直接放上去。

        while(i<=mid)

            b[k++]= a[i++];

        while(j<=right)

            b[k++]= a[j++];

        for(i=left,j=0;i<=right; ++i,++j)

            a[i]= b[j];

        free(b);

}

下面再展示另一个merge的算法,这个方法用了两个哨兵在临时数组末尾:

void merge(int*a,int left,int right)

{

    int mid = (left+right)>>1;

    //因为要放多一个哨兵,所以长度长了一个。左边是[left,mid]所以是mid-left+1

    //的长度,加1就是mid-left+2。右边是rigt-(mid+1)+1,再加1就是

    //right-mid+1了。

    int b1[mid-left+2],b2[right-mid+1];

    b1[mid-left+1]= b2[right-mid] = MAX;

    int i,j,k;

//下面的第一个forif是把复制两部分到临时数组那里去,因为右边部分的长度不会比 //左边的长,所以用右边的边界条件,然后再判断左边是不是还有没复制进去的。

    for(j=0,i=mid+1;i<=hight; ++i,++j) {

        b1[j]= a[low+j];

        b2[j]= a[i];

    }

    if (low+j == mid)

        b1[j]= a[mid];

    for(i=j=0,k=low;k<=hight; )

        a[k++]= b1[i] < b2[j] ? b1[i++] : b2[j++];

}


那么这个分支算法的时间复杂度是多少呢?如果设它是T(n),那么就是T(n)= 2T(n/2)+O(n)。所以就引出一个问题,怎么去解这递归式子。

用主方法T(n)= aT(n/b)+f(n),那么有:



所以我们可以算出合并排序的时间复杂度是nlgn

另外还有两个方法可以分析时间复杂度,一个是万能的代换法,另一个是很直观的递归树法。


接下来,在介绍两个应用分治思想的算法,一个是二分查找,一个是求fibonacci。例如一组数,查找的时候,我们从前到后一个个的比较,找出那个数。如果数组的有序的呢?还这样顺序的找岂不是浪费了已知信息。既然是排好了,那么我们就可以跟这组数中间那个数比较,如果大了,就说明在右边,小了,就说明在左边,这样每次找都可以排除一半的选择。算法的c代码如下:

int binary_search(int x,int *a,int left,int right)

{

    int mid;

    while (left<=right){

        mid= (left+right)>>1;

        if(x<a[mid])

            right= mid-1;

        else if(x>a[mid])

            left= mid+1;

        else

            return mid;

    }

    return FAIL;

}

通常我们在查找失败的时候,还可以返回一个数字i,使a[i-1]<x<a[i],代表如果x要插入的话,插在位置i上面,看看边界条件,在查找失败的时候,left>right,那么大多少了,因为midleftright之间的数,而且每次都只是+1-1的,所以left最多只会比right1,之前那次迭代必然有left=mid=right,如果是+1导致的,就是说x>a[mid],那么就应该返回mid+1,就是说返回left,同样如果是-1导致的,就是说x<a[mid],那么就返回mid就可以了,而这个时候mid=left,所以我们发现在查找失败的时候,只要返回left就可以了,所以把returnFAIL改成returnleft;就可以返回应该插入的位置了。


接下来,就说fibonacci数的求法,用分治的方法的话,我们就要用到快速幂乘和矩阵。先说说快速幂乘积。快速幂乘积就是分治的思想,假设a^n,那么就有a^n=a^(n/2)*a^(n/2),因为这这里是向下取整的,当n是偶数的时候,成立,当n是基数的时候,我们就少成了一个a,所以这个算法要分奇偶两种情况,c代码如下:

int power1(int x,int n)

{

    if(n==1)    return x;

    if(n&0x1)//判断奇偶性

        return power1(x,n>>1)*power1(x,n>>1)*x;

    else

        return power1(x,n>>1)*power1(x,n>>1);

}

对于上面的方法,其实我们还可以用二进制来优化的,因为a^(n1+n2)=a^n1*a^n2,那么我们把n分解成二进制,然后用位运算来把相应位置上的ax次幂成上去就可以啦。C代码如下:

int power2(int a,int n)

{

    int s=1;

    while (n) {

        if(n&0x1)

            s*= x;

        a*=a;

        n>>=1;

    }

    return s;

}


上面这个算法还可以用来当n很大也不溢出的快速求a^nmod m,由数论的知识知道:

    a^n/2 和 (a^n/2mod m) 同余m

那么按照同余的运算法则,那么就有:

    a^n和 (a^n/2mod m)(a^n/2 mod m) 同余m

所以我们就可以用快速求幂的算法来求同余幂,c代码如下:

int mod(int a,int n,int m)

{

    int s = 1;

    while(n){

        if(n&0x1)

            s=s*a%m;

        a=(a*a)%m;

        n>>=1;

    }

    return s;

}


现在的话回到主题,怎么用分治法来求fibonacci数,我们知道fib(n)=fib(n-1)+fib(n-2)

那么如果我们用迭代的方法,时间复杂度是O(n),是这样写的:

int fib(int n)

{

    if(!n) return 0;

    if(n<3) return 1;

    int f1=1,f2=1,f3=1;

    for(inti=3; i<=n; ++i) {

        f1= f2;

        f2= f3;

        f3= f1+f2;

    }

    return f3;

}

用分治法的话,我们可以把把时间复杂度降为O(lgn),用的方法是矩阵,构造一个小的矩阵,使用每次相乘的时候,都会有这个式子:fib(n)=fib(n-1)+fib(n-2),那么我们就可以用快速幂乘积的方法来把矩阵相乘了。先假定一下这样的矩阵

    fib(n)         fib(n-1)

    fib(n-1)      fib(n-2)

如果要它乘上另一个矩阵有那条式子的话,很容易就想到矩阵

1 1

1 0

用这个矩阵自己幂乘后,发现每次相乘之后,其实就等于上面那个矩阵,用归纳法可以严格证明。就是说如果要求f(n),那么就等于求这个矩阵的n次方,然后放回位置(0,0)上的值。有了这个发现,我们就可以写代码了。对于矩阵相乘,每次都是:

    M1[0][0]*M2[0][0]+M1[0][1]*M2[1][0]

    M1[0][0]*M2[0][1]+M1[0][1]*M2[1][1]

    M1[1][0]*M2[0][0]+M1[1][1]*M2[1][0]

    M1[1][0]*M2[0][1]+M1[1][1]*M2[1][1]

算法的c语言代码如下:

int fib(int n)

{

    if(!n) return 0;

    if(n<3) return 1;

    n-=2;

    int a[2][2]={{1,1},{1,0}};

    int b[2][2]={{1,1},{1,0}};

    while (n) {

        if(n&0x1)

            matrixMutiply(b,a);

        matrixMutiply(a,a);

        n>>=1;

    }

    return b[0][0];

}


void matrixMutiply(int m1[][2],int m2[][2])

{

    int m3[2][2];

    m3[0][0]= m1[0][0]*m2[0][0]+m1[0][1]*m2[1][0];

    m3[0][1]= m1[0][0]*m2[0][1]+m1[0][1]*m2[1][1];

    m3[1][0]= m1[1][0]*m2[0][0]+m1[1][1]*m2[1][0];

    m3[1][1]= m1[1][0]*m2[0][1]+m1[1][1]*m2[1][1];

    m1[0][0]= m3[0][0];

    m1[0][1]= m3[0][1];

    m1[1][0]= m3[1][0];

    m1[1][1]= m3[1][1];

}

这一部分主要说的内容就是这样子的了。下面再把一些题目的解答写一下


2.3-7

这题,就先用O(nlgn)的方法对数组进行排序,然后用二分查找,看看每个数有没有相对应的数,相加之后等于x,当然这里可以常数因子的优化,就是把x/2加入做哨兵,然后把数组分成左右两部分,左边<=x/2,右边>=x/2。具体要怎么做呢?就是把数组排序完之后,用插入排序的方法,把x/2插入里面,设它插在位置j,那么就对于[0,j-1)的每一个数,二分查找[j+1,n)有没有相加之后等于x的。


2.4

提示我们修改一下合并排序的算法。求逆序对的数量,就是说求每个数后面比它小的数字有多少个。用合并排序的话,我们知道左边的那一堆的是在右边那一堆的前面,那么就是说在合并的时候,对于每放上一张左边的数,就看看它压住了多少张右边的数。那么总的一个流程,就应该是我先求出两个数都在左边那一堆的逆序对数,再求出两个数都在右边那堆的逆序对数,然后再加上一个数在左边,另一个数在右边的逆序对数,把它们都加起来,就是整个集合的逆序对数了。C代码如下:

int merge_sort_inversion(int *a,int low,int hight)

{

    if(low<hight){

        int mid = (low+hight)>>1;

        int sum = merge_sort_inversion(a,low,mid);

        sum+= merge_sort_inversion(a,mid+1,hight);


        int b1[mid-low+2],b2[hight-mid+1];

        b1[mid-low+1]= b2[hight-mid] = MAX;

        int i,j,k;

        for(j=0,i=mid+1;i<=hight; ++i,++j) {

            b1[j]= a[low+j];

            b2[j]= a[i];

        }

        if (low+j==mid)

            b1[j]= a[mid];

        for(i=j=0,k=low;k<=hight;) {

            if(b1[i]<b2[j]){

                a[k++]= b1[i++];

                sum+=j;//j就等于右边有多少个数字放去了

            }else

                a[k++]= b2[j++];

        }

        return sum;

    }else

        return0;

}

int key = a[i];

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

a[j+1]= a[j];

a[j+1]= key;

}

接着引入合并排序(mergesort),这个算法用的就是分治的思想,分而治之。把一个问题,分解成几个个子问题,对子问题求解完之后,再对它们的结果合并,如此一来,就解决了问题。对于排序这个例子,我们想象一下如果要分治的话要怎么分呢?既然是要分成子问题,把子问题解决掉之后合并解决大问题,那就是说我分完子问题之后,分开的地方都排好序了,然后把他们合并。怎么合并呢?既然子问题排好了,那么我们就可以每次比较子问题,选出它们最小的那个。在这里我们假设分成两个,那么可以想象成我有两堆牌,他们排好序了,然后每次比较牌面的两张,谁小就放进新的一堆那里。C代码如下:

mergesort(int*a,int left,int right)

{

if(left<right){

intmid = (left+right)>>1;

mergesort(a,left,mid);

mergesort(a,mid+1,right);

merge(a,left,right);

}

}

merge(int*a,int left,int right)

{//b这个临时数组,把[leftmid][mid+1,right]这两部分合并到b那里,然 //后再把b覆盖到aleftright这一段

inti=left,j=mid+1,k=0;

int*b = (int*)malloc(sizeof(int)*(right-left+1));

while(i<=mid&& j<=right)

if(a[i]>a[j])

b[k++]= a[i++];

else

b[k++]= a[j++];

//其中一堆还有剩余的话,就直接放上去。

while(i<=mid)

b[k++]= a[i++];

while(j<=right)

b[k++]= a[j++];

for(i=left,j=0;i<=right; ++i,++j)

a[i]= b[j];

free(b);

}

下面再展示另一个merge的算法,这个方法用了两个哨兵在临时数组末尾:



merge(int*a,int left,int right)

{

intmid = (left+right)>>1;

//因为要放多一个哨兵,所以长度长了一个。左边是[left,mid]所以是mid-left+1

//的长度,加1就是mid-left+2。右边是rigt-(mid+1)+1,再加1就是

//right-mid+1了。

intb1[mid-left+2],b2[right-mid+1];

b1[mid-left+1]= b2[right-mid] = MAX;

inti,j,k;

//下面的第一个forif是把复制两部分到临时数组那里去,因为右边部分的长度不会比 //左边的长,所以用右边的边界条件,然后再判断左边是不是还有没复制进去的。

for(j=0,i=mid+1;i<=hight; ++i,++j) {

b1[j]= a[low+j];

b2[j]= a[i];

}

if(low+j==mid)

b1[j]= a[mid];

for(i=j=0,k=low;k<=hight; )

a[k++]= b1[i] < b2[j] ? b1[i++] : b2[j++];

}



那么这个分支算法的时间复杂度是多少呢?如果设它是T(n),那么就是T(n)= 2T(n/2)+O(n)。所以就引出一个问题,怎么去解这递归式子。

用主方法T(n)= aT(n/b)+f(n),那么有:



所以我们可以算出合并排序的时间复杂度是nlgn

另外还有两个方法可以分析时间复杂度,一个是万能的代换法,另一个是很直观的递归树法。


接下来,在介绍两个应用分治思想的算法,一个是二分查找,一个是求fibonacci。例如一组数,查找的时候,我们从前到后一个个的比较,找出那个数。如果数组的有序的呢?还这样顺序的找岂不是浪费了已知信息。既然是排好了,那么我们就可以跟这组数中间那个数比较,如果大了,就说明在右边,小了,就说明在左边,这样每次找都可以排除一半的选择。算法的c代码如下:

int binary_search(int x,int *a,int left,int right)

{

    int mid;

    while (left<=right){

        mid= (left+right)>>1;

        if(x<a[mid])

            right= mid-1;

        else if(x>a[mid])

            left= mid+1;

        else

            return mid;

    }

    return FAIL;

}

通常我们在查找失败的时候,还可以返回一个数字i,使a[i-1]<x<a[i],代表如果x要插入的话,插在位置i上面,看看边界条件,在查找失败的时候,left>right,那么大多少了,因为midleftright之间的数,而且每次都只是+1-1的,所以left最多只会比right1,之前那次迭代必然有left=mid=right,如果是+1导致的,就是说x>a[mid],那么就应该返回mid+1,就是说返回left,同样如果是-1导致的,就是说x<a[mid],那么就返回mid就可以了,而这个时候mid=left,所以我们发现在查找失败的时候,只要返回left就可以了,所以把returnFAIL改成returnleft;就可以返回应该插入的位置了。


接下来,就说fibonacci数的求法,用分治的方法的话,我们就要用到快速幂乘和矩阵。先说说快速幂乘积。快速幂乘积就是分治的思想,假设a^n,那么就有a^n=a^(n/2)*a^(n/2),因为这这里是向下取整的,当n是偶数的时候,成立,当n是基数的时候,我们就少成了一个a,所以这个算法要分奇偶两种情况,c代码如下:

int power1(int x,int n)

{

    if(n==1)    return x;

    if(n&0x1)//判断奇偶性

        return power1(x,n>>1)*power1(x,n>>1)*x;

    else

        return power1(x,n>>1)*power1(x,n>>1);

}

对于上面的方法,其实我们还可以用二进制来优化的,因为a^(n1+n2)=a^n1*a^n2,那么我们把n分解成二进制,然后用位运算来把相应位置上的ax次幂成上去就可以啦。C代码如下:

int power2(int a,int n)

{

    int s=1;

    while (n) {

        if(n&0x1)

            s*= x;

        a*=a;

        n>>=1;

    }

    return s;

}


上面这个算法还可以用来当n很大也不溢出的快速求a^nmod m,由数论的知识知道:

    a^n/2(a^n/2mod m)同余m

那么按照同余的运算法则,那么就有:

    a^n(a^n/2mod m)(a^n/2 mod m)同余m

所以我们就可以用快速求幂的算法来求同余幂,c代码如下:

int mod(int a,int n,int m)

{

    int s = 1;

    while(n){

        if(n&0x1)

           s=s*a%m;

        a=(a*a)%m;

        n>>=1;

    }

    return s;

}


现在的话回到主题,怎么用分治法来求fibonacci数,我们知道fib(n)=fib(n-1)+fib(n-2)

那么如果我们用迭代的方法,时间复杂度是O(n),是这样写的:

int fib(int n)

{

    if(!n) return 0;

    if(n<3) return 1;

    int f1=1,f2=1,f3=1;

    for(inti=3; i<=n; ++i) {

        f1= f2;

        f2= f3;

        f3= f1+f2;

    }

    return f3;

}

用分治法的话,我们可以把把时间复杂度降为O(lgn),用的方法是矩阵,构造一个小的矩阵,使用每次相乘的时候,都会有这个式子:fib(n)=fib(n-1)+fib(n-2),那么我们就可以用快速幂乘积的方法来把矩阵相乘了。先假定一下这样的矩阵

    fib(n)        fib(n-1)

    fib(n-1)     fib(n-2)

如果要它乘上另一个矩阵有那条式子的话,很容易就想到矩阵

1 1

1 0

用这个矩阵自己幂乘后,发现每次相乘之后,其实就等于上面那个矩阵,用归纳法可以严格证明。就是说如果要求f(n),那么就等于求这个矩阵的n次方,然后放回位置(0,0)上的值。有了这个发现,我们就可以写代码了。对于矩阵相乘,每次都是:

    M1[0][0]*M2[0][0]+M1[0][1]*M2[1][0]

    M1[0][0]*M2[0][1]+M1[0][1]*M2[1][1]

    M1[1][0]*M2[0][0]+M1[1][1]*M2[1][0]

    M1[1][0]*M2[0][1]+M1[1][1]*M2[1][1]

算法的c语言代码如下:

int fib(int n)

{

    if(!n) return 0;

    if(n<3) return 1;

    n-=2;

    int a[2][2]={{1,1},{1,0}};

    int b[2][2]={{1,1},{1,0}};

    while (n) {

        if(n&0x1)

            matrixMutiply(b,a);

        matrixMutiply(a,a);

        n>>=1;

    }

    return b[0][0];

}


void matrixMutiply(int m1[][2],int m2[][2])

{

    int m3[2][2];

    m3[0][0]= m1[0][0]*m2[0][0]+m1[0][1]*m2[1][0];

    m3[0][1]= m1[0][0]*m2[0][1]+m1[0][1]*m2[1][1];

    m3[1][0]= m1[1][0]*m2[0][0]+m1[1][1]*m2[1][0];

    m3[1][1]= m1[1][0]*m2[0][1]+m1[1][1]*m2[1][1];

    m1[0][0]= m3[0][0];

    m1[0][1]= m3[0][1];

   m1[1][0]= m3[1][0];

    m1[1][1]= m3[1][1];

}

这一部分主要说的内容就是这样子的了。下面再把一些题目的解答写一下


2.3-7

这题,就先用O(nlgn)的方法对数组进行排序,然后用二分查找,看看每个数有没有相对应的数,相加之后等于x,当然这里可以常数因子的优化,就是把x/2加入做哨兵,然后把数组分成左右两部分,左边<=x/2,右边>=x/2。具体要怎么做呢?就是把数组排序完之后,用插入排序的方法,把x/2插入里面,设它插在位置j,那么就对于[0,j-1)的每一个数,二分查找[j+1,n)有没有相加之后等于x的。


2.4

提示我们修改一下合并排序的算法。求逆序对的数量,就是说求每个数后面比它小的数字有多少个。用合并排序的话,我们知道左边的那一堆的是在右边那一堆的前面,那么就是说在合并的时候,对于每放上一张左边的数,就看看它压住了多少张右边的数。那么总的一个流程,就应该是我先求出两个数都在左边那一堆的逆序对数,再求出两个数都在右边那堆的逆序对数,然后再加上一个数在左边,另一个数在右边的逆序对数,把它们都加起来,就是整个集合的逆序对数了。C代码如下:

int merge_sort_inversion(int *a,int low,int hight)

{

    if(low<hight){

        int mid = (low+hight)>>1;

        int sum = merge_sort_inversion(a,low,mid);

        sum+= merge_sort_inversion(a,mid+1,hight);


        int b1[mid-low+2],b2[hight-mid+1];

        b1[mid-low+1]= b2[hight-mid] = MAX;

        int i,j,k;

        for(j=0,i=mid+1;i<=hight; ++i,++j) {

           b1[j]= a[low+j];

            b2[j]= a[i];

        }

        if (low+j==mid)

           b1[j]= a[mid];

        for(i=j=0,k=low;k<=hight;) {

            if(b1[i]<b2[j]){

                a[k++]= b1[i++];

                sum+=j;//j就等于右边有多少个数字放去了

           }else

                a[k++]= b2[j++];

       }

       return sum;

    }else

        return0;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值