复杂度分析

复杂度分析的意义

我们平时把写的代码运行一遍,然后通过监控和统计等手段,可以计算出算法执行的时间和占用的内存大小。对于这种方法,我们称之为事后统计法。这种方法有着很大的局限性。一方面他的测试结果受测试环境的影响,另一方面测试结果受测试数据的影响也很大。

大O复杂度表示法

下面来看一个例子,

int cal(int n){
   int sum = 0;
   int i = 1;
   for(;i < n;i++){
      sum = sum + i;
   }
   return sum;
}

假设每条语句的执行时间相同,记为unit_time

所以第2,3,7行代码的执行时间分别需要1unit_time,但是第4,5行代码因为有个循环,且i < n所以他们的循环时间为2n * unit_time,所以这段代码的总执行时间为(2n+ 3)unit_time.

规律:一段代码的总执行时间与每一条语句的执行次数(累加和)成正比。

再看一个例子

int cal(int n){
   int sum = 0;
   int i = 1;
   int j = 1;
   for(;i < n;i++){
      j = 1;
      for(;j < n;j++){
         sum = sum + i*j;
      }
   }
}

此时2,3,4代码的执行时间为1unit_time,5,6执行时间为2n unit_time,7,8执行时间为2n^2

总执行时间为(2n*2 + 2n + 3) * unit_time.

此时我们把这个规律总结成一个公式。

T(n) = O(f(n))    表示代码的执行时间T(n)与f(n)成正比。

T(n)表示代码的执行总时间,n表示数据规模,f(n)表示每条语句执行次数的累加和。

套用大O表示法,第一个例子成为T(n) = O(2n + 3),第二个例子T(n) = O(2n^2 + 2n + 3).

实际上,大O时间复杂度并不表示代码的真正执行时间,而是表示代码执行时间随着数据规模增大的变化趋势,因此常称为渐进时间复杂度,简称时间复杂度。

当n很大时,我们可以忽略公式中的低阶,常量,系数三个部分,只保留最大量级。此时第一个例子就可以写成T(n) = O(n),第二个例子T(n) = O(n^2)。

时间复杂度分析方法

1.加法法则:代码总的复杂度等于量级最大的那段代码的复杂度

大O复杂度只表示一种变化趋势。通常只记录最大量级,因此再代码分析的时候,只关注循环次数最多的那一段代码就可以。

看一个例子

int cal(int n){
   int sum1 = 0;
   int p = 1;
   for(;p < 100;p++){
     sum1 = sum1 + 0;
   }

   int sum2 = 0;
   int q = 1;
  
   for(;q < n;q++){
     sum2 = sum2 + 0;
   }

   int sum = 0;
   int i = 1;
   int j = 1;
   for(;i < n;i++){
      j = 1;
      for(;j < n;j++){
         sum = sum + i*j;
      }
   }
   
}

此时需要注意的是,sum1代码段中,p < 100只是一个常量级的执行时间,与数据规模无关。时间复杂度表示的是代码执行时间随数据规模的增长趋势,因此无论常量级的执行时间多长,本身对增长趋势并没有影响。所以这段代码的时间复杂度为T(n) = O(n^2).

结论:总的时间复杂度等于量级最大部分的那段代码的时间复杂度。这条法则就是加法法则,

公式表示为:

T1(n) = O(f(n)); T2(n) = O(g(n))

T(n) = T1(n) + T2(n) = max(O(f(n),O(g(n))) = O(max(f(n),g(n)))

2.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积 

此处也比较好理解,就不举例子了,直接看公式

T1(n) = O(f(n));T2(n) = O(g(n))

T(n) = T1(n) * T2(n)) = O(f(n) * g(n));

几种常见的时间复杂度量级

1.O(1)

只要代码的执行时间不随数据规模n的变化,代码就是常量级时间复杂度,统一记作O(1)。

此时需要注意O(1)是常量级时间复杂度的一种表示方法并不是指就执行了一行代码。

2.O(logn)、O(nlogn)

对数阶时间复杂度非常常见,比较难分析

看一个例子

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

从代码中可以看出,每次循环一次就乘2,当i(2^x) = n时循环结束。不难发现,变量i的取值为等比数列。  所以根据公式可以算出x = 以2为底n的对数。

如果我没把以上代码换成

i = i * 3;

此时的时间复杂度就为以3为底。

实际上,无论是以什么数字为底,我们可以把所有的对数阶时间复杂度统一记为O(logn).

对于O(nlogn)是不是类似于乘法公式运算得到的。此处就不详细解释了

3.O(m + n)、O(mn)

先看一个例子

int cal(int n,int m){
   int sum1 = 0;
   int i = 1;
   for(;i < m;i++){
      sum1 = sum1 + i;
   }

   int sum2 = 0;
   int j = 1;
   for(;j < n;j++){
      sum2 = sum2 + j;
   }
   
   return sum1 + sum2;
}

上述代码可以看出,m和n是两个无关的数据规模,但是最终的时间复杂度与这两者有关。所以,时间复杂度为O(m + n);

空间复杂度分析方法

前面我们说,时间复杂度的全程为渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。空间复杂度称为渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。

看一个例子

public void reverse(int a[],int n){
   int temp[] = new int[n];
   for(int i = 0;i < n;i++){
      temp[i] = a[n - i - 1];
   }
   for(int i = 0;i < n;i++){
      a[i] = temp[i];
   }
}

在第三行代码中,申请了一个空间来存储变量i,但是他是常量阶的,与数据规模n没有关系,也就是说,i占用存储空间并不会随数据规模n变化,第二行代码中,申请了一个大小为n的int类型的数组,除此之外,剩下的代码没有浪费存储空间,所以该段代码的空间复杂度为O(n).

最好时间复杂度和最坏时间复杂度

看一个例子

int find(int[] array,int n,intx){
   int i = 0;
   int pos = -1;
   for(;i < n;i++){
      if(array[i] == x)
         pos = i;
   }
   return pos;
}

很简单的看出来,这段代码的时间复杂度为O(n),但是分析这个代码可以看出,在数组中查找一个元素,并不需要把数组从头遍历到尾,有可能中途就找到了。

下面看看优化的代码。

int find(int[] array,int n,intx){
   int i = 0;
   int pos = -1;
   for(;i < n;i++){
      if(array[i] == x)
         pos = i;
         break;
   }
   return pos;
}

很简单,加了一个break,但是现在的代码时间复杂度还有O(n)吗,如果第一个元素就是要查找的值,时间复杂度为O(1),但是如果数组中没有这个元素,需要从头遍历到尾,时间复杂度就成了O(n)。所以此时就引申出了最好时间复杂度和最坏时间复杂度的概念。概念很好理解,就不多赘述了。

平均时间复杂度

平均时间复杂度指的是代码被重复执行无数次,对应的时间复杂度的平均值.

此时的时间复杂度为O(n).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值