数据结构与算法分析(一)

意识到数据结构和算法很重要,那还是源于去年保研期间的一些经历。也一直想着抽出一段时间来学习,无奈人忙事杂,先是练车学驾照耗费了一些时日,前段时间出了趟国,去东南亚几个国家走了一遭,玩了一个月,自然相当开心,这事以后也还会讲,再加上一直陆续做自己的个人网站,也一直没闲着。这不,自己的个人网站V1.0前天上线,最近也没有太多事情,就拿起书,正式的学。

书用的是Mark Allen Weiss写的《数据结构与算法分析——C语言描述》,现在看了前两章,看得出语言相当精简,例子也都非常适宜,很详实,书本身也不厚,讲这么多的东西,理解起来自然也就有些难度。说回来,C语言还是大一的时候学的,当时也是第一次接触编程语言,学的多烂自己也清楚,这几年接触的语言,像Python和JavaScript等,抽象性都非常高,突然转到C,还有些不适应,因此在看这本书的时候,反而有很长的时间再温习C语言的书。

废话少说,直接步入正题!

具体的代码,除非自己觉得相当好的,否则就不贴了。

第一章讲了一些基本的概念,其实是灌输一些思想:解决问题的方法也许有很多,在面临大量输入的情况下,不同算法的性能下降的厉害,因此仅仅写出一个可以工作的程序是远远不够的。话说的很犀利,这几乎就是我以前的观念,我想想了想,有这么几个原因:第一个就是自己接触到的问题输入量还不够大,因此算法的效率就不是那么明显,二是现在的计算机性能都非常的好,速度快到你还没意识到算法之间时间运行上的差异,它就结束了。

所以,现实给我们提出的挑战就是:寻找一个性能优异,而且相当稳定,面临大量输入,依旧可以从容不迫地输出,而不至于崩溃的算法。

书的第二章举了几个例子,怎么说呢,不寒而栗吧,待会会说到。

还讲了一些数学的概念,都是级数相关的,个人认为比较重要的一个方法就是:数学归纳法。一个结论也许看起来很自然,但是证明起来难度却很大,数学归纳法包含了“递归”的思想,充分利用已有的信息,逐步的接近一个结论,用起来往往不是很难,毕竟从k到k+1之间的规律还是很好找的。有时有些问题从正面很难突破,借用数学归纳法,借力打力,得出结论。

下面说递归。一个函数,用自己来定义自己,就是递归。递归是一种非常直白的思路,很符合人类思维的模式,用来解决一些看起来很复杂的问题有奇效,递归并不是循环,尽管它们看起来相似,如果往我来形容它们之间的差距,我会说:循环里的计算是等阶的,而递归是降阶的。递归就是把一个复杂的问题分解,一步步分解到一个简单的单元上,然后往回推,一步步就把原始的问题解决了。

书里总结了几个关于递归的规律:

1、基准情形,无需递归就能接触的,也就是最小的那个单元

2、不断推进,每一次递归调用都必须使求解状况朝接近基准情形的方向推进,也就是降阶

3、设计法则,假设所有的递归调用都能运行,递归如果中断,不仅无法分解到基准,而且无法由基准情形回推

4、合成效益法则,也就是避免重复性的工作,这点很重要,这提示我们,递归好用,但并不适合所有的情况

书的第二章开始着手具体的算法分析。算法是求解一个问题需要遵循的、被清楚地指定的简单指令的集合。计算机的思维模式和人不一样,我感觉算法就是是计算机的伟大之处,要知道,CPU上的那一个个的小开关,它们只会开和关,但一群开关提供的计算量却是惊人的。你只要给它一个清晰地、每一步都很简单的步骤,它就能给出答案。伟大!

这一章的前半段讲的非常晦涩,说到底,还是大O分析,就是评估算法运行时间的最坏情况。为什么考虑最坏的情况?仔细想想,这种考虑不仅非常的巧妙,而且很实用。一来呢,平均情况不好把握,影响因素比较多,所以呢,干脆把变量的影响降到最低,直接假设为最坏状况吧,算出来的时间应该是算法运行所需要的最长时间,有了这个数,我们心里就有底了。有的时候我么需要这种极限的思维,因为极限的背后,往往有着比较简单的结论,而这些结论通常都是本质性的。

常见的算法时间复杂度由小到大依次为:Ο(1)Ο(log2n)Ο(n)Ο(nlog2n)Ο(n2)Ο(n3)Ο(2n)Ο(n!)

这个时候书里举出了一个反例,是用递归算法计算斐波那契数列。如下:

int Fib(int N){

    if(N<=1){

        return 1;

    }

    else{

        return Fib(n-1)+Fib(n-2);

    }

}

