1. 什么是时间复杂度?
what is Time Complexity?
定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数->进而分析T(n)随n的变化情况并确定T(n)的数量级
它表示随问题规模n的增大,算法执行时间的增长率是->f(n)就简称为时间复杂度 记作 O ( f ( n ) ) O(f(n)) O(f(n))
先简单的介绍 O ( 1 ) O ( n ) O ( n 2 ) O(1) \space O(n)\space O(n^2) O(1) O(n) O(n2), 官方名称 常数阶,线性阶,平方阶
那么我们怎么去计算时间复杂度呢?这个算法执行时间的增长率是啥东东
推导大O阶:
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在而且不是1,则去除与这个项相乘的常数。结果就是大O阶
说实话这三行文字看起比较抽象,有点类似高数里面的求导?
1.1 常数阶
对于顺序结构的时间复杂度,下面这高斯算法,时间复杂度是 O ( 1 ) O(1) O(1)。
private static void gaussian(){
Clock clock = Clock.systemUTC();
System.out.println(clock.millis()); // 测试Java8提供的新方法
long startTime=System.currentTimeMillis(); // 获取开始时间
System.out.println(startTime);
// 对于顺序结构的时间复杂度 跟n的大小无关 O(1)
int sum, n=1000000;
sum = (n+1)*n/2;
System.out.println(sum);
long endTime=System.currentTimeMillis(); // 获取结束时间
System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
}
另外对于分支结构(不包含在循环结构中)而言,无论是false还是true,执行次数是恒定的,不会随着n的变化而变化
1.2 线性阶
线性阶的循环结构会稍微复杂一点,要确定算法程序的阶次,通常我们需要对某个确定的语句或者某个语句集代码块的运行次数,分析复杂度,关键分析循环结构的运行情况
private static void myLoop(){
int n = 10000;
for (int i = 0; i < n; i++) {
// todoSomething
System.out.println(i);
}
}
这个在循环体里面的代码块必须执行n次,所以时间复杂度是 O ( n ) O(n) O(n)
1.3 对数阶
private static void logarithm(){
int n = 10000, i=0;
// i的每次变化阶级影响着循环的次数 所以需要根据公式计算而不是O(n)
while(i < n){
i = i*2;
}
}
由于i每次乘以2,有公式 2 x = n 2^x=n 2x=n,计算得出: x = l o g 2 n x=log_2n x=log2n,所以这个循环的时间复杂度是 l o g 2 n log_2{n} log2n,也就是 O ( l o g 2 n ) O(log_2{n}) O(log2n)
1.4 平方阶
private static void mySquare(){
int n = 100;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.print(i + j);
}
System.out.println();
}
}
这是一个循环嵌套,内层循环是
O
(
n
)
O(n)
O(n),对于外层循环而言也是
O
(
n
)
O(n)
O(n);所以整个循环体时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)
如果外层循环改为m,时间复杂度就变为
O
(
m
n
)
O(mn)
O(mn)
那么下面这个例子时间复杂度是多少呢?
private static void mySpecialSquare(){
int n = 100;
for (int i = 0; i < n; i++) {
// 这里j每次从i开始了 不是每次从0开始
for (int j = i; j < n; j++) {
// 这里要求是O(1)的操作
System.out.print(i + j);
}
System.out.println();
}
}
这时候就不能单独分析内层和外层了,因为内层循环受到外层循环的条件i的影响;所以整体分析了:
当
0
≤
i
<
n
0\le i<n
0≤i<n,计算出内循环总的执行次数:
∑
i
=
1
n
i
=
1
+
2
+
3
+
.
.
.
+
(
n
−
1
)
+
n
=
n
(
n
+
1
)
2
=
n
2
2
+
n
2
\color{blue} \displaystyle\sum_{i=1}^n i=1+2+3+...+(n-1)+n=\frac {n(n+1)} 2=\frac {n^2} 2 +\frac n 2
i=1∑ni=1+2+3+...+(n−1)+n=2n(n+1)=2n2+2n
随着你的增大,只保留我们的最高项,然后去除系数,就得到
O
(
n
2
)
O(n^2)
O(n2)
好了经过徐师兄上面例子的前奏预热,下面摆几个姿势练练技术
int n,i,j;
function(n);// O(n)
for(i=0; i<n; i++){// O(n^2)
function(n);
}
forfor(i=0; i<n; i++){// n(n+1)/2
for (int j = i; j < n; j++) {
// 执行O(1)操作
}
}
这个其实就是把前面几种情形通过顺序结构连接起来而已,所以总的执行次数是:
f
(
n
)
=
1
+
n
+
n
2
+
n
(
n
+
1
)
2
=
3
2
n
2
+
3
2
n
+
1
\color{green}f(n)=1+n+n^2+\frac {n(n+1)} 2=\frac 32 n^2 + \frac32n+1
f(n)=1+n+n2+2n(n+1)=23n2+23n+1
随着n的增大,这个代码的时间复杂度最终也是
O
(
n
2
)
O(n^2)
O(n2)
1.5 常见的时间复杂度
程序执行次数 | 时间复杂度O | 非正式术语 |
---|---|---|
12常数 | O ( 1 ) O(1) O(1) | 常数阶 |
2 n + 3 2n+3 2n+3 | O ( n ) O(n) O(n) | 线性阶 |
3 n 2 + 2 n + 1 3n^2+2n+1 3n2+2n+1 | O ( n 2 ) O(n^2) O(n2) | 平方阶 |
l o g 2 n log_2n log2n | O ( l o g n ) O(logn) O(logn) | 对数阶 |
3 n + 2 n l o g 2 n + 1 3n+2nlog_2n+1 3n+2nlog2n+1 | O ( n l o g n ) O(nlogn) O(nlogn) | n l o g n nlogn nlogn阶 |
4 n 3 + 3 n 2 + 2 n + 1 4n^3+3n^2+2n+1 4n3+3n2+2n+1 | O ( n 3 ) O(n^3) O(n3) | 立方阶 |
2 n 2^n 2n | O ( 2 n ) O(2^n) O(2n) | 指数阶 |
它们的大小关系是(高中是不是证明过很多次了?(^^)):
O
(
1
)
<
O
(
l
o
g
n
)
<
O
(
n
)
<
O
(
n
l
o
g
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
\color{red}O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
像
O
(
n
3
)
O(n^3)
O(n3)及以上的时间复杂度过大,基本不会去考虑采用这个算法
1.6 最坏情况与平均情况
最坏情况运行时间:这是最重要的指标,一般在没有特殊指定下都是指最坏时间复杂度
平均运行时间:通过运行一定数量的实验数据得到的 是实际情况最有意义的
2. 什么是算法空间复杂度
通常我们在写代码的时候,完全可以用空间来换取时间,定义:计算算法所需的存储空间。
通常“复杂度”都只时间复杂度,也是研究的重点
总结:算法的优劣度直接决定了程序运行的效率
愚公移山的精神固然值得学习,但是发明炸药和挖掘机是更加聪明的做法