算法时间、空间复杂度分析
1. 为什么需要复杂度分析
解决同样的问题有多种算法,那么如何知道算法的执行效率呢?复杂度分析不依赖于代码执行环境,通过复杂度分析就可以粗略的估计代码 (算法) 的执行效率及空间占用,指导我们写出最优的代码、优化执行效率低的代码。
2. 大O复杂度表示法
大O复杂度表示法表示程序的执行时间或占用空间随数据规模的增长趋势,平时所说得算法的时间、空间复杂度通常都使用大O复杂度表示法表述。
2.1 大O时间复杂度
大O时间复杂度表示代码执行时间随数据规模增长的变化趋势,公式:T(n)=O(f(n)),下面用一段简单的代码分析代码的时间复杂度,并使用大O表示法表述:
public int sum(int n) {
int sum = 0;
int i = 0;
for(; i <= n; i++) {
sum += i;
}
return sum;
}
我们假设总的执行时间为T(n),每行代码执行时间都是一样的,为单位时间unit_time(尽管每行代码实际执行时间不一样,但我们只是的粗略估计), 那第2、3行代码需要2*unit_time的时间,4、5行执行了n次,需要2n*unit_time的时间,那么代码执行的总时间为**(2n + 2)unit_time*,用公式表示如下:
T(n) = (2n + 2) * unit_time;
从公式可以得出结论,代码的执行时间T(n)与每行代码执行次数中的n成正比,根据大O表示法公式:T(n)=O(f(n)), n表示数据规模的大小,f(n)表示每行代码执行的次数总和(2n + 2),所以上述公式转换为大O时间复杂度表示为:
T(n) = O(2n + 2);
大O时间复杂度并不表示代码执行的真正时间,而是表示代码执行时间随数据规模增长的变化趋势,也叫渐进时间复杂度,简称时间复杂度。当数据规模n趋近于无穷时,公式中的常量、低阶、系数并不能左右增长趋势,可以忽略,所以上述公式用时间复杂度表示为:
T(n) = O(n);
2.2 大O空间复杂度
大O空间复杂度表示算法的存储空间与数据规模之间的增长关系,公式:S(n)=O(f(n)),大O空间复杂度也叫渐进空间复杂度,简称空间复杂度。空间复杂度是指算法除了数据本身所占用的空间外,算法执行还需额外申请存储空间,额外申请的空间就是算法的时间复杂度。下面用一段简单的代码分析代码的空间复杂度及大O空间复杂度表示法:
public int[] sort(int[] m, int[] n) {
int i = m.length + n.length;
int[] result = new int[i];
Arrays.sort(result);
return result;
}
代码第2行申请一个空间存储变量i,但它是常量阶的,跟数据规模m和n没有关系,可以忽略,代码第3行创建了一个m+n长度的int类型数组,其他地方没有再申请额外的存储空间,所以该代码的空间复杂度是O(m+n)。
3. 如何进行复杂分析
3.1 时间复杂度分析
3.1.1 关注循环执行次数最多的代码段
时间复杂度公式表示中, 我们通常忽略公式中的常量、低阶、系数, 只记录最大两阶量级即可, 因此我们只需要重点关注循环次数最多并且与数据规模n有关系的代码即可。
public void example(int n) {
//变量定义
int a = 0, b = 10, c = 100;
for(int i = 0; i < 10; i++) {
//代码
}
for(int i = 0; i < n; i++) {
//代码
}
for(int i = 0; i < 10000; i++) {
//代码
}
}
第3、4、5、10、11行代码需要常量级的执行时间,与数据规模n无关,所以对复杂度没有影响。循环最多的代码是6、7行代码,所以重点分析,两行代码总共执行了n次,所以时间复杂度是O(n)。
3.1.2 加法法则:总复杂度等于量级最大代码段的复杂度
加法公式:T(n) = T1(n) + T2(n) = O(f(n)) + O(g(n)) = O(Max(f(n), g(n)));
public void example(int n) {
for(int i = 0; i < n; i++) {
//代码
}
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++){
//代码
}
}
}
第2、3行和5、6、7行的时间复杂度分别是O(n)、O(n^2),我们只取其中量级最大的即可,代入公式为:T(n) = O(n) + O(n^2) = O(Max(n, n^2)) = O(n^2)。
3.1.3 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
乘法公式:T(n) = T1(n) * T2(n) = O(f(n)) * O(g(n)) = O(f(n) * g(n));
乘法法则落实到具体的代码中就是嵌套循环,举一个简单的例子:
public void example(int n) {
for(int i = 0; i < n; i++) {
f(n);
}
}
public void f(int n) {
for(int i = 0; i < n; i++) {
//代码
}
}
假设f()函数是一个普通的操作,那么2、3行的代码的时间复杂度T1(n) = O(n)。但f()并不是一个简单的操作,它的时间复杂度是T2(n) = O(n),所以整个算法的时间复杂度T(n) = T1(n) * T2(n) = O(n * n) = O(n^2)。
3.2 空间复杂度分析
空间复杂度分析相对简单, 只需要分析算法运行过程中所使用的辅助空间的大小,即除输入数据和程序之外的临时分配的额外空间。常见的空间复杂度有O(1)、O(n) 、O(n^2), 原地工作算法即算法所需辅助空间是常量,即O(1)。