起初我看到这里觉得这个算法还相当合理,逻辑也很清楚。但作者给出了它的大O分析,时间复杂度是指数级的,这样的算法是我们需要极力避免的。问题出在哪里?大量的重复计算。有个格言说得好“计算任何事情不要超过一次”。斐波那契有个非常明显的规律,就是前两位数字相加等于第三位数,依次递推,每一个位置的数字其实都已经包括了前面数字的信息,但是上述的递归算法却将每一位数都分解为最原始的最小单元,无疑浪费了很多的信息,也没用充分利用斐波那契的规律。尽管代码很短,却并非我们需要的。

书里又给出了一个非常经典的问题:最大子序列和的解。

书里给出了四种解法,时间复杂度一次从Ο(n3)> Ο(n2)>O(nlog2n)>O(n)

//第一种

int MaxSubsequence1(const int A[], int N){

    int Thissum, Maxsum=0;

    int i, j, k;

    for(i=0; i<N; i++){

        for(j=i; j<N; j++){

            Thissum = 0;

            for(k=i; k<=j; k++){

                Thissum += A[k];

            }

            if(Thissum>Maxsum){

                Maxsum = Thissum;

            }

        }

    }

    return Maxsum;

}

//第二种

int MaxSubsequence2(const int A[], int N){

    int Thissum, Maxsum=0;

    int i, j;

    for(i=0; i<N; i++){

        Thissum = A[i];

        for(j=i+1; j<N; j++){

            if(Thissum>Maxsum){

                Maxsum = Thissum;

            }

            Thissum += A[j];

        }

    }

    return Maxsum;

}

//第三种

int Max3(int a, int b, int c){

    int max;

    if(a>b){

        if(a>c){

            max=a;

        }

        else{

            max=c;

        }

    }

    else{

        if(b>c){

            max=b;

        }

        else{

            max=c;

        }

    }

    return max;

}

int MaxSubsum(const int A[], int left, int right){

    int MaxLeftSum=0, MaxRightSum=0;

    int center, i;

    if(left==right){

        if(A[left]<0){

            return 0;

        }

        else{

            return A[left];

        }

    }

    center = (right+left)/2;

    MaxLeftSum = MaxSubsum(A, left, center);

    MaxRightSum = MaxSubsum(A, center+1, right);

    int MaxLeftBorderSum=0, MaxRightBorderSum=0;

    int LeftBorderSum=0, RightBorderSum=0;

    for(i=center; i>=left; i--){

        LeftBorderSum += A[i];

        if(LeftBorderSum>MaxLeftBorderSum){

            MaxLeftBorderSum = LeftBorderSum;

        }

    }

    for(i=center+1; i<=right; i++){

        RightBorderSum += A[i];

        if(RightBorderSum>MaxRightBorderSum){

            MaxRightBorderSum = RightBorderSum;

        }

    }

    return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum+MaxRightBorderSum);

}

int MaxSubsequence3(const int A[], int N){

    return MaxSubsum(A, 0, N-1);

}

//第四种

int MaxSubsequence4(const int A[], int N){

    int Thissum=0, Maxsum=0;

    int i;

    for(i=0; i<N; i++){

        Thissum += A[i];

        if(Thissum>Maxsum){

            Maxsum=Thissum;

        }

        else if(Thissum<0){

            Thissum = 0;

        }

    }

    return Maxsum;

}

第三种算法用到了“分治”的策略,递归的求解,将大问题分而化之,再通过一些少量的计算将二者结合,非常巧妙地思想,极大的降低了时间复杂度。

而第四种无疑更为奇妙了,完全就是线性的复杂度,也称作“联机算法”。

经典的算法:对分查找,用到了二分的思想,也是分而化之,比较常见,不多说。

欧几里得算法求解两数的最大公约数,也是经典算法了。

书的练习题里,有一个关于最大子序列乘积的问题,很有意思。网上查了,才知道叫“动态规划”。

int MaxSeqMul(int A[]){

    int max, min, tempMax, tempMin, temp, i;

    tempMax = tempMin = temp = A[0];

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

        max=Max(A[i], tempMin*A[i], tempMax*A[i]);

        min = Min(A[i], tempMin*A[i], tempMax*A[i]);

        tempMax = max;

        tempMin = min;

        temp = Max(temp, max, 0);

    }

    return temp;

}

也是线性的时间。

总结:其实看了这么多例子,感受应该说很深刻了。以前没有这个意识,觉得算法能写出来就算不错了,何曾想过这之间的效率差异这么大,也深深的佩服想出这些算法的先驱前辈,何等的智慧。这本书写的非常详实,由浅入深,有些例子虽然看懂了,还要静下心来慢慢的消化,书看来还是应该多读,否则井底之蛙,以为世界只有自己的眼光这么大。

却道,此心安处是吾乡
原文地址:https://www.cnblogs.com/lucifer25/p/6501887.html