简述
相信有很多小伙伴都知道数据结构和算法,也都看过学过数据结构和算法。当然我也不例外,准备入坑了,感受下程序的奥妙。在我们学习编写数据结构和算法之前,我们应该先学会如何去分析和总结一个算法的优缺点。这时我们会用到数据结构和算法的复杂度分析(时间复杂度、空间复杂度),通过这个复杂度的分析来初略的估计一个算法的好坏。
什么是复杂度
- 复杂度分为时间复杂度和空间复杂度,是评估数据结构与算法性能的两个维度(时间和空间)。
- 复杂度是用来描述算法的运行时间或者是占用的空间与数据规模之家的一种增长关系的趋势。
- 数据结构和算法是让代码更好的利用计算的空间和运行起来更快,提高代码的运行效率。
为什么要用复杂度分析来估算一个算法的好坏
在开发中,我们也许做过有时想知道一段代码的执行时间,比如一个接口的请求时间。我们通常会用记录开始和结束的两个时间点来进行计算得到代码的运行时间,这样我们也达到了估算一段代码运行效率的效果。有时也会通过统计监控机制去得到算法的运行时间和占用内存的大小。那可能就有人问了我们为什么要这个复杂度的分析?其实主要有以下原因:
- 一段代码在不同的机器上运行,所需要的时间不一样,受限于硬件的影响
- 一段代码的运行还受到数据多少的影响,就拿循环来说,数据越多需要的时间越多
所以,我们要估算一个算法的好坏应该去除这些影响因素,大 O 复杂度表示法就登场了,就是用来粗略表示数据结构和算法好坏的一种方式。
什么是大 O 表示法
其实就是一个复杂度表示的方法,或者可以说是一种增长关系。因为运行每一行代码都需要一定的时间,我们可以粗略的给定一个固定值,可以知道算法执行的时间和每行代码执行的时间成正比用公式可以简单表示为:
1 | T(n) = O(f(n)) |
其中 T(n)表示算法的总运行时间,f(n)表示每行代码执行的时间,所以总的时间受到 n 的影响。由于复杂度表示的是一种增长变化的趋势所以对于常量、低阶以及系数等不产生决定性影响的可以忽略,所以在做复杂度分析的时候可以忽略掉一部分数据。
时间复杂度分析和常见的种类
分析方法
- 只关注运行次数最多的一段代码。比如一个循环假设为 n,那么这行代码就执行了 n 次那么就可以粗略算做时间复杂度为 O(n)
- 如果有多段代码耗时,可以以最大的耗时作为时间复杂度,因为最耗时的代码都执行完了,那耗时少的肯定也执行完了。比如一段代码循环 n 次,另一段代码嵌套循环 n 次,那两段代码的时间复杂度就是 O(n)和$O(n^2)$,所以总体的时间复杂度就是$O(n^2)$
- 如果有嵌套的代码,那时间复杂度可以以乘积来表示,比如嵌套的循环都为 n 次,那总体的复杂度就是$O(n^2)$
常见的时间复杂度
- 常量阶(O(1))
- 对数阶(O(logn))
- 线性阶(O(n))
- 线性对数阶(O(long))
- 平方阶($O(n^2)$)
- 立方阶($O(n^3)$)
- n 次方阶($O(n^n)$)
- 指数阶($O(2^n)$)
- 阶乘阶(O(!n))
O(1)
这个表示的是常量阶,并不是一行代码执行的时间复杂度,如果有 3 行代码执行,那么它的时间复杂度还是 O(1)而不是 O(3)。时间复杂度的一种表示方法,总体来说就是如果代码运行的时间不受数据大小 n 的影响那时间复杂度就是 O(1)。
O(logn)、O(nlogn)
如果一个循环 n 次,每次是 i*2。那么 i 的值就是 2 的次方幂,只要算出 2 的次方数,那就是这个循环的复杂度,比如:
1 | var i=1; |
可以粗略算出$k=log_2n$,所以就是 O(logn)的时间复杂度。O(nlogn)的复杂度就是复杂度为 O(logn)的代码执行了 n 遍,就得到了这个 O(nlogn)复杂度。
O(m+n)、O(m*n)
如果有两段代码分别执行,我们能判断出哪个耗时长那就直接取最长的时间为复杂度,但是有时两段代码我们不能确定谁耗时长,所以就不能简单以长的为准那就可以用 O(m+n)来表示其复杂度。如果为嵌套执行的话时间复杂度还是 O(m*n)
常见的空间复杂度
空间复杂度是表示占用计算机内存空间的表示,如果定义一个常量,那就是 O(1)的空间复杂度。常见的有:
- O(1)
- O(n)
- $O(n^2)$
像 O(logn)、O(nlogn)这样的对数阶复杂度平时都用不到
小结
总体而言基本可以用以上方法进行简单的分析,得到我们代码的复杂度。复杂度从低到高:从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、$O(n^2)$,我们只有掌握了复杂度的分析才能大概判断一个算法的优劣,有助于我们优化代码的性能