看完终于明白什么是时间复杂度和空间复杂度了

307 阅读9分钟

我正在参加「掘金·启航计划」,这是我的第 3 篇文章。

放低心态、认真学习,机会总是留给不断努力的自己

有同学看了之前的文章,私信我不懂如何计算时间复杂度和空间复杂度,有的甚至不明白如何定义这个。本篇文章就简单阐述一下时间、空间复杂度分析。

其实,如果你想学习数据结构与算法,就必须要掌握时间、空间复杂度分析。我个人愚见,复杂度分析是算法学习的必要条件也是学习算法的精髓,学会了,数据结构和算法就会了一半。

1 为什么要进行复杂度分析?

有的 xdm 说,我通过监控就能看到每次代码执行的时间和内存占用情况。为什么需要做时间、空间复杂度分析呢?这两种哪个数据更加精准呢?

确实,通过监控查看到的算法执行效率这种是正确的,但是这种方法是有很大的局限性的。

1.1 依赖环境影响

运行结果依赖环境的硬件,如,同一份代码用 M1 和 M1 Pro 处理器来运行,M1 Pro 的结果会更快。换了其他环境也会不一样。

1.2 数据规模影响

在排序算法中,同一个排序算法,原始数据的有序度不一致也会导致时间会有不一样。若数据是已经排序好的,算法就不需要做任何排序操作,很显然时间也就很短。

若测试数据太少,测试的结果可能不能很准确的反应算法的性能。如,对于较少的数据排序,快速排序相对于插入排序就会要慢。

所以,我们在评估算法的执行效率的时候就需要抛开这些情况。时间、空间复杂度分析这种方法就是为了解决这类问题。

2 大O复杂度表示法

什么事算法的执行效率?简单来说就是算法的执行时间。我们在运行代码之前,就应该能评估出来执行的时间是多少。分析一下下方代码,我们求 1,2,3,4... n 的和。我们一起评估一下该代码需要的时间。

假设每行语句的执行时间是 unit_time。在这个之上,那我们分析一下上面这段代码总执行时间 T(n) 会是多少。

首先第 2、3 行,每行需要 1 个 unit_time 的时间,4、5 行循环了 n 遍,需要 2n * unit_time 个时间,所以这段代码需要执行的时间是 (2n + 2) * unit_time。

按照这个分析思路,我们再来看这段代码:

若每行执行时间是unit_time,那上面代码的总执行时间T(n)是多少?
2、3、4 行每个需要 1 个 unit_time 的执行时间, 5、6 行循环 n 遍,需要 2n unit_time,7、8行共执行了 n2n^2 遍。共需要 t(n) = (2 n2n^2 + 2n +3) unit_time。

尽管我们不知道unit_time的具体值,但是通过这两段代码执行时间的推导过程,我们可以得到一个非常重要的规律,那就是,所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。 我们可以把这个规律总结成一个公式。

上面这个公式。其中 T(n) 代表 执行代码需要的时间,n 表示数据规模的大小,f(n) 表示每行代码执行的总次数。f(n) 代表的是一个公式。公式中的 O 表示代码的执行时间 T(n) 与 f(n) 表达式成正比。

所以,第一个代码中 T(n) = O(2n+2),第二个代码中的 T(n)= O(2n 2^2 + 2n + 3)。这就是大 O 时间复杂度法,其中大 O 并不是表示代码真正的执行时间,而是代码随着数据规模增长的变化趋势。这就是时间复杂度。

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

3 时间复杂度分析

如何分析一段代码的时间复杂度呢?我一般是结合下面三种方法的。

3.1 执行多的代码块

上面说的大O这种复杂度表示方法只是表示数据规模增长的变化趋势。其中会忽略公式中的常量、系数、低阶。只要记录一个最大阶的量级就可以。所以,我们分析算法时间复杂度的时候,只需要关注循环次数最多的那一处代码,这段核心代码执行次数的n的量级,就是我们要分析的时间复杂度了。

还拿上面第一段代码来分析,加深理解。

从中可以看出,我们只需要关注 4、5 行代码即可,其中 2、3 行都是常量的执行时间,不会受到 n 的数据大小影响。对复杂度并没有影响。只需要重点分析循环部分。所以总的时间复杂度就是 O(n)。

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

