时间复杂度
复杂度 | 可能对应的算法 | 备注 |
---|---|---|
O(1) | 位运算 | 常数级复杂度,一般面试中不会有 |
O(logn) | 二分法,倍增法,快速幂算法,辗转相除法 | |
O(n) | 枚举法,双指针算法,单调栈算法,KMP算法,Rabin Karp,Manacher's Algorithm | 又称作线性时间复杂度 |
O(nlogn) | 快速排序,归并排序,堆排序 | |
O(n^2) | 枚举法,动态规划,Dijkstra | |
O(n^3) | 枚举法,动态规划,Floyd | |
O(2^n) | 与组合有关的搜索问题 | |
O(n!) | 与排列有关的搜索问题 |
在面试中,经常会涉及到时间复杂度的计算。当你在对于一个问题给出一种解法之后,面试官常会进一步询问,是否有更优的方法。此时就是在问你是否有时间复杂度更小的方法(有的时候也要考虑空间复杂度更小的方法),这个时候需要你对常用的数据结构操作和算法的时间复杂度有清晰的认识,从而分析出可优化的部分,给出更优的算法。
例如,给定一个已经排序的数组,现在有多次询问,每次询问一个数字是否在这个数组中,返回True or False.
方法1: 每次扫描一遍数组,查看是否存在。
这个方法,每次查询的时间复杂度是: O(n)方法2:由于已经有序,可以使用二分查找的方法。
这个方法,每次查询的时间复杂度是: O(logn)方法3:将数组中的数存入Hashset。
这个方法,每次查询的时间复杂度是: O(1)
可以看到,上述的三种方法是递进的,时间复杂度越来越小。
在面试中还有很多常见常用的方法,他们的时间复杂度并不是固定的,都需要掌握其时间复杂度的分析,要能够根据算法过程自己推算出时间复杂度。
T 函数推导法
我们介绍一种时间复杂度的推导方法:T函数推导法
比如二分法。二分法是每次通过 O(1) 的时间将规模为 n 的问题降低为规模为 n/2 的问题。
这里我们用 T(n) 来表示规模为 n 的问题在该算法下的时间复杂度,那么我们得出推导公式:
T(n)=T(n/2)+O(1)
我们来逐个说明一下这个公式的意义。
首先 T 代表的是 Time Complexity, nnn 代表的是问题规模(二分法里就是数组的大小)。
那么 T(n) 代表的就是:求处理问题规模为n的数据的时间复杂度是多少。注意这里是一个问句,不是一个答案。
T(n) 根据算法的不同可以是 O(n) 也可以是 O(nlogn) 或任何值,而 O(n) 就是 O(n)。
然后 O 代表的是时间复杂度。O(1) 就意味着,你大概用一个 if 语句,或者简单的加加减减,就可以完成。O 在这里的意思是数量级约等于。在 O 的世界里,我们只考虑最高项是什么,不考虑系数和常数项。比如:
O(100n)=O(n)
O(n^2+n)=O(n^2)
O(2^n+n^2+10)=O(2^n)
如何推导 T 函数
我们可以使用不断展开的方法进行推导:
T(n) = T(n/2) + O(1)
= T(n/4) + O(1) + O(1)
= T(n/8) + O(1) * 3
= T(n/16) + O(1) * 4
...
= T(1) + O(1) * logn
= O(logn)
在时间复杂度的领域里,有如下的一些性质:
T(1)=O(1) // 解决规模为1的问题通常时间复杂度为O(1)。这个不100%对,但是99.9%的情况下都是如此。
k∗O(n)=O(kn)
O(n)+O(m)=O(n+m)
上面的方法,是采用 T 函数展开的方法,将二分法的时间复杂度最终用 O(...) 来表示。
空间复杂度
类似于时间复杂度,空间复杂度就是衡量算法运行时所占用的临时存储空间的度量。也是一个与问题规模n有关的函数。
算法所占用的空间主要有三个方面:算法代码本身占用的空间、输入输出数据占用的空间、算法运行时临时占用的空间。
其中,代码本身和输入输出数据占用的空间不是算法空间复杂度考虑的范围内,空间复杂度只考虑运行时临时占用的空间,又称为算法的额外空间(Extra space)。
临时占用的空间包括:
- 为参数列表中形参变量分配的空间
- 为函数体中局部变量分配的空间
(如果是递归函数,需要将上述两部分占用空间的和乘以递归的深度,这是堆栈空间)
在递归函数中,除了变量和数组所开辟的临时空间以外,还有一个空间我们需要纳入考虑,就是递归时占用的栈空间(Stack)。
递归函数需要保存当前的环境,以便在递归返回的时候能够还原之前的现场。因此递归的深度越深,所要占用的栈空间越大。当空间超出一定范围的时候就会出现程序爆栈(Stack Overflow)的情况。
很多博客文章中会写堆栈空间与递归调用的次数成正比,这个是不完全正确的,应该是与递归的深度成正比(此处只讨论单线程)。
因为递归在返回到上一层的时侯,就会将本层的空间释放,因此占用的栈空间不会比最深的一次调用所占用的空间更多。
在面试中,很多时候面试官给出的问题会附带一个“不能使用多余的空间”这样的要求。很多时候这是在要求你的空间复杂度只能是O(1)的,也就是你只能开几个辅助变量,而不能开大数组。
其他常见的还有,分析一下你的算法空间复杂度,寻找空间复杂度更优的解法等。
一般来说,算法占用的时间和空间会是两个互相平衡的元素,有的时候我们牺牲空间来换取时间,有的时候我们牺牲时间来换取空间。
在面试中常见算法的空间复杂度:
快速排序: 最优:O(logn),最差:O(n)
二分查找: O(1)
最短路(Dijkstra)算法: O(V) (V表示点集大小)
大部分的空间复杂度计算方法
累加下面两个部分的内容即是你代码的空间复杂度:
- 你的代码里开辟了多少新的空间(new 了多少新的内容出来)
- 你的递归深度 * 递归函数内部的参数和局部变量所占用的空间
以快速排序为例子
快速排序的思路如下:
1.选择一个基准元素,将原数组分为两部分,左边部分小于该元素,右边部分大于该元素。
2.分别递归处理左边和右边。
最好情况:
每次都能恰好将数组分成左右相同长度的两部分,需要的递归深度是:lgnlgnlgn,每次将数组分成两部分时,我们选择不使用辅助数组,在原数组上“就地”处理,所以每层的空间是O(1)
因此总的复杂度是:O(logn)最差情况:
每次都将数组分成长度差最大的两部分,即一边只有一个元素,其余的在另外一边,最大深度为:n, 因此空间复杂度为:O(n)
有些递归算法的空间复杂度是稳定的,不会退化,快排的递归深度与其每次选择的“基准值”有很大关系,因此存在退化的情况。
内存中的栈空间与堆空间
分解质因数
以 sqrt(n)为时间复杂度的算法并不多见,最具代表性的就是分解质因数了。
题目描述
http://www.lintcode.com/problem/prime-factorization/
具体步骤
- 记up=sqrt(n)作为质因数k的上界, 初始化k=2
- 当k<=up 且 n不为1 时,执行步骤3,否则执行步骤4。
- 当n被k整除时,不断整除并覆盖n,同时结果中记录k,直到n不能整出k为止。之后k自增,执行步骤2。
- 当n不为1时,把n也加入结果当中,算法结束。
几点解释
- 不需要判定k是否为质数,如果k不为质数,且能整出n时,n早被k的因数所除。故能整除n的k必是质数。
- 为何引入up?为了优化性能。当k大于up时,k已不可能整除n,除非k是n自身。也即为何步骤4判断n是否为1,n不为1时必是比up大的质数。
- 步骤2中,也判定n是否为1,这也是为了性能,当n已为1时,可早停。
代码
1 public List<Integer> primeFactorization(int n) {
2 List<Integer> result = new ArrayList<>();
3 int up = (int) Math.sqrt(n);
4
5 for (int k = 2; k <= up && n > 1; ++k) {
6 while (n % k == 0) {
7 n /= k;
8 result.add(k);
9 }
10 }
11
12 if (n > 1) {
13 result.add(n);
14 }
15
16 return result;
17 }
复杂度分析
最坏时间复杂度 O(sqrt{n})。当n为质数时,取到其最坏时间复杂度。
空间复杂度 O(log(n)),当n质因数很多时,需要空间大,但总不会多于O(log(n))