算法与数据结构(一)分析算法的执行效率和资源消耗

前言

学习数据结构和算法不仅仅是让我们可以用更合理的方式实现程序,使应用的程序性能更佳,更重要的是,它会影响你的程序思维,帮助你理解某些框架的底层实现,更方便你造轮子。 我最近在极客App上学习王争老师(前Google员工)的课程,《数据结构与算法之美》,接下来用该篇来记录一下入门篇学习内容。

开篇

入门篇主要讲解时间、空间复杂度,通过实例,学习复杂度分析,为之后的学习铺路。通过这一模块的学习,掌握时间、空间复杂度的概念,大 O 表示法的由来,各种复杂度分析技巧,以及最好、最坏、平均、均摊复杂度分析方法。

为什么要学习数据结构和算法

在大学也学习过数据结构相关的课程,但当时由于课程生涩,又觉得用处不大,也没有仔细研究,结果到现在仍对数据结构半知半解,只能说大概了解一些数组、链表、快排这些最最基本的数据结构和算法的只是,稍微复杂一点的就完全没概念了。

实际上,算法和数据结构的掌握对程序猿是相当重要的。

  1. 最现实的一点,想进大厂,算法都是躲不了的一关,越是厉害的公司,越是注重考察数据结构与算法这类基础知识。相比短期能力,他们更看中你的长期潜力。
  2. 不能做一辈子的CRUD工程师。确实,对于大部分业务开发来说,我们平时可能更多的是利用已经封装好的现成的接口、类库来堆砌、翻译业务逻辑,很少需要自己实现数据结构和算法。但是,不需要自己实现,并不代表什么都不需要了解。如果不知道这些类库背后的原理,不懂得时间、空间复杂度分析,你如何能用好、用对它们呢?调用了某个函数之后,你又该如何评估代码的性能和资源的消耗呢?掌握数据结构和算法,不管对于阅读框架源码,还是理解其背后的设计思想,都是非常有用的。
  3. 不要只会写凑合能用的代码。性能好坏起码是一个评判代码能力的非常重要的标准。但是,如果你连代码的时间复杂度、空间复杂度都不知道怎么分析,怎么写出高性能的代码呢?

开篇总结

我们学习数据结构和算法,并不是为了死记硬背几个知识点。我们的目的是建立时间复杂度、空间复杂度意识,写出高质量的代码,能够设计基础架构,提升编程技能,训练逻辑思维,积攒人生经验,以此获得工作回报,实现你的价值,完善你的人生。掌握了数据结构与算法,你看待问题的深度,解决问题的角度就会完全不一样。


数据结构和算法的概念

从广义上讲,数据结构就是指一组数据的存储结构。算法就是操作数据的一组方法。
从狭义上讲,是指某些著名的数据结构和算法,比如队列、栈、堆、二分查找、动态规划等。这些都是前人智慧的结晶,我们可以直接拿来用。我们要讲的这些经典数据结构和算法,都是前人从很多实际操作场景中抽象出来的,经过非常多的求证和检验,可以高效地帮助我们解决很多实际的开发问题。
数据结构是为算法服务的,算法要作用在特定的数据结构之上。


复杂度分析

大 O 复杂度表示法

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

以上代码求 1,2,3…n 的累加和,从 CPU 的角度来看,这段代码的每一行都执行着类似的操作:读数据-运算-写数据。尽管每行代码对应的 CPU 执行的个数、执行的时间都不一样,但是,我们这里只是粗略估计,所以可以假设每行代码执行的时间都一样,为 unit_time。第 2、3 行代码分别需要 1 个 unit_time 的执行时间,第 4、5 行都运行了 n 遍,所以需要 2n*unit_time 的执行时间,所以这段代码总的执行时间就是(2n+2)*unit_time。可以看出来,所有代码的执行时间 T(n) 与每行代码的执行次数成正比。

我们可以把这个规律总结成一个公式:
在这里插入图片描述
其中,T(n) 表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比

大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级即可。如上例中的代码时间复杂度可记为T(n) = O(n)。

时间复杂度分析

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

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

