什么是算法复杂度
算法复杂度,即算法在编写写成可执行程序后,运行时所需要的资源,资源包括时间资源和内存资源
通俗的说,就是执行一段代码所需要的资源(主要是花费的时间以及占用的空间)。
大 O 复杂度表示法
我们记一个算法的复杂度为O(n)
,表示数据规模为n的情况下,算法使用的时间和空间资源。(也可以理解O(n)
描述着,代码执行花费的时间和空间随着n增大变化的趋势)。
算法复杂度从时间上考虑是时间复杂度
(快不快),从空间上考虑是空间复杂度
(占的内存多不多)。
时间复杂度
1、O(1)
public void method1(int n){
int a = 5; // 执行1次
int b = 6; // 执行1次
int c = a + b;// 执行1次
System.out.println(c);// 执行1次
}
现在有如上的一段代码,假定每一行执行的时间为“1”,那么上述代码用掉的时间为4。无论n是多大,使用的时间都是4,记为O(n) =4。**当n趋于无限大的时候,这个4可以近似的认为是1。**为什么呢,还记得我们为什么需要算法复杂度吗?我们需要通过时间复杂度来描述这个代码随着n的增大,所需要时间的变化趋势
。而常数项对变化趋势影响很小,所以可以认为是O(1)。
2、O(n)
public void method2(int n){
for (int i = 1 ; i <= n; ++i) {
System.out.println("haha"); // 第3行
}
}
同样假定每一行执行的时间为“1”,那么这个代码要花费多长时间呢。这个时候,我们只需要看第3行执行多少次即可。
这个很轻易的看出这是与n有关的。n=1,那么System.out.println("haha")
只要执行1次;n=2,执行2次;n=3,执行3次。当等于n时,执行n次。所以这个时间的变化是成线性上升的,所以O(n)=n;
3、O( n 2 n^2 n2)
public void method3(int n){
for (int i = 1 ; i <= n; ++i) { // 第2行 第1层嵌套
System.out.println("haha"); // 第3行
for (int j = 1 ; j <= n; ++j) { // 第4行 第2层嵌套
System.out.println("heihei");// 第5行
}
}
}
老规矩,假定每一行执行的时间为“1”。
上面说过,常数项对变化趋势影响很小,所以可以认为是O(1)。那么同样, 当高次项与低次项同时存在时,比如
n
3
n^3
n3 +
n
2
n^2
n2 +
n
n
n,只有高次项对变化的趋势影响最大。所以我们只需要保留高次项,删除低次项即可。即:
n
3
n^3
n3 +
n
2
n^2
n2 +
n
n
n = O(
n
3
n^3
n3)(这个不是上述代码的复杂度,还没有开始计算呢,)
注意:1、具有多层嵌套的情况下,从最里层嵌套开始看。
首先看下2号嵌套,
对于System.out.println("heihei")
来说,无论j取什么值,2号for循环的循环体都执行一次
,j=2,System.out.println("heihei")
执行一次,j=100还是执行一次。j的取值在1到n之间,所以对于整个2号for循环,一共执行了n次(n个1累加)。然后看1号for循环。
当i=1时,1号的循环体执行1+n次(System.out.println(“haha”)1次,2号for循环体n次);i=2时,1好的循环体执行1+n次(System.out.println(“haha”)1次,2号for循环体n次);i=3时,1号的循环体执行1+n次(System.out.println(“haha”)1次,2号for循环体n次)。那么i=n时,1号的循环体1+n次(System.out.println(“haha”)1次,2号for循环体n次)。累加起来就是:
∑
i
=
1
n
1
+
n
\displaystyle\sum_{i=1}^{n} 1+n
i=1∑n1+n = n +
n
2
n^2
n2。只保留高次项,所以时间复杂度O(
n
2
n^2
n2)。(从此也可以看出,我们其实只需要算出具有最多次嵌套中的代码执行的次数即可。于本例子中就是算出第5行执行多少次就ok了)
4、O( n 2 n^2 n2)
public void method4(int n){
for (int i = 1 ; i <= n; ++i) {
System.out.println("haha");
for (int j = 1 ; j <= i; ++j) {
System.out.println("heihei");
}
}
}
仔细看看,这个和3还是有区别的,2号for里面的n变成i了。
同样先看下2号嵌套,
j=1时,执行1次,
j=2时,执行1次,
当 j=i时,还是执行1次,所以2号for循环执行i次(i个1累加)。
对于1号for循环,
当i=1时,执行1次(因为最终计算复杂度时,会忽略低次项,所以此次直接忽略System.out.println(“haha”)的执行次数,并且上面已经得出2号for循环的执行次数是i,此时i=2,也意味着2号for循环的执行次数是2),
当i=2时,执行2次
当i=3时,执行3次,
当i=n时,执行n次,
所以时间复杂度:
∑
i
=
1
n
i
=
\displaystyle\sum_{i=1}^{n} i =
i=1∑ni=
n
∗
(
n
+
1
)
2
\frac{n*(n+1)}{2}
2n∗(n+1) =
n
2
+
n
2
\frac{n^2+n}{2}
2n2+n ,忽略低次项以及系数后,为O(
n
2
n^2
n2)
5、O( n 2 n^2 n2)
public void method5(int n){
for (int i = 1 ; i <= n; ++i) {
System.out.println("haha");
for (int j = i ; j <= n; ++j) {
System.out.println("heihei");
}
}
}
仔细看看,这个和3还有4相比,又变化了
同样先看下2号嵌套,
j=i时,执行1次,
j=i+1时,执行1次,
当 j=n时,还是执行1次,所以2号for循环执行n-i+1次(n-i+1个1累加)。
对于1号for循环,
当i=1时,执行n次(因为最终计算复杂度时,会忽略低次项,所以此次直接忽略System.out.println(“haha”)的执行次数,并且上面已经得出2号for循环的执行次数是n-i+1,此时i=1,也意味着2号for循环的执行次数是n),
当i=2时,执行n-1次,
当i=3时,执行n-2次,
当i=n时,执行1次,
所以时间复杂度:
∑
i
=
1
n
i
=
\displaystyle\sum_{i=1}^{n} i =
i=1∑ni=
n
∗
(
n
+
1
)
2
\frac{n*(n+1)}{2}
2n∗(n+1) =
n
2
+
n
2
\frac{n^2+n}{2}
2n2+n ,忽略低次项以及系数后,为O(
n
2
n^2
n2)
6、O( n 3 n^3 n3)
public void method6(int n){
for (int i = 1 ; i <= n; ++i) {
System.out.println("haha");
for (int j = 1 ; j <= i; ++j) {
System.out.println("heihei");
for (int k = 1 ; k <= j; ++k) {
System.out.println("hehe");
}
}
}
}
这次来个3层循环的。
老规矩,3号for的执行次数为j,
那么2号for的循环次数就是
n
2
+
n
2
\frac{n^2+n}{2}
2n2+n
对于1号for来说,
i=1,执行
1
2
+
1
2
\frac{1^2+1}{2}
212+1 ,
i=2,执行
2
2
+
2
2
\frac{2^2+2}{2}
222+2 ,
i=3,执行
3
2
+
3
2
\frac{3^2+3}{2}
232+3 ,
i=n,执行
n
2
+
n
2
\frac{n^2+n}{2}
2n2+n ,
所以时间复杂度: ∑ i = 1 n i 2 + i 2 = \displaystyle\sum_{i=1}^{n} \frac{i^2+i}{2}= i=1∑n2i2+i= O( n 3 n^3 n3)
7、O( l o g n log n logn)
public void method10(int n){
for (int i = 1 ; i <= n; i*=2) {
System.out.println("heihei");
}
}
当i=
l
o
g
2
n
log_2^n
log2n时,跳出循环。
i取值范围((
2
0
2^0
20,
2
1
2^1
21,
2
2
2^2
22,
2
3
2^3
23,…,
2
t
2^t
2t),(t =
l
o
g
2
n
log_2^n
log2n)
所以这个循环一共走
l
o
g
2
n
log_2^n
log2n次,即O(
l
o
g
2
n
log_2^n
log2n)
8、O(n l o g n logn logn)
public void method8(int n){
for (int i = 1 ; i <= n; i*=2) {
System.out.println("haha");
for (int j = 1 ; j <= n; ++j) {
System.out.println("heihei");
}
}
}
2号for的执行次数为n
1号for的i实际的取值范围(
2
0
2^0
20,
2
1
2^1
21,
2
2
2^2
22,
2
3
2^3
23,…,
2
t
2^t
2t),(t =
l
o
g
2
n
log_2^n
log2n)
i=
2
0
2^0
20 ,执行n 次,
i=
2
1
2^1
21,执行n 次 ,
i=
2
2
2^2
22,执行n 次 ,
i=n,执行n 次 ,
所以时间复杂度:
∑
x
=
0
l
o
g
2
n
n
=
\displaystyle\sum_{x=0}^{log_2^n} n=
x=0∑log2nn= n*
l
o
g
2
n
log_2^n
log2n 。此处直接忽略系数 O(n
l
o
g
n
logn
logn)
9、O(n)
public void method9(int n){
for (int i = 1 ; i <= n; i*=2) {
System.out.println("haha");
for (int j = 1 ; j <= i; ++j) {
System.out.println("heihei");
}
}
}
2号for的执行次数为i
1号for的i实际的取值范围(
2
0
2^0
20,
2
1
2^1
21,
2
2
2^2
22,
2
3
2^3
23,…,
2
t
2^t
2t),(t =
l
o
g
2
n
log_2^n
log2n)
i=
2
0
2^0
20 ,执行
2
0
2^0
20 次,
i=
2
1
2^1
21,执行
2
1
2^1
21 次 ,
i=
2
2
2^2
22,执行
2
2
2^2
22 次 ,
i=n,执行n 次 ,
所以时间复杂度:
∑
x
=
0
l
o
g
2
n
2
x
=
\displaystyle\sum_{x=0}^{log_2^n} 2^x=
x=0∑log2n2x=
2
0
(
1
−
2
t
)
1
−
2
\frac{2^0(1-2^t)}{1-2}
1−220(1−2t)
又因为t=
l
o
g
2
n
log_2^n
log2n
所以
2
0
(
1
−
2
t
)
1
−
2
\frac{2^0(1-2^t)}{1-2}
1−220(1−2t) = (n-1)
因此:O(n)
总结:
没有循环,时间复杂度:O(1)
有循环:从最内层循环开始,
1、确定变量取值范围
2、确定变量每次取值的时间复杂度,记为f(n)
3、循环相加f(n)
重复1到3步,即可计算出嵌套循环的时间复杂度
各种时间复杂度比较
上面的例子中,演示了O(1)、O(n)、O(
n
2
n^2
n2)、O(
n
3
n^3
n3)、O(
l
o
g
n
logn
logn)、O(
n
∗
l
o
g
n
n*logn
n∗logn)
下图是几个复杂度比较
最好、最坏、平均、均摊时间复杂度
最好时间复杂度:在最理想的情况下,执行这段代码的时间复杂度。就是代码最少执行多少次。
最坏时间复杂度:在最糟糕的情况下,执行这段代码的时间复杂度。就是代码最多------------------------------------执行多少次。
那么在最好与最坏的情况下一定会存在这其他的情况,累加 这些情况下的时间复杂度 乘以 每种情况出现的频率 即可求出平均时间复杂度。
平均时间复杂度:
∑
i
=
1
n
O
(
i
)
∗
P
(
i
)
\displaystyle\sum_{i=1}^{n} O(i)*P(i)
i=1∑nO(i)∗P(i).
O
(
i
)
O(i)
O(i)代表i情况下花费的时间
P
(
i
)
P(i)
P(i)代表出现i这种情况的概率。
1
// array表示一个长度为n的数组
// 代码中的array.length就等于n
int[] array = new int[n];
int count = 0;
void insert(int val) {
if (count == array.length) {
int sum = 0;
for (int i = 0; i < array.length; ++i) {
sum = sum + array[i];
}
array[0] = sum;
count = 1;
}
array[count] = val;
++count;
}
最好时间复杂度:不执行if语句,O(1)
最坏时间复杂度:执行if语句,if语句里面有个for循环,需要执行n次,所以O(n)
平均时间复杂度:对于数据规模n,即取值范围为1,2,3,…,n