复杂度分析的意义
我们平时把写的代码运行一遍,然后通过监控和统计等手段,可以计算出算法执行的时间和占用的内存大小。对于这种方法,我们称之为事后统计法。这种方法有着很大的局限性。一方面他的测试结果受测试环境的影响,另一方面测试结果受测试数据的影响也很大。
大O复杂度表示法
下面来看一个例子,
int cal(int n){
int sum = 0;
int i = 1;
for(;i < n;i++){
sum = sum + i;
}
return sum;
}
假设每条语句的执行时间相同,记为unit_time
所以第2,3,7行代码的执行时间分别需要1unit_time,但是第4,5行代码因为有个循环,且i < n所以他们的循环时间为2n * unit_time,所以这段代码的总执行时间为(2n+ 3)unit_time.
规律:一段代码的总执行时间与每一条语句的执行次数(累加和)成正比。
再看一个例子
int cal(int n){
int sum = 0;
int i = 1;
int j = 1;
for(;i < n;i++){
j = 1;
for(;j < n;j++){
sum = sum + i*j;
}
}
}
此时2,3,4代码的执行时间为1unit_time,5,6执行时间为2n unit_time,7,8执行时间为2n^2
总执行时间为(2n*2 + 2n + 3) * unit_time.
此时我们把这个规律总结成一个公式。
T(n) = O(f(n)) 表示代码的执行时间T(n)与f(n)成正比。
T(n)表示代码的执行总时间,n表示数据规模,f(n)表示每条语句执行次数的累加和。
套用大O表示法,第一个例子成为T(n) = O(2n + 3),第二个例子T(n) = O(2n^2 + 2n + 3).
实际上,大O时间复杂度并不表示代码的真正执行时间,而是表示代码执行时间随着数据规模增大的变化趋势,因此常称为渐进时间复杂度,简称时间复杂度。
当n很大时,我们可以忽略公式中的低阶,常量,系数三个部分,只保留最大量级。此时第一个例子就可以写成T(n) = O(n),第二个例子T(n) = O(n^2)。
时间复杂度分析方法
1.加法法则:代码总的复杂度等于量级最大的那段代码的复杂度
大O复杂度只表示一种变化趋势。通常只记录最大量级,因此再代码分析的时候,只关注循环次数最多的那一段代码就可以。
看一个例子
int cal(int n){
int sum1 = 0;
int p = 1;
for(;p < 100;p++){
sum1 = sum1 + 0;
}
int sum2 = 0;
int q = 1;
for(;q < n;q++){
sum2 = sum2 + 0;
}
int sum = 0;
int i = 1;
int j = 1;
for(;i < n;i++){
j = 1;
for(;j < n;j++){
sum = sum + i*j;
}
}
}
此时需要注意的是,sum1代码段中,p < 100只是一个常量级的执行时间,与数据规模无关。时间复杂度表示的是代码执行时间随数据规模的增长趋势,因此无论常量级的执行时间多长,本身对增长趋势并没有影响。所以这段代码的时间复杂度为T(n) = O(n^2).
结论:总的时间复杂度等于量级最大部分的那段代码的时间复杂度。这条法则就是加法法则,
公式表示为:
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)))
2.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
此处也比较好理解,就不举例子了,直接看公式
T1(n) = O(f(n));T2(n) = O(g(n))
T(n) = T1(n) * T2(n)) = O(f(n) * g(n));
几种常见的时间复杂度量级
1.O(1)
只要代码的执行时间不随数据规模n的变化,代码就是常量级时间复杂度,统一记作O(1)。
此时需要注意O(1)是常量级时间复杂度的一种表示方法并不是指就执行了一行代码。
2.O(logn)、O(nlogn)
对数阶时间复杂度非常常见,比较难分析
看一个例子
i=1;
while(i <= n){
i = i * 2;
}
从代码中可以看出,每次循环一次就乘2,当i(2^x) = n时循环结束。不难发现,变量i的取值为等比数列。 所以根据公式可以算出x = 以2为底n的对数。
如果我没把以上代码换成
i = i * 3;
此时的时间复杂度就为以3为底。
实际上,无论是以什么数字为底,我们可以把所有的对数阶时间复杂度统一记为O(logn).
对于O(nlogn)是不是类似于乘法公式运算得到的。此处就不详细解释了
3.O(m + n)、O(mn)
先看一个例子
int cal(int n,int m){
int sum1 = 0;
int i = 1;
for(;i < m;i++){
sum1 = sum1 + i;
}
int sum2 = 0;
int j = 1;
for(;j < n;j++){
sum2 = sum2 + j;
}
return sum1 + sum2;
}
上述代码可以看出,m和n是两个无关的数据规模,但是最终的时间复杂度与这两者有关。所以,时间复杂度为O(m + n);
空间复杂度分析方法
前面我们说,时间复杂度的全程为渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。空间复杂度称为渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
看一个例子
public void reverse(int a[],int n){
int temp[] = new int[n];
for(int i = 0;i < n;i++){
temp[i] = a[n - i - 1];
}
for(int i = 0;i < n;i++){
a[i] = temp[i];
}
}
在第三行代码中,申请了一个空间来存储变量i,但是他是常量阶的,与数据规模n没有关系,也就是说,i占用存储空间并不会随数据规模n变化,第二行代码中,申请了一个大小为n的int类型的数组,除此之外,剩下的代码没有浪费存储空间,所以该段代码的空间复杂度为O(n).
最好时间复杂度和最坏时间复杂度
看一个例子
int find(int[] array,int n,intx){
int i = 0;
int pos = -1;
for(;i < n;i++){
if(array[i] == x)
pos = i;
}
return pos;
}
很简单的看出来,这段代码的时间复杂度为O(n),但是分析这个代码可以看出,在数组中查找一个元素,并不需要把数组从头遍历到尾,有可能中途就找到了。
下面看看优化的代码。
int find(int[] array,int n,intx){
int i = 0;
int pos = -1;
for(;i < n;i++){
if(array[i] == x)
pos = i;
break;
}
return pos;
}
很简单,加了一个break,但是现在的代码时间复杂度还有O(n)吗,如果第一个元素就是要查找的值,时间复杂度为O(1),但是如果数组中没有这个元素,需要从头遍历到尾,时间复杂度就成了O(n)。所以此时就引申出了最好时间复杂度和最坏时间复杂度的概念。概念很好理解,就不多赘述了。
平均时间复杂度
平均时间复杂度指的是代码被重复执行无数次,对应的时间复杂度的平均值.
此时的时间复杂度为O(n).