复杂度分析(上)

最近买了一个《数据结构与算法之美》的专栏,下面是学习的收获(我是知识的搬运工)。

在之前的面试经历中,面试官只要问到数据结构和算法相关的题时就犯愁,因为平时研究的少,工作中也没有刻意使用,所以一般答这种题都不太好,比如让分析复杂度,下面就简单介绍下复杂度。

复杂度描述的是算法执行时间或占用空间与数据规模的增长关系

大 O 复杂度表示法:

如果我们想知道一段代码的执行时间,可以采用测试的方式,但是测试结果会受到测试的环境的数据的规模的形影,而复杂度分析是可以在不运行代码的情况下,得到一段代码的执行时间。

例子:

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 }

上面这段代码是计算 1,2,3...的和,执行这段代码对CPU来说,每一行执行的都是类似的操作:读数据-运算-写数据。当然每行代码执行的是有差别的,这里假设每行代码执行时间一样,都是 unit_time。那么我们计算一下上面的代码总的执行时间是多少?

2,3行执行分别需要 1个 unit_time,4,5行执行了n遍,所以上面代码总用时 (2n+2)* unit_time。粗略分析,所有代码的执行时间T(n)与每行代码的执行次数成正比。

我们再来看一段代码。

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

2,3,4行分别需要1个 unit_time,5,6行执行了n遍,7,8行执行了 n^2遍,所以总的执行时间为 (2n^2+2n+3)*unit_time。通过上面两个小例子,发现规律是,所有代码代码的执行时间 T(n) 与每行代码的执行次数成正比。

用 大 O 表示就是 T(n) = O(f(n))

其中 T(n) 是代码的执行时间,n 表示数据的规模,f(n)表示代码执行的总次数。O 表示代码的执行时间 T(n) 与 f(n) 成正比关系。

所以第一个例子的 T(n) = O(2n + 2),第二个例子 T(n) = O(2n^2+2n+3),都是大O表示法,但是要注意的是 大O时间复杂度表示的并不是代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫做时间渐进复杂度,简称时间复杂度。

当n很大时,公式中的低阶,系数,常量这三个对增长趋势影响较少,可以忽略。只需要记录最大的量级。所以第一个例子可以记为 T(n)=O(n),第二个例子记为 T(n)=O(n^2)

前面介绍了时间复杂度的由来和表示方法,那么我们如何分析一段代码的时间复杂度呢?有三种方法

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

前面介绍到大O复杂度分析法只是表示一种变化趋势,通常忽略低阶,系数,常量,只需看最大阶的量级。所以在分析代码的时候,只关注循环次数最多的那一段代码就好。

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 }

2,3行是常量级执行时间,与n无关,对复杂度度没啥影响。4,5行是循环次数执行最多的,这两行代码执行了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,然后在求三者之和。

sum_1执行100次,是常量的执行时间,和规模n无关。这里需要注意,即使这段代码执行100000次或更多次,只要是一个已知的数,跟n无关,就是常量级的执行时间。当n无限大的时候,可以忽略。尽管对代码执行时间有很大影响,但时间复杂度表示的是一个算法执行效率与数据规模增长的变化趋势所以不管常量的执行时间多大,都可以忽略。因为它本身对增长趋势没有影响。

同理,sum_2和sum_3分别是 O(n)和O(n^2),对于这三个,我们取量级最大的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)))

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

T1(n)=O(f(n)),T2(n)=O(g(n)),T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)),一般指嵌套循环,下面看个例子

1int cal(int n) {
2   int ret = 0; 
3   int i = 1;
4   for (; i < n; ++i) {
5     ret = ret + f(i);
6   } 
7 } 
8 
9 int f(int n) {
10  int sum = 0;
11  int i = 1;
12  for (; i < n; ++i) {
13    sum = sum + i;
14  } 
15  return sum;
16 }

f()函数的时间复杂度是 T1(n)=O(n),如果先把f()函数看成简单的操作,则cal()函数的时间复杂度是T2(n)=O(n),所以整个cal()函数的时间复杂度是T(n)=T2(n)*T1(n)=O(n*n)=O(n^2)

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

①O(1)

O(1)是常量级时间复杂度的一种表示方法,并非只执行一行代码。只要代码执行时间不是随着n的增大而增大,这样的代码的时间复杂度都是O(1)。或者说,通常只要算法中不存在循环、递归,即使代码有很多行,时间复杂度仍是O(1)

1 int i = 8;
2 int j = 6;
3 int sum = i + j;

上面的代码是3行,但是时间复杂度是O(1),而不是O(3)。

②O(logn)、O(nlogn)

对数阶时间复杂度很常见,同时也是很难分析的一种时间复杂度。下面看一个例子

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

通过上面的分析时间复杂度的方法,我们可以知道这段代码的第3行是执行次数最多的,只要算出第3行执行的次数,就是整个代码的时间复杂度。 i从1开始取值,每一次循环乘以2.可以看到 i=i*2是一个等比数列

我们只要算出x是多少,就是执行的次数了  2^x=n -->x=log2n,所以时间复杂度应该为O(log2n),如果代码改下,时间复杂度是多少呢?

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

很容易就能看出来,应该是O(log3n)。

但是上面的O(log2n)和O(log3n)可以通过换底公式换成以2为底的对数,且可以忽略系数,所以都记做 O(logn)。

关于O(nlogn),就是把上面的代码在循环执行n遍了。其中归并排序、快速排序的时间复杂度就是O(nlogn)

③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)

所以将加法法则做个修改:T1(m)+T2(n)=O(f(m)+g(n))

 

空间复杂度分析

前面说到时间复杂度全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称是渐进空间复杂度,表示算法的存储空间与数据规模的增长关系。

1void print(int n) {
2  int i = 0;
3  int[] a = new int[n];
4  for (i; i <n; ++i) {
5    a[i] = i * i;
6  }

7  for (i = n-1; i >= 0; --i) {
8    print out a[i]
9  }
10}

第2行代码,申请了一个空间存储变量i,但它是常量阶的,根数据规模n无关,可以忽略。第3行申请了大小为n的int类型的数组,其余代码则没有申请空间。所以这段代码的空间复杂度是 O(n)

常见的空间复杂度是 O(1)、O(n)、O(n^2),对数阶的复杂度平时用不到。

复杂度分析口诀:

①单段代码看高频:如循环

②多段代码取最大:如一段代码中既有单循环又嵌套循环,那么取嵌套循环

③嵌套代码求乘积:如递归、嵌套循环等

④多个规模求加法:代码中有多个数据规模,就要取这些的加法

 

下面是一个复杂度趋势图

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值