1. 算法复杂度
1.1 算法复杂度概念
算法,顾名思义就是计算方法,是输入和输出的中间商。算法复杂度,就是用来衡量算法优劣的。
1.2 记法
算法复杂度常用大 O O O标识法记录, O O O表示数量级, O ( f ( n ) ) O(f(n)) O(f(n))表示取问题规模为 n n n的数量计算式子的数量级,这个数量级直接决定当 n n n增长时,算法以什么样的数量级增长。通常常数都化为 1 1 1,通常有下面通识:
- O ( 3 ) = O ( 1 ) O(3)=O(1) O(3)=O(1) 常量变为 1 1 1
- O ( 2 n ) = O ( n ) O(2n)=O(n) O(2n)=O(n) 系数变为 1 1 1
- O ( n 2 + n ) = O ( n ) O(n^2 + n)=O(n) O(n2+n)=O(n) 取幂次最高的
1.3 算法复杂度包含
世界无非就是时间空间,算法也不例外,关系到时间和空间两个维度。算法复杂度,大方面来看分为时间复杂度和空间复杂度。
2. 时间复杂度
2.1 定义
是算法运行所消耗的时间多少的度量,用 T ( n ) T(n) T(n)表示,有
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
2.2 包含类型
2.2.1 最好情况时间复杂度
是在最理想情况下的时间复杂度,可以说是花时间最少的。例如冒泡排序,如果本身就是已经排好序的了,走第一个冒泡,算法就可以完成了。
2.2.2 最坏情况时间复杂度
是最坏情况下的时间复杂度,可以说是花时间最多的。例如冒泡排序,如果本身是倒序排好的,则算法将走到最后一个冒泡才能完成。
2.2.3 平均情况时间复杂度
这种情况考虑了各种情况的概率,然后计算每种情况的次数,按权重取个平均值。例如,要在n个不同数中找到某一个数,显然这个数在每个位置的概率一样为 1 n \dfrac{1}{n} n1,在第一个位置只需要检查一次,在第二个位置需要检查两次,依次类推,我们计算出平均次数如下:
T ( n ) ‾ = 1 n × 1 + 1 n × 2 + . . . + 1 n × n = 1 n × ( 1 + 2 + . . . + n ) = n + 1 2 \overline{T(n)}=\dfrac{1}{n}\times1+\dfrac{1}{n}\times2+...+\dfrac{1}{n}\times{n}=\dfrac{1}{n}\times(1+2+...+n)=\dfrac{n+1}{2} T(n)=n1×1+n1×2+...+n1×n=n1×(1+2+...+n)=2n+1
从而可知道该算法平均情况复杂度为 O ( n ) O(n) O(n)。
2.2.4 均摊时间复杂度
这种复杂度使用场景较为特殊,比如对一个数据进行连续操作,在大多数情况下复杂度较低,特殊情况下复杂度较高,而这组操作存在前后连贯的时序关系。我们将这组操作放在一起,将复杂度高的均摊到复杂度低的上面。一般均摊时间复杂度等于最好情况时间复杂度。
2.3 常见时间复杂度
2.3.1 常数阶 O ( 1 ) O(1) O(1)
public int sum(int i, int j) {
i++;
j++;
return i + j;
}
可以看到这个求和算法经过了若干步就返回了,并没有循环等复杂结构,经过常数步骤后便完成,所以该算法为常数阶。
2.3.2 对数阶 O ( l o g n ) O(log_{}{n}) O(logn)
public int getStep(int n) {
int i = 1;
while (i < n) {
i = i * 2;
}
return i;
}
这个可以看成是从步长 1 1 1开始,每一步是原来步长的两倍,知道这个步长超过给定数。我们可以看到也就是:假定经过 x x x步,要达到 2 x > = n 2^x >= n 2x>=n,也就是 x > = l o g 2 n x>=log_{2}{n} x>=log2n,即 x = ⌈ l o g 2 n ⌉ x=\lceil log_{2}{n} \rceil x=⌈log2n⌉,所以该算法为对数阶,注意这种算法底数可以为 2 2 2,也可以为其他正整数,统称对数阶 O ( l o g n ) O(log_{}{n}) O(logn)。
2.3.3 线性阶 O ( n ) O(n) O(n)
public int[] getArray(int n) {
int[] result = new int[n];
for (int i = 0; i < n; ++i) {
result[i] = i;
}
return result;
}
这个算法可以看到该算法经过了一个循环,次数为 n n n,故为线性阶。
2.3.4 线性对数阶 O ( n l o g n ) O(nlog_{}{n}) O(nlogn)
public void doGetStep(int n) {
for (int m = 0; m < n; m++) {
int i = 1;
while (i < n) {
i = i * 2;
}
}
}
通过上面的对数阶分析我们知道,我们执行 n n n次对数阶,当然执行次数就为 O ( n l o g n ) O(nlog_{}{n}) O(nlogn),为线性对数阶。
2.3.5 平方阶 O ( n 2 ) O(n^2) O(n2)
public int[][] getArrays(int n) {
int[][] result = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
result[i][j] = i * j;
}
}
return result;
}
这个算法可以看到该算法经过了两个循环,每个循环次数为 n n n,总体次数为 n 2 n^2 n2,故为平方阶。
2.3.6 K方阶 O ( n k ) O(n^k) O(nk)
通过以上分析不难发现,经过 n n n个循环,每次为 n n n,则算法时间复杂度就为 O ( n k ) O(n^k) O(nk)。
2.4 不常见时间复杂度
2.4.1 指数阶 O ( n k ) O(n^k) O(nk)
public int fibonacci(int n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
我们来看这个问题,这是著名的fibonacci问题的递归解法,网上通用的时间复杂度是 O ( 2 n ) O(2^n) O(2n),是通过树形来证明的,但是有些不严谨,因为有些叶子节点是不足够的,正确时间复杂度应该是 O ( ( 1 + 5 2 ) n ) O((\dfrac{1+\sqrt{5}}{2})^n) O((21+5)n),有比较简明的证明方法,这里不过多介绍。
2.4.2 阶乘阶 O ( n ! ) O(n!) O(n!)
public void doFactorial(int n) {
if (n == 1) {
return;
}
for (int i = 0; i < n; i++) {
doFactorial(n - 1);
}
}
我们看这个算法,第一循环执行 n n n次,第二循环 n − 1 n-1 n−1次,得到次数为 n × ( n − 1 ) × . . . × 2 × 1 n\times(n-1)\times...\times2\times1 n×(n−1)×...×2×1,即算法复杂度为 O ( n ! ) O(n!) O(n!),这种算法不常用,不多讲解。
3. 空间复杂度
3.1 定义
是算法运行所占用的存储空间大小的量度,用 S ( n ) S(n) S(n)表示,有:
S ( n ) = O ( f ( n ) ) S(n)=O(f(n)) S(n)=O(f(n))
该存储空间包含:
- 算法程序所占的存储空间
- 初始数据所占的存储空间
- 执行过程中所需要的额外空间
3.2 常见空间复杂度
3.2.1 常数阶 O ( 1 ) O(1) O(1)
public int sum(int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += i;
}
return total;
}
可以看到该算法用到了n、total、i这几个变量,不随规模变化而改变,因此空间复杂度为一个常量,因此该空间复杂度属于常数阶。
3.2.2 线性阶 O ( n ) O(n) O(n)
public int[] getArray(int n) {
int[] result = new int[n];
for (int i = 0; i < n; ++i) {
result[i] = i;
}
return result;
}
注意到该算法用了n、result、i这几个变量,总数为 n + 2 n+2 n+2,因此该空间复杂度属于线性阶。
3.2.1 平方阶 O ( n 2 ) O(n^2) O(n2)
public int[][] getArrays(int n) {
int[][] result = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
result[i][j] = i * j;
}
}
return result;
}
注意到用了n、result、i这几个变量,总数为 n 2 + 2 n^2 + 2 n2+2,因此该空间复杂度属于平方阶。