在开发中,咱们会常常听到关于时间复杂度、空间复杂度相关词汇,若是你没有这方面的知识,你确定会一脸懵逼。那什么是时间复杂度、空间复杂度还有咱们又怎么去分析?首先咱们先来弄清楚咱们为何须要作复杂度分析。java
真实的时间复杂度、空间复杂度咱们须要在机器上执行咱们编写的代码,才能统计出咱们的代码这这个环境下的真实时间复杂度、空间复杂度。这种方法统计出来的结果很是准确,可是极限性也很是大。算法
测试环境中硬件的不一样会对测试结果有很大的影响。好比,拿一样一段代码,分别用 Intel Core i9 处理器和 IntelCore i3处理器来运行,不用说,i9处理器要比 i3 处理器执行的速度快不少。还有,好比本来在这台机器上 a 代码执行的速度比 b 代码要快,当换到另外一台机器上时,可能 会有截然相反的结果。数组
好比排序算法,对同一个排序算法,待排序数据的有序度不同,排序的执行时间就会有很大的差异。极端状况下,若是数据已是有序的,那排序算法不须要作任何操做,执行时间就会很是短。除此以外,若是测试数据规模过小,测试结果可能没法真实地反应算法的性能。好比,对于小规模的数据排序,插入排序可能反倒会比快速排序要快!微信
那能不能不用具体的测试数据来测试,就能够粗略地估计算法的执行效率的方法?答案是确定的,也就是咱们的主题时间复杂度、空间复杂度的分析,通常用大O公式来进行代码时间复杂度、空间复杂度的预测分析。数据结构
1 public void sum(int n) {
2 int sum = 0;
3 for (int i = 1; i <= n; i++) {
4 sum += i;
5 }
6 System.out.println(sum);
7 }
复制代码
假设每行代码的执行时间为time
,咱们来粗略估计一下这段代码块的执行总时间,第二行代码执行须要1个time
,第三、4行代码都执行了n遍,因此须要的时间为n
* time
,第6行代码执行的时间为1个time
,因此整个代码块的执行时间为(2n+2) * time
,若是咱们用 T(n) 函数来表示代码的执行总时间,那么T(n) = (2n+2) * time
能够看出 T(n) 与 n 成正比关系。这就能够用大O公式来表示。函数
大O公式:T(n)=O(f(n))性能
T(n) 表示代码执行的时间;n 表示数据规 模的大小;f(n) 表示每行代码执行的次数总和。 O 表示代码的执行时间 T(n) 与 f(n) 表达式成正比。这就是大 O 时间复杂度表示法。大 O时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随 数据规模增加的变化趋势,因此,也叫做渐进时间复杂度(asymptotic time complexity),简称时间复杂度。 当 n 很大时,你能够把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增 长趋势,因此均可以忽略。咱们只须要记录一个最大量级就能够了,因此咱们示例中的总时间就能够用 T(n) =O(n) 来标识。学习
前面咱们已经了解了大O公式,那咱们如何进行代码的复杂度分析呢?能够从如下三个准则入手。测试
咱们知道用大O公式来表示时间复杂度的时候,忽略了常量、低阶、系数等,咱们只须要关注循环执行次数最多的那一段代码就能够了,这段代码执行的次数 n 就是整个代码块的时间复杂度。为了方便咱们理解这段话,咱们用上面的代码来分析一下,增强理解。spa
1 public void sum(int n) {
2 int sum = 0;
3 for (int i = 1; i <= n; i++) {
4 sum += i;
5 }
6 System.out.println(sum);
7 }
复制代码
代码 二、6 都是常量级的执行时间,对时间复杂度没有影响,执行最多的代码是 三、4 两行代码,一共执行了 n 次,因此整个代码块的时间复杂度为 O(n)
1 public void test(int n) {
2 for (int i = 0; i < n; i++) {
3 System.out.println(i);
4 }
5 for (int i = 0; i < n; i++) {
6 for (int j = 0; j < n; j++) {
7 System.out.println(i * j);
8 }
9 }
10 }
复制代码
这段代码有两个时间复杂度,2-4 行代码的时间复杂度 T1(n) = O(n),5-8 行代码的时间复杂度为 T2(n) = O(n²)。当 n 无限大的时候,T1(n) 对整个代码块的时间复杂度的影响是能够忽略的,整个代码块的时间复杂度就为 T2(n)=O(n²),换句话说总的时间复杂度就等于量级最大的那段代码的时间复杂度。那咱们将这个规律抽象成公式就是: 若是 T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)+T2(n)=max(O(f(n)),O(g(n)))=O(max(f(n),g(n)))
1 public void test(int n) {
2 for (int i = 0; i < n; i++) {
3 for (int j = 0; j < n; j++) {
4 System.out.println(i * j);
5 }
6 }
7 }
复制代码
这段代码中第二行代码的复杂度为T1(n)=O(n),第3行代码块的T2(n)=O(n),第四行代码的复杂度为O(1)能够忽略,由于这段代码是循环,因此时间复杂度T(n) = T1(n) * T2(n) = O(n * n) = O(n²)
经过上面的三种准则就可以很好的分析代码的时间复杂度,虽然代码千奇百怪,可是常见的复杂度量级并很少,咱们来看看几种常见时间复杂度。
上面从上至下依次的时间复杂度愈来愈大,执行的效率愈来愈低,咱们来看看几种常见复杂的案例。
常数阶很是简单,就是没有变量,都是常量,那样代码的时间复杂度就为 O(1)。下面两段代码的时间复杂度都为 O(1)。
public void test(){
for (int i = 0;i <100;i++){
System.out.println(i);
}
}
public void sum(int n) {
int i = 2;
int j = 6;
int sum = i + j;
System.out.println(sum);
}
复制代码
i=1;
while (i <= n) {
i = i * 2;
}
复制代码
从上面代码能够看到,在while循环里面,每次都将 i 乘以 2,乘完以后,i 距离 n 就愈来愈近了。咱们试着求解一下,假设循环x次以后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n 也就是说当循环 log2^n 次之后,这个代码就结束了。所以这个代码的时间复杂度为:O(logn)
public void sum(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
System.out.println(sum);
}
复制代码
这段代码,for循环里面的代码会执行n遍,所以它消耗的时间是随着n的变化而变化的,所以这类单层循环的代码均可以用O(n)来表示它的时间复杂度。
public void test1(int n) {
for (int i = 0; i < n; i++) {
int m = 0;
while (m < n) {
m *= 2;
}
}
}
复制代码
线性对数阶O(nlogN) 其实很是容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了 O(nlogN)。
public void test(int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println(i * j);
}
}
}
复制代码
平方阶 O(n²) 就是两层循环,每层循环的次数是一个变量,这种的两层循环的代码的时间复杂度均可以用 O(n²) 表示。立方阶O(n³)、K次方阶O(n^k)跟这个同样,只是多层循环而已。
上面就是经常使用时间复杂度的案例,在时间复杂度分析中,你也许还据说过最好状况时间复杂度
、最坏状况时间复杂度
、平均状况时间复杂
,那这些又是什么呢?先来看一段案例。
public int find(int[] array, int n, int x) {
int i = 0;
int pos = -1;
for (; i < n; ++i) {
if (array[i] == x) {
pos = i;
break;
}
}
return pos;
}
复制代码
上面的代码是在数组中找出值等于x
的下标。根据上面学习的大O公式,这段代码的时间复杂度为 O(n),可是这段代码的时间复杂度必定为O(n)吗?不必定的,若是数组中第一个元素正好是要查找的变量x,那就不须要继续遍历剩下的 n-1 个数据了,那时间复杂度就是O(1)。但若是数组中不存在变量x,那就须要把整个数组都遍历一遍,时间复杂度就成了O(n)。因此,不一样的状况下,这段代码的时间复杂度是不同的。
为了表示代码在不一样状况下的不一样时间复杂度,就须要引入上面提到的三个概念:最好状况时间复杂度、最坏状况时间复杂度和平均状况时间复杂度。
最好状况时间复杂度就是,在最理想的状况下,执行这段代码的时间复杂度。就像上面的示例,在最理想的状况下,要查找的变量x正好是数组的第一个元素,这个时候对应的时间复杂度就是最好状况时间复杂度 O(1)。
最坏状况时间复杂度就是,在最糟糕的状况下,执行这段代码的时间复杂度。就像上面的示例,若是数组中没有要查找的变量x,须要把整个数组都遍历一遍才行,因此这种最糟糕 状况下对应的时间复杂度就是最坏状况时间复杂度 O(n)。
最好状况时间复杂度和最坏状况时间复杂度对应的都是极端状况下的代码复杂度,发 生的几率其实并不大。为了更好地表示平均状况下的复杂度,就出现了平均状况时间复杂度
的概念。那平均状况时间复杂度如何分析呢?以上面的那段代码为例。 要查找的变量 x在数组中的位置,有 n+1 种状况:在数组的 0~n-1 位置中和不在数组中。把每种状况下,查找须要遍历的元素个数累加起来,而后再除以 n+1,就能够获得须要遍历的元素个 数的平均值,即:
在上面的学习中,咱们知道时间复杂度的大O标记法中,能够省略掉系数、低阶、常量,因此,我们把刚刚这个公 式简化以后,获得的平均时间复杂度就是 O(n)。
空间复杂度相对时间复杂度来讲就简单不少了,空间复杂度也不是用来计算程序实际占用的空间的。空间复杂度是对一个算法在运行过程当中临时占用存储空间大小的一个量度。空间复杂度比较经常使用的有:O(1)、O(n)、O(n²),咱们一块儿来看看这几种经常使用的空间复杂度。
空间复杂度 O(1) 说明临时开辟的内存空间跟变量n
没有关系,不会随着n
的变化而变化。例以下面这段代码。
public void sum(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
System.out.println(sum);
}
复制代码
虽然上面的这段代码有变量n
,可是在循环的时候并无开辟新的内存空间。因此这是的空间复杂度为 O(1)。
空间复杂度为 O(n) 说明在执行代码的过程当中,开辟的临时空间大小跟n
成正比的关系,例以下面这段代码.
public void array(int n) {
int[] array = new int[n];
for (int i = 1; i <= n; i++) {
array[i] = i;
}
}
复制代码
这段代码中新new
了一个大小为n
的array
数组,因此这段代码的空间复杂度为O(n)。
空间复杂度 O(n²) 就是在代码的执行过程当中新开辟了一个二维列表,以下面这段代码。
public void array(int n) {
int[][] array = new int[n][n];
for (int i = 1; i <= n; i++) {
for (int j=0;j<n;j++) {
array[i][j] = j;
}
}
}
复制代码
以上,就是对算法的时间复杂度与空间复杂度的分析,欢迎你们一块儿交流。
打个小广告,欢迎扫码关注微信公众号:「平头哥的技术博文」,一块儿进步吧。