1、大O记法
计算机怎么判断程序性能?
我们都知道,编程基本上是在和数据打交道,大多数程序基本都在处理获取数据、查询数据、操作数据、返回数据相关的逻辑
因此出现了数据结构和算法,这两者出现本质为了解决如何能够更快、更省进行数据处理
更快:程序运行时间更短,对应的指标我们称之为时间复杂度
更省:程序运行所消耗的内存更少,对应的指标我们称之为空间复杂度
为什么需要复杂度?
我相信,大家第一眼看到程序运行时间更短,消耗的内存更少这句话,第一反应通过开始运行和结束运行两个时间点差额和内存使用情况
通过这种计算方法可以判断性能,为什么还需要进行复杂度分析呢?主要有三个原因:
-
测试结果大大依赖硬件条件
以时间举例,你不可能绝对的说某个代码需要跑10s,在某台机器上需要跑10s。因为程序的执行和机器的性能正向相关,如果放在一台老的机器上可以会大大多于10s,而放在一款未来高性能机器上可能只需要5s。因此受硬件环境的影响,并不能简单的用时间去统计
-
测试结果事后才能计算
上面所有的操作,往往都是在程序运行之后才能形成统计结果,但我们大多希望在伪码阶段,便能预测程序的执行性能。因此这种事后统计法不能作为性能预测的方法
-
测试结果受原始数据特性影响大
我们以排序为例,需要多少时间呢?试想一下如果给出的数据就已经是排好序的,那需要的时间几乎等于0,而如果给出的数据是完全的打乱的,那需要时间肯定非常长。因此我们发现测试结果本身就存在差异,除非我们的样本足够的多,否则计算出来的结果往往具有片面性
-
......
总结:综上所述,我们需要一个不用具体的测试数据和测试环境,就可以粗略地估计算法执行效率的方法。这个方法称作为复杂度,称作为大О记法
2、时间复杂度
如何计算时间复杂度呢?由于无法使用实际运行时间预估程序的运行时间,我们可以使用步数作为时间复杂度的计算
所谓步数,就是数组的每次索引值的读取,就算作一步,也可以称为 unit_time
案例1:数组值获取 (O(1))
如下代码:
int a[] = {0, 1, 2, 3, 4, 5, 6};
System.out.println(a[3]); // step1
计算机可以通过一步跳到任意一个索引的位置进行数据的读取
如果用大〇记法,称作为常数时间---O(1)。即:无论数组的长度多少,获取数组中某个值的都是一步到位,并且随着数组数量的增加,时间复杂度并不会提升
如果用数学公式 f(x)
表示程序运行所需要的时间(x 表示数组的长度,后面坐标图都一样),结果是:f(x) = 1
用对数时间---O(log(N)) 与 O(1) 比较可以发现:o(log(N))
比o(1)
增长的更快一些,性能更差一点
案例2:计算 N 的阶乘 (O(N))
题目:我们需要定义一个函数,这个函数能计算传入参数 N 的阶乘 N!
public int factorial(int n) {
int result = 1; //step1
for (int i = 1; i <= n; i++) { //step2
result *= i; // step3
}
return result;
}
step1∶初始化变量result 1步
step2:初始化变量i 1步,i++相当于i = i + 1,需要执行N次,因此 step2一共N+1步
step3:执行N步
(在此只考虑赋值的情况,i <= n
代码暂不考虑)
在这种情况下,一共的时间复杂度为 2N+2,使用大O记法,称作为 线性时间---O(2N+2)
从这两个案例可以看出,大O记法表示的是:代码执行时间随着数据规模增长变化的趋势,也就是需要把 N 从 0 开始逐次增加,观察其中的变化。而在这种情况下,常量、系数部分并不决定增长趋势,所以可以忽略。所以上面O(2N+2),通常会表示为O(N)
案例3:计算一个数组中所有的组合方式 (O(N^2))
我们需要定义一个函数,能打印传入参数的两两组合情况。例如:我们传入一个数组{ "hello", "spring" ,"springmvc", "mybatis"},里面有4个英文词语,我们希望两两组合,那么一共有哪些组合情况呢?
public void combine(String args[]) {
for(int i=0; i<arg.length; i++) { //step1
for(int j=i+1; j<args.length; j++) { //step2
System.out.println(args[i] + args[j]); //step3
}
}
}
最终的结果为 N^2 +N+1 步。注意,此次有指数出现了,我们称为 指数时间--O(N^2 +N+1),忽略常数为 O(N^2)
对于指数时间而言,线性时间都可以被忽略,例如:O(N^2+N+1),最终忽略以后的结果为 O(N^2)
总结:从上面3个案例中可以得出:大O记法,只保留最大趋势公式,并且 指数 > 线性 > 对数 > 常数,因此在计算时间复杂度的时候,其实不用一行行看代码,只需要关注 for循环嵌套情况,在写代码时,如果能用对数复杂度的代码替换线性复杂度的代码,那就是大大的性能优化
3、空间复杂度
如何计算空间复杂度?
空间复杂度和时间复杂度一样,同样遵循 大O记法。时间复杂度是以步作为基础单位,空间复杂度是以一个基础数据类型值当做基础单位
如:
O(1):
int a = 0;
int j = 0;
O(N):
int a[] = new int[n];
优先考虑时间复杂度
在实际编程过程中,大多数情况只会考虑时间复杂度!!
因为我们默认计算机内存是足够大的,足够给我们使用的
如摩尔定律所说的那样:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换而言之,每个18-24个月,同样大小的芯片里可存储的数据量将翻一倍。因此完全可以忽略内存空间不够的情况。一般只有参与嵌入式开发、单片机相关的编程开发才需要注意空间的问题。