数据结构与算法系列 - 时间复杂度分析

什么是复杂度分析?

  1. 数据结构和算法,本身是让计算机,即快又省。

  2. 执行效率是一个非常重要的考量指标,需要从时间维度和空间维度来评估数据结构和算法性能。

  3. 分别用时间复杂度和空间复杂度来描述性能问题,二者统称为复杂度。

为什么要进行复杂度分析?
  1. 复杂度分析,不依赖硬件的具体的环境。
  2. 测试结果不受数据规模大小的影响。

复杂度分析之时间复杂度

定义:时间复杂度的全称是 渐进时间复杂度, 表示算法的执行时间与数据规模之间的增长关系。

大O复杂度分析法

例子1:

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

分析: 从 CPU 的角度来看,这段代码的每一行都执行着类似的操作: 读数据,运算,写数据 。尽管每行代码对应的 CPU 执行的个数、执行的时间都不一样,但是,我们这里只是粗略估计,所以可以假设每行代码执行的时间都一样,为 unit_time。在这个假设的基础之上,这段代码的总执行时间是多少呢?

解答:

其中,循环这块,会执行N次,那就是n个unit_time,其他的都是1个unit_time。第二行和第三行还有第七行,都是 1个unit_time

第四行,第五行,因为一直循环,是2n*unit_time

例子2:双重循环

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;
     }
   }
}

其实我们最关注的的应该是双重循环这块的时间单位,其他的可以忽略,这块就是:n*unit_time * n*unit_time。

两个例子得出的规律:

所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。

其中,T(n) 我们已经讲过了,它表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。

这就是 大 O 时间复杂度表示法。

大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势, 所以,也叫作 渐进时间复杂度, (asymptotic time complexity),简称: 时间复杂度。

当 n 很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n); T(n) = O(n 2 )。这里的2代表的是n的平方。

时间复杂度分析三个重要方法

1. 只关注循环执行次数最多的一段代码

我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了, 这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。

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

如上,这个例子,就可以排除其他的代码的时间复杂度分析,只关注循环,即复杂度等于: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;
}

100的那个第一个循环可以忽略,第二个循环是o(n),第三个循环是 o(n2)。

为什么第一个循环100的可以忽略,不计算时间复杂度呢?

即便这段代码循环 10000 次、100000 次,只要是一个已知的数,跟 n 无关,照样也是常量级的执行时间。当 n 无限大的时候,就可以忽略。尽管对代码的执行时间会有很大影响,但是回到时间复杂度的概念来说,它表示的是一个算法执行效率与数据规模增长的变化趋势,所以不管常量的执行时间多大,我们都可以忽略掉。因为它本身对增长趋势并没有影响。

总的时间复杂度就等于:o(n2)【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;
}

cal的时间复杂度是:o(n), f函数时间复杂度是:o(n),相乘就是:o(n2)

几种常见的时间复杂度分析实例

数据结构与算法系列 - 时间复杂度分析_第1张图片

1. O(1) 复杂度

一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)

2. O(logn)、O(nlogn)

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

从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得我们高中学过的等比数列吗?实际上,变量 i 的取值就是一个等比数列。如果我把它一个一个列出来,就应该是这个样子的:

所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了。通过 2x =n 求解 x, x=log2n 时间复杂度 = o(log2n),其实,在时间复杂度真正使用的时候,我们都会忽略对数x,那么就是:o(logn)。

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,两个数据规模,但是无法评估m和n的大小,此时的复杂度,应该是两个的乘积:o(m*n)。

时间复杂度随着数据规模增长的趋势图: 

数据结构与算法系列 - 时间复杂度分析_第2张图片

总结

1.说明:文章的内容通过阅读《极客时间-数据结构与算法专栏》综合整理而来的读书笔记。

2.各个数据结构与算法复杂度速查表:https://liam.page/2016/06/20/big-O-cheat-sheet/

你可能感兴趣的:(数据结构与算法)