代码复杂度分析——时间、空间复杂度

最近练习算法题,又看了极客时间中的《数据结构与算法之美》写的真不错,于是总结一下关于复杂度的知识,代码和图片都是课程里面的。虽然是按课程写的,但是自己写一遍最好,否则看过就忘了。

数据结构与算法本身解决的是的问题。如何让代码运行的更快,如何更省空间有个重要的标准:复杂度分析

一、大 O 复杂度表示法

算法的执行效率,粗略的讲就是算法代码执行的时间,在我们不做精确监控统计,靠观察怎么分析代码执行的时间呢?

int cal(int n) {
   int sum = 0;--------2
   int i = 1;--------3
   for (; i <= n; ++i) {--------4
     sum = sum + i;--------5
   }
   return sum;
 }

上面这段代码是计算1到n的累加和,从CPU的角度来看每一行的执行都是:读数据-计算-写数据。尽管每行代码对应的CPU执行时间都不一样,做粗略计算的时候只需要估算,把这个时间看成相同的一个时间:unit_time。

2,3行代码分别需要1*unit_time,for循环里面的第4,5行需要n*unit_time,得到的结果是2*unit_time+2n*unit_time = (2n+2)*unit_time。得出一个结论:所有代码执行的时间T(n)与每行代码执行的时间unit_time成正比

按照这个方法看下面一段代码:

int cal(int n) {
   int sum = 0;--------2
   int i = 1;--------3
   int j = 1;--------4
   for (; i <= n; ++i) {--------5
     j = 1;--------6
     for (; j <= n; ++j) {--------7
       sum = sum +  i * j;--------8
     }
   }
 }

2,3,4分别行执行了1次总共3*unit_time,5,6行分别执行了n*unit_time,7,8行分别执行了n^2*unit_time,总共是(2n^2+2n+3)*unit_time.尽管不知道*unit_time的具体值,但还是得到那个规律:代码执行的时间T(n)与每行代码执行的次数f(n)成正比

总结一个公式就是:

  1.  T(n)表示代码执行时间
  2. n是数据规模
  3. f(n)是每行代码执行次数总和
  4. O表示代码执行时间和代码执行总次数成正比

所以第一段代码的T(n) = O(2n+2),第二段代码的T(n) = O(2n^2+2n+3)这就是时间复杂度(asymptotic time complexity)的表示方法

而代码规模n很大时就可以忽略掉低阶,常量和系数,最终得到,第一段代码的T(n) = O(n),第二段T(n) = O(n^2)

二、时间复杂度分析

1.只关注循环执行次数最对的代码

就像上面第一段代码,我们只关注for循环的运行次数就可以得出这段代码的时间复杂度是T(n) = O(n)

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

这里有段代码,我们分析一下


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

   int sum_2 = 0;
   int q = 1;
   for (; q < n; ++q) {
     sum_2 = sum_2 + q;
   }
 
   int sum_3 = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1; 
     for (; j <= n; ++j) {
       sum_3 = sum_3 +  i * j;
     }
   }
 
   return sum_1 + sum_2 + sum_3;
 }

这段代码可以分为三部分sum_1、sum_2、sum_3

  1. 第一部分for'循环了100次,时间复杂度是O(1),常数级别的如果有更高阶可以忽略掉,为什么是O(1)下面解释
  2. 第二部分的时间复杂度分别是O(n)
  3. 第三部分的时间复杂度分别是O(n^2)

很显然最高阶是第三部分,所以这段代码的时间复杂度是O(n^2)

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

看一段代码


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

for循环里面嵌入的方法f(x)的时间复杂度为O(n)外部for循环的复杂度是O(n)那么这段代码的时间复杂度就是O(n*n)=O(n^2)。其实还是跟上面分析的一样是两个否循环的嵌套跟2中那个最高阶的一样。

三、常见的时间复杂度实例

上面这些可以分两类,多项式量级非多项式量级,其中费多项式量级只有划黄色下划线那两个。非多项式量级的算法问题叫作 NP(Non-Deterministic Polynomial,非确定多项式)问题。

按我们的数学知识,非多项式量级的时间复杂度会随数据规模n的增大而急剧增大,求解问题的执行时间会无线增长,所以非多项式量级的算法其实效率非常低。所以不多去分析了,只分析多项式量级算法。

1.O(1)

看段代码

 int i = 8;
 int j = 6;
 int sum = i + j;

这段代码的时间复杂度并不是O(3)而是O(1),只要代码的运行时间不随n的增大而增长我们都记作O(1),只要不存在递归和循环,再多行代码的时间复杂度都是O(1)

2. 对数阶O(logn)、O(nlogn)


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

不难看出这段代码的第三行的阶数最高,i的取值是个等比数列

x=\log_{2}n如果把2换成3就有\log_{3}n其实不管底是2,3还是10时间复杂度都可以记为O(logn)因为\log_{3}n=(C * \log_{2}n其中C = \log_{3}2是个常数可以忽略,对数阶时间复杂度表示的时候可以省略调对数的底,统一写成O(logn)。理解了这个O(nlogn)其实就不难理解了,就是一个嵌套的乘法法则,一个循环嵌套一个时间复杂度为O(logn)的代码。比如归并排序和快速排序的时间复杂度都是O(nlogn)

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

代码的时间复杂度由两种数据的规模来决定


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

  int sum_2 = 0;
  int j = 1;
  for (; j < n; ++j) {
    sum_2 = sum_2 + j;
  }

  return sum_1 + sum_2;
}

像这种我们无法预估m和n的量级谁的比较大,所以无法用加法法则忽略掉一个,所以表示为O(m+n),但乘法法则依然有效所以可以有O(m * n)

四、空间复杂度

空间复杂度的全称是渐进空间复杂度(asymptotic space complexity),表示的是算法的存储空间跟数据规模的增长关系。看代码:

void print(int n) {
  int i = 0;
  int[] a = new int[n];
  for (i; i = 0; --i) {
    print out a[i]
  }
}

代码中申请了一个长度为n的数组,其他地方没有占用到更多空间,所以空间复杂度就是O(n),常见的空间复杂度就是 O(1)、O(n)、O(n2 ),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。

 

你可能感兴趣的:(数据接管与算法)