首页 > 代码库 > 基本概念之算法

基本概念之算法

什么是算法

算法(Algorithm)定义:

  • 一个有限指令集:一堆指令放在一起做一件事情,而这个指令集一定是有限的
  • 有零个或多个输入(有些情况下不需要输入)
  • 有一个或多个输出(一定有输出)
  • 一定在有限步骤之后终止
  • 每一条指令必须
    • 有充分明确的目标,不可以有歧义
    • 计算机能处理的范围之内
    • 描述不依赖于任何一种计算机语言以及具体的手段

例1:选择排序算法的伪码描述

void SelectionSort(int List[],int N){
    /*将N个整数List[0]...List[N-1]进行非递减排序*/
    for(i=0;i<N;i++){
        MinPosition=ScanForMin(List,i,N-1);
        /*从List[i]到List[N-1]中找最小元,并将其位置赋给MinPosition;*/
        Swap(List[i],List[MinPosition]);
        /*将未排序部分的最小元换到有序部分的最后位置;*/      
    }  
}

这段伪码的描述是比较抽象的:

  • 抽象点1:List到底是数组还是链表?
  • 抽象点2:swap用函数还是用宏去实现?

这两个抽象点都需要具体的实现,但是在算法中,这种具体的实现过程是不被关心的。


 什么是好的算法?

用以下两个指标来衡量:

  • 空间复杂度S(n)——根据算法写成的程序在执行时占用存储单元的长度。这个长度往往与输入数据的规模有关。空间复杂度过高的算法可能导致使用的内存超限,造成程序非正常中断。
  • 时间复杂度T(n)——根据算法写成的程序在执行时耗费时间的长度。这个长度往往也与输入数据的规模有关。时间复杂度过高的算法可能导致我们在有生之年都等不到运行结果。

这两个指标与我们处理数据的规模是直接相关的。

例2:PrintN函数的递归实现

1 void PrintN(int N){
2     if(N){
3         PrintN(N-1);
4         printf("%d\n",N);
5     }  
6 }

当N=100000时,程序是非正常跳出了。

分析原因:当传入参数N=100000时,程序判断N不为0,开始递归调用PrintN。在调用之前,系统会把当前的函数所有的现有的状态都存到系统内存的某个地方,

技术分享

然后执行递归函数,执行完后,系统会把这些存储状态回复回来,接着执行此函数的下一句,

技术分享

直到递归到N为1的时候。

技术分享

可以发现程序占用空间的数量实际上与原始N的大小成正比,S(N)=C*N,当N非常大的时候,程序可利用的空间有限,会直接爆掉,所以非正常退出了。

但是在循环程序里面,只用到了一个临时变量和一个for循环,没有涉及任何程序调用的问题,所以不管N多大,它所占用的空间始终是固定的,占用空间的量不会随着N的增长而增长,空间占用始终是一个常量,不会出现问题。

例3:求多项式的值。

1 double f(int n,double a[],double x){
2     int i;
3     double p=a[0];
4     for(i=1;i<=n;i++){
5         p+=(a[i]*pow(x,i));//累加求和
6     }
7     return p;
8 } 
1 double f(int n,double a[],double x){
2     int i;
3     double p=a[n];
4     for(i=n;i>0;i--){
5         p=a[i-1]+x*p;
6     }
7     return p;
8 } 

在分析程序运行效率时,机器执行加减法的效率比执行乘除法的效率要快很多。

第一个函数,每一次for循环执行i次乘法,不要忽略pow(x,i)函数每次执行i-1次乘法,总共执行的乘法次数是:1+2+...+n=(n+1)n/2

第二个函数,每次循环只执行了1次乘法,总共执行的乘法次数是:n

那么,用时间复杂度来衡量时,T1(n)=C1n2+C2n,T2(n)=C?n,当n很大时,第二个程序比第一个程序要快很多很多。

在分析一般算法的效率时,经常关注下面两种复杂度:

  • 最坏复杂度Tworst(n)
  • 平均复杂度Tavg(n)

  显然Tavg(n)<=Tworst(n),我们一般倾向于计算Tworst(n),因为“平均”在不同情况下有不同的理解,分析起来比较复杂。


复杂度的渐进表示

 在分析一个算法时,没必要将算法执行了多少次一步步数出来,需要关注的是随着数据规模的增大,复杂度增长的性质是怎样变化的。

  • T(n)=O(f(n))表示存在常数C>0,n0>0,使得当n>=n0时有T(n)<=C·f(n)
  • T(n)=Ω(g(n))表示存在常数C>0,n0>0,使得当n>=n0时有T(n)<=C·g(n)
  • T(n)=θ(h(n))表示同时有T(n)=O(h(n))和T(n)=Ω(h(n))

 当讨论一个函数复杂度的上界和下界时,其实不是唯一的,它可以有无穷多个上界和下界。但是太大的上界和太小的下界对于分析算法而言是没有帮助的,所以用大O记法时,一般取能找到的最小的上界,最大的下界。

输入规模 n
函数 1 2 4 8 16 32
1 1 1 1 1 1 1
logn 0 1 2 3 4 5
n 1 2 4 8 16 32
nlogn 0 2 8 24 64 160
n2 1 4 16 64 256 1024
n3 1 8 64 512 4096 32768
2n 2 4 16 256 65536 4294967296
n! 1 2 24 40326 2092278988000 26313×1033

在表中所列的输入规模n的最大值才取到32,函数中logn以什么为底并不重要,加了底只是多了一个常数倍而已,影响不大。

           技术分享

这张图显示了不同函数增长的速率,作为专业的程序员,如果设计的算法复杂度达到n2,要考虑能不能降低算法的复杂度。

每秒10亿指令计算机的运行时间表
n f(n)=n nlogn n2 n3 n4 n10 2n
10 .01us .03us .1us 1us 10us 10sec 1us
20 .02us .09us .4us 8us 160us 2.84hr 1ms
30 .03us .15us .9us 27us 810us 6.83d 1sec
40 .04us .21us 1.6us 64us 2.56ms 121.36d 13d
50 .05us .28us 2.5us 125us 6.25ms 3.1yr 4×1013yr
100 .10us .66us 10us 1ms 100ms 3171yr 32×10283yr
1000 1.00us 9.96us 1ms 1sec 16.67min 3.17×1013yr  
10000 10us 130.03us 100ms 16.67min 115.7d 3.17×1023yr  
100000 100us 1.66ms 10sec 11.57d 3171yr 3.17×1033yr  
1000000 1.0ms 19.92ms 16.67min 31.71yr 3.17×107yr 3.17×1043yr  
us=微秒=10-6 sec=秒 hr=小时 yr=年 ms=毫秒=10-3 min=分钟 d=日  

 复杂度分析小窍门

  • 若两段算法复杂度分别有T1(n)=O(f1(n))和T2(n)=O(f2(n)),则
    • T1(n)+T2(n)=max(O(f1(n)),O(f2(n)))
    • T1(n)×T2(n)=O(f1(n)×f2(n))
  • 若T(n)是关于n的k阶多项式,那么T(n)=Θ(nk)
  • 一个for循环的时间复杂度等于循环次数乘以循环体代码的复杂度
  • if-else结构的复杂度取决于if的条件判断复杂度和两个分支部分的复杂度,总体复杂度取三者中最大

 

 

 

技术分享

基本概念之算法