还是那这个例子来看,其中第 2、3 行代码都是常量级的执行时间,与 n 的大小无关,所以对于复杂度并没有影响。循环执行次数最多的是第 4、5 行代码,所以这块代码要重点分析。因为这两行代码被执行了 n 次,所以总的时间复杂度就是 O(n)。
2.加法法则:总复杂度等于量级最大的那段代码的复杂度
如果 T 1 ( n ) = O ( f ( n ) ) T1(n)=O(f(n)) T1(n)=O(f(n)) T 2 ( n ) = O ( g ( n ) ) T2(n)=O(g(n)) T2(n)=O(g(n));那么 : T ( n ) = T 1 ( n ) + T 2 ( n ) = m a x ( O ( f ( n ) ) , O ( g ( n ) ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))) T(n)=T1(n)+T2(n)=max(O(f(n)),O(g(n)))=O(max(f(n),g(n))).
3.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
类比上面的加法法则,如果 T 1 ( n ) = O ( f ( n ) ) T1(n)=O(f(n)) T1(n)=O(f(n)) T 2 ( n ) = O ( g ( n ) ) T2(n)=O(g(n)) T2(n)=O(g(n));那么 : T ( n ) = T 1 ( n ) ∗ T 2 ( n ) = O ( f ( n ) ) ∗ O ( g ( n ) ) = O ( f ( n ) ∗ g ( n ) ) T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)) T(n)=T1(n)T2(n)=O(f(n))O(g(n))=O(f(n)g(n)).
落实到具体的代码上,我们可以把乘法法则看成是嵌套循环

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() 函数。假设 f() 只是一个普通的操作,那第 4~6 行的时间复杂度就是,T1(n) = O(n)。但 f() 函数本身不是一个简单的操作,它的时间复杂度是 T2(n) = O(n),所以,整个 cal() 函数的时间复杂度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n2)。

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

复杂度量级(按数量级递增)

  • 常量阶O(1)
  • 对数阶O( l o g n logn logn)
  • 线性阶O( n n n)
  • 线性对数阶O( n l o g n nlogn nlogn)
  • 平方阶O( n 2 n^2 n2)、立方阶O( n 3 n^3 n3)…k次方阶O( n k n^k nk)
  • 指数阶O( 2 n 2^n 2n)
  • 阶乘阶O( n ! n! n!)
    以上复杂度量级,我们可以粗略地分为两类,多项式量级非多项式量级。其中,非多项式量级只有两个:O( 2 n 2^n 2n) 和 O( n ! n! n!)。

由数或字母的积组成的代数式叫做单项式,单独的一个数或一个字母也叫做单项式。由若干个单项式相加组成的代数式叫做多项式。

我们把时间复杂度为非多项式量级的算法问题叫作 NP(Non-Deterministic Polynomial,非确定多项式)问题。当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。因此我们主要关注几种常见的多项式时间复杂度。

1. O(1)

首先必须明确一个概念,O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。
总结一下,只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。

2. O( l o g n logn logn)、O( n l o g n nlogn nlogn)

对数阶时间复杂度非常常见,同时也是最难分析的一种时间复杂度。

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

从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了。通过 2 x = n 2^x=n 2x=n 求解 x 可知 x = l o g 2 n x=log_2n x=log2n,所以,这段代码的时间复杂度就是 O( l o g 2 n log_2n log2n)。

另外,对数之间是可以互相转换的, l o g 3 n log_3n log3n 就等于 l o g 3 2 ∗ l o g 2 n log_32 * log_2n log32log2n,所以 O( l o g 3 n log_3n log3n) = O( C ∗ l o g 2 n C * log_2n Clog2n),其中 C = l o g 3 2 C=log_32 C=log32 是一个常量。基于我们前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,即 O(Cf( n n n)) = O(f( n n n))。所以,O( l o g 2 n log_2n log2n) 就等于 O( l o g 3 n log_3n log3n)。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O( l o g n logn logn)。

同理,由乘法法则可知,如果一段代码的时间复杂度是 O( l o g n logn logn),我们循环执行 n 遍,时间复杂度就是 O( n l o g n nlogn nlogn) 了。
3. O( m + n m+n m+n)、O( m ∗ n m*n mn)
这种情况中代码的复杂度由两个数据的规模来决定。

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 m+n m+n)。

针对这种情况,原来的加法法则就不正确了,我们需要将加法规则改为: T 1 ( m ) + T 2 ( n ) = O ( f ( m ) + g ( n ) ) T1(m) + T2(n) = O(f(m) + g(n)) T1(m)+T2(n)=O(f(m)+g(n))。但是乘法法则继续有效: T 1 ( m ) ∗ T 2 ( n ) = O ( f ( m ) ∗ f ( n ) ) T1(m)*T2(n) = O(f(m) * f(n)) T1(m)T2(n)=O(f(m)f(n))

空间复杂度分析

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系

常见的空间复杂度就是 O(1)、O( n n n)、O( n 2 n^2 n2),像 O( l o g n logn logn)、O( n l o g n nlogn nlogn) 这样的对数阶复杂度平时都用不到。


总结

复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增长关系,可以粗略地表示,越高阶复杂度的算法,执行效率越低 。常见的复杂度并不多,从低阶到高阶有:O(1)、O( l o g n logn logn)、O( n n n)、O( n l o g n nlogn nlogn)、O( n 2 n^2 n2)。

复杂度排序

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值