可以先分析一下下面这段代码,看下我们是否想的一样

这段代码一个简单的求和。共三个部分,我们试着分析一下对应的时间复杂度。然后取一个量级最大的作为整个代码的复杂度。

2~6 行时间复杂度很明显是一个常量执行的时间,共循环了 100 次,与 n 的规模大小无关。

说明一下。这段代码哪怕循环 10000 次还是 1000000次,只要和 n 的数据规模无关,均可被认为是常量的执行时间。尽管对代码的执行时间会有影响,时间复杂度表示的是一个算法执行效率与数据规模增长的变化趋势。所以不管常量执行的时间多大,都被忽略不计。因为它没有影响到增长趋势。

我们接下来看下 711、1220 行对应的复杂度,其实上面已经说过了,是O(n) 和 O(n2)(n^2) 。想信大家一眼就能分析出来。结合一下,我们取最大量级。所以时间复杂度就是 O(n2)(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.3 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

我们看下下面这个乘法法则代码,分析下时间复杂度。

cal 函数,4~6行 时间复杂度为 O(n)。但是 f() 函数时间复杂度也是 O(n),cal 函数总的时间复杂度就是 T1(n) * T2(n) = O(n*n) = O (n2)(n^2)

所以乘法法则就是 假设T1(n) = O(n),T2(n) = O(n2),则T1(n) * T2(n) = O (n3)(n^3)

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

接下来,我们讲下常见的时间复杂度的案例吧。

4.1 O(1)

O(1) 其实就是常量级的时间复杂度表示方法。当然并不是指一行。上面说得例子就有,多行的时间复杂度也是O(1)。比如:

时间复杂度并不是 O(2),而是 O(1)。

记住。只要执行的时间不是随着 n 的数据规模增大的,这样的时间复杂度都为 O(1)。大多数情况下,只要代码中不存在循环语句、递归语 句,即使有成千上万行的代码,其时间复杂度也是Ο(1)

4.2 O(logn)O(nlogn)

对数求时间复杂度还是比较难的,上面例子中,i 从 1开始,每次循环 * 3。当大于 n 时,循环结束。其实可以看出这就是等比数列。变量 i 的取值就是一个等比数列。

其中当我们知道 k 多少时,就能知道执行的次数了。 通过 3k=n3^k = n 得出 k= log3n3^n。所以时间复杂度就是O(log3n3^n)。

实际上,不管是以3为底、以4为底,还是以5为底,我们可以把所有对数阶的时间复杂度都记为 O(logn)。 有人会问为什么?其实很简单。 因为对数是可以互相转换。log3n就等于log32log2nlog3^2 * log2^n,所以 O(log3n)=O(Clog2n)O(log3^n) = O(C * log2^n),其中 C= log32log3^2是一个常量。所以我们就可以忽略底了,就可以表示为 O(logn)。

如果 O(logn) 知道了,那么 O(nlogn) 就简单了。首先,O(nlogn) 也是一种非常常见的算法时间复杂度。如,归并排序、快速排序的时间复杂度都是 O(nlogn)。 上面讲到的乘法法则,如果一段代码的时间复杂度是 O(logn),我们循环执行n遍,时间复 杂度就是 O(nlogn) 了。

5 空间复杂度分析

我们上面都在说时间复杂度分析和大O表示法,如果都理解了,那么这空间复杂度分析就简单了。

首先,时间复杂度表示算法执行时间与数据规模之间的增长关系。空间复杂度表示算法的存储空间与数据规模之间的增长关系。看下面一段代码

image.png

我们可以看出第 2 行时常量,可以忽略,第 3 行申请了一个大小为 n 的 int 切片( Go 中叫做切片,可以理解成数组),除了这些,剩下的代码都没有占用空间了,所以空间复杂度为 O(n).

空间复杂度分析比时间复杂度分析要简单很多。 所以,这里就讲这么多了!

6 总结

复杂度包含时间、空间复杂度,主要用于分析算法执行效率与数据规模直接的增长关系。在日常中,往往越高级复杂度的算法,执行效率会偏低。常见的有O(1) > O(logn) > O(n) > O(nlogn) > O(n2) O( n^2 )

image.png

欢迎点赞、评论、收藏哈