算法的复杂度介绍

算法的复杂度介绍

算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
为什么要进行算法分析?因为我们需要预测算法所需的资源:计算时间(CPU 消耗)、内存空间(RAM消耗)、通信时间(带宽消耗)等。
除了需要预测算法所需的资源,我们还需要预测算法的运行时间:在给定输入规模时,所执行的基本操作数量,或者称为算法复杂度(Algorithm Complexity)。
那么,应该如何去衡量不同算法之间的优劣呢?
主要还是从算法所占用的“时间”和“空间”两个维度去衡量。
时间维度:是指执行当前算法所消耗的时间,我们通常用“时间复杂度”来描述。
空间维度:是指执行当前算法需要占用多少内存空间,我们通常用“空间复杂度”来描述。

算法分析主要关注点是:最坏情况(Worst Case):任意输入规模的最大运行时间。
算法分析要保持大局观(Big Idea),其基本思路是:
(1)忽略掉那些依赖于机器的常量。
(2)关注运行时间的增长趋势。

一、时间复杂度(Time complexity)
算法的“时间复杂度”,很多人首先想到的的方法就是把这个算法程序运行一遍,那么它所消耗的时间就自然而然知道了。这适合事后分析统计方法,但这种方式非常容易受编程语言、运行环境的影响,在性能高的机器上跑出来的结果与在性能低的机器上跑的结果相差会很大。而且对测试时使用的数据规模也有很大关系。再者,并我们在写算法的时候,还没有办法完整的去运行呢。另一种更为通用的方法就出来了:“ 大O表示法”,记做T(n)=O(f(n)),撇开上述因素,认为算法的执行时间是问题规模n的函数,大O表示代码执行时间随数据规模增长的变化趋势。算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级。

【若有某个辅助函数 f(n),使得当 n 趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称 f(n)是 T(n)的同数量级函数,记作 T(n)=O(f(n)),O(f(n))称为算法的渐进时间复杂度,简称时间复杂度。
虽然对 f(n)没有规定,但是一般都取尽可能简单的函数。
渐近精确界记号:Θ(读big-theta),如:Θ(2n2+n+1)=n2(Θ即去掉低阶项、常数项、高阶项的常参得到)】

求解算法的时间复杂度的具体步骤是:
(1)首先找出算法中的基本语句。算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
(2)计算基本语句的执行次数的数量级。只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上,那就是增长率。
(3)用 O(大写字母 O)记号表示算法的时间性能。将基本语句执行次数的数量级放入 O(大写字母 O)记号中。O是渐近上界记号(读big-oh),如:O(2n2+n+1)=O(n2)
如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体;如果算法中包含并列的循环,则将并列循环的时间复杂度相加。
例如,有以下程序段

for (i=1; i<=n; i++) 
    x++; 
for (i=1; i<=n; i++) 
    for (j=1; j<=n; j++) 
         x++;  

第一个 for 循环的时间复杂度为 O(n),第二个 for 循环的时间复杂度为 O(n2),整个算法的时间复杂度为 O(n+n2)= O(n2)。

O(1)表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,其时间复杂度就是 O(1)。

算法的执行时间取决于控制结构和原操作的综合效果。但一般重点关注原操作的次数。
在一个算法中,执行原操作的次数越少,其执行时间也就相对地越少;执行原操作次数越多,其执行时间也就相对地越多。算法中所有原操作的执行次数称为算法频度,这样一个算法的执行时间可以由算法频度来计量。

例子:

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

假设每行代码的执行时间都是一样的,我们用 1颗粒时间 来表示,那么这个例子的第一行耗时是1个颗粒时间,第三行的执行时间是 n个颗粒时间,第四行的执行时间也是 n个颗粒时间(第二行和第五行是符号,暂时忽略),那么总时间就是 1颗粒时间 + n颗粒时间 + n颗粒时间 ,即 (1+2n)个颗粒时间,即: T(n) = (1+2n)*颗粒时间,从这个结果可以看出,这个算法的耗时是随着n的变化而变化,如果n无限大的时候,常量1就没有意义了,倍数2也意义不大,因此,我们可以简化的将这个算法的时间复杂度表示为:T(n) = O(n)

又如:求两个n阶方阵相加C=A+B的算法如下,求T(n)。

