01-算法的时间复杂度和空间复杂度-总结
算法的时间复杂度和空间复杂度-总结
1 什么是算法
1.1 什么是算法
- 算法是用于解决特定问题的一系列的执行步骤
// 计算 a 和 b 的值
public static int plus(int a, int b) {
return a + b;
}
// 计算 1 + 2 + 3 + ... + n 的和
public static int sum(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += i;
}
return result;
}
1.2 斐波那契数列 : fibonacci number
- 使用不同算法,解决同一个问题,效率可能相差非常大
- 比如:求第n个斐波那契数(fibonacci number)
/* * 斐波那契数列 : fibonacci number * 0 1 1 2 3 5 8 13 ... */ // 方式一: public static int fib1(int n) { if (n < 0) return 0; if (n <= 1) return n; return fib1(n - 1) + fib1(n - 2); } // 方式二: public static int fib2(int n) { if (n < 0) return 0; if (n <= 1) return n; int first = 0; int second = 1; for (int i = 0; i < n; i++) { second += first; first = second - first; } return second; }
1.3 计算算法的执行时间
- 当n = 45时, 计算函数所用时间
int num = 45;
// fib1 所用时间 : 0.44秒
TimeTool.check("fib1", new Task() {
@Override
public void execute() {
System.out.println(fib1(num));
}
});
// fib2 所用时间 : 0.0秒
TimeTool.check("fib2", new Task() {
@Override
public void execute() {
System.out.println(fib2(num));
}
});
2. 如何评判一个算法的好坏?
2.1 从数学上证明算法的正确性、可读性、健壮性
- 健壮性 : 对不合理输入的反应能力和处理能力
2.2 时间维度, 也就是时间复杂度(time complexity)
-
(1) 时间频度 : 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为 语句频度 或 时间频度。记为
T(n)
。 -
(2) 时间复杂度 : 在刚才提到的时间频度中,n称为 问题的规模,当n不断变化时,
时间频度T(n)
也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法中基本操作重复执行的次数是问题规模 n 的某个函数,用T(n)
表示,若有某个辅助函数f(n)
, 使得当n趋近于无穷大时,T(n) / f(n)
的极限值为不等于零的常数,则称 f(n) 是 T(n) 的同数量级函数。记作 T(n) = O(f(n)), 称 O(f(n)) 为算法的渐进时间复杂度,简称 时间复杂度。
2.3 空间维度, 也就是空间复杂度(space complexity)
-
类似于时间复杂度的讨论,一个算法的
空间复杂度
(Space Complexity)S(n)
定义为该算法所耗费的存储空间,它也是问题规模n的函数。渐近空间复杂度也常常简称为空间复杂度
。 -
空间复杂度(Space Complexity)
是对一个算法在运行过程中临时占用存储空间大小的量度
。 -
一个算法在计算机存储器上所占用的存储空间,包括三个方面
- 存储算法本身所占用的存储空间
存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。
- 算法的输入输出数据所占用的存储空间
算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变。
- 算法在运行过程中临时占用的存储空间
算法在运行过程中临时占用的存储空间随算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是“就地"进行的,是节省存储的算法;有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元。
- 如当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2(n));当一个算法的空间复杂度与n成线性比例关系时,可表示为0(n).若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
3 时间复杂度相关知识
3.1 大O表示法(Big O)
-
般用大 O 表示法来描述复杂度,它表示的是数据规模n对应的复杂度
-
忽略常数、系数、低阶
- 9>> O(1)
- 2n+3 >> O(n)
- n2 + 2n+6 >> O(n2)
- 4n3 + 3n2 + 22n+ 100 >> O(n3)
- 写法上,n3等价于n^3
-
注意:大 O 表示法仅仅是一种粗略的分析模型,是一种估算,能帮助我们短时间内了解一个算法的执行效率
3.2 对数阶的细节
- 对数阶一般省略底数
- 所以log2(n)、log(n) 统称为log(n)
3.3 常见的复杂度
- О(1) < О(lоgn) < О(n) < О(nlоgn) < О(n^2) < О(n^З) < O(2^n) < О(n!) < О(n^n)
- 可以借助函数生成工具对比复杂度的大小
4 如何计算一个算法的执行时间?
算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。而度量一个程序的执行时间通常有两种方法。
4.1 事后统计法
- 在
1.3
使用的方法就是事后统计法 - 该计算法的缺点
- 执行时间严重依赖硬件以及运行时各种不确定的环境因素
- 必须编写相应的测算代码
- 则试数据的选择比较难保证公正性
4.2 事前分析估算法, 常见的时间复杂度计算
4.2.1 常数阶O(1)
- 无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如
// 汇编指令
// 1
if (n > 10) {
System.out.println("n > 10");
} else if (n > 5) { // 2
System.out.println("n > 5");
} else {
System.out.println("n <= 5");
}
// 1 + 4 + 4 + 4
for (int i = 0; i < 4; i++) {
System.out.println("test");
}
// O(1)
- 上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
4.2.2 线性阶O(n)
// O(n)
// 1 + 3n
for (int i = 0; i < n; i++) {
System.out.println("test");
}
4.2.3 平方阶O(n²)
// 1 + 2n + n * (1 + 3n)
// 1 + 2n + n + 3n^2
// 3n^2 + 3n + 1
// O(n^2)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println("test");
}
}
4.2.4 对数阶O(log(n))
// 执行次数 = log2(n)
// O(logn)
while ((n = n / 2) > 0) {
System.out.println("test");
}
n 执行次数
4 2
6 2
8 3
16 4
执行次数 c
2^c = n
c = log(n)
4.2.5 线性对数阶O(nlog(n))
- 线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
// 一种情况
for (int i = 0; i < n; i++) {
while ((n = n / 2) > 0) {
System.out.println("test");
}
}
// 第二种情况
// 1 + 2*log2(n) + log2(n) * (1 + 3n)
// 1 + 3*log2(n) + 2 * nlog2(n)
// O(nlogn)
for (int i = 1; i < n; i = i * 2) {
// 1 + 3n
for (int j = 0; j < n; j++) {
System.out.println("test");
}
}
4.2.6 指数阶O(2^n)
- 就拿
斐波那契数列
的第一种方法来说
public static int fib1(int n) {
if (n <= 1) return n;
return fib1(n - 1) + fib1(n - 2);
}
当 n 无限大的时候, 执行次数变为 2^n
T(n) = O(2^n)
4.2.7 立方阶O(n³)、K次方阶O(n^k)
- 参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似。
4.2.8 多个数据规模的情况
// O(n + k)
for (int i = 0; i < n; i++) {
System.out.println("test");
}
for (int i = 0; i < k; i++) {
System.out.println("test");
}
4.2.9 斐波那契的线性代数解法-特征方程
5 空间复杂度 S(n)
既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。
空间复杂度比较常用的有:O(1)、O(n)、O(n²),我们下面来看看:
5.1 常数阶O(1)
- 如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
- 举例:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
- 代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
5.2 线性阶O(n)
int[] m = new int[n]
for(i = 1; i <= n; ++i) {
j = i;
j++;
}
- 这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)