void matrixadd(int A[N][N],int B[N][N],int C[N][N],int n)
{  for (int i=0;i<n;i++)		     //  n+1
     for (int j=0;j<n;j++)	         //  n(n+1)
         C[i][j]=A[i][j]+B[i][j];    //  n^2
}

T(n)= n+1+n(n+1)+n2 = 2n2+2n+1。公式中低阶、常量、系数三部分并不会左右增长趋势,可以忽略,所以大O表示法只要取f(n)中最大量级的即可, T(n) =2n2+2n+1 =O(n2),或T(n)=O((n2))
也就是只关注T(n)的最高阶,忽略其低阶项和常系数,这样既可简化T(n)的计算,又能比较客观地反映出当n很大时算法的时间性能。
一般地:
• 一个没有循环的算法的执行时间与问题规模n无关,记作O(1),也称作常数阶。
• 一个只有一重循环的算法的执行时间与问题规模n的增长呈线性增大关系,记作O(n),也称线性阶。
• 其余常用的算法时间复杂度还有平方阶O(n2)、立方阶O(n3)、对数阶O(logn)、指数阶O(2n)等。

常见的时间复杂度量级有:
常数阶O(1)
对数阶O(logn) 【:在计算机领域,对数log一般以2为底数,不写】
线性阶O(n)
线性对数阶O(nlogn)
平方阶O(n²)
立方阶O(n³)
指数阶(2n)
阶乘阶O(n!)
上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。

下面分别对几个常见的时间复杂度进行示例说明。

常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:

int i = 1;
int j = 2;
int m = i + j;

上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。一般来说,只要算法中不存在循环语句,其时间复杂度就是 O(1)。

线性阶O(n)
这个在最开始的代码示例中就讲解过了,如:

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

这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。

对数阶O(logN)
还是先来看代码:

int i = 1;
while(i<n)
{
    i = i * 2;
}

从上面代码可以看到,在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n
也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)

线性对数阶O(nlogN)
线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
对上面的代码修改一点:

for(m=1; m<n; m++)
{
    i = 1;
    while(i<n)
    {
        i = i * 2;
    }
}

平方阶O(n²)
平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。
举例:

for(x=1; i<=n; x++)
{
   for(i=1; i<=n; i++)
    {
       j = i;
       j++;
    }
}

这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²)
如果将其中一层循环的n改成m,即:

for(x=1; i<=m; x++)
{
   for(i=1; i<=n; i++)
    {
       j = i;
       j++;
    }
}

那它的时间复杂度就变成了 O(m*n)

可以参考上面的O(n²) 去理解立方阶O(n³)、K次方阶O(nk),O(n³)相当于三层n循环,其它的类似。

阶乘阶O(n!)
在此简单一提,算法时间复杂度量为阶乘阶O(n!) 的典型问题是旅行商问题(常被称为“旅行推销员问题”,最短路径问题):
给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路。
涉及到n个城市时,需要执行n!次操作。随着城市数量的增长,花费的运算时间简直不可想象!后来,人们想出了许多相对折中的解决方案,找出近似答案。

二、空间复杂度(Space Complexity)
时间复杂度不是用来计算程序具体耗时的,空间复杂度也不是用来计算程序实际占用的空间的。一个算法的存储量包括形参所占空间和临时变量所占空间。在对算法进行存储空间分析时,只考察临时变量所占空间。

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义,记做S(n)=O(g(n))。其中“O”的含义与时间复杂度分析中的相同。
例如:

int Max(int a[],int n)
{int maxi=0;
   for (int i=1;i<n;i++)
      if (a[i]>a[maxi])
         maxi=i;
   return a[maxi];
}

函数体内分配的变量空间为临时空间,不计形参占用的空间,这里的仅计i、maxi变量的空间。空间复杂度为S(n) =1+1 =O(1)

空间复杂度比较常用的有:O(1)、O(n):

空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
举例:

int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)

空间复杂度 O(n)
我们先看一个代码:

int[] m = new int[n]
for(i=1; i<=n; ++i)
{
   j = i;
   j++;
}

这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)

对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求较低的时间复杂度时,可能会使空间复杂度增加,即占用较多的存储空间;反之,当追求较低的空间复杂度时,可能会使时间复杂度增加,即耗费较长的运行时间。
另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,包括算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等,才能够设计出比较好的算法。

附录
https://segmentfault.com/a/1190000023860213

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学习&实践爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值