一.数据结构和算法所要解决的问题
数据结构和算法本身解决的是“快”和“省”的问题,即如何让代码运行的更快,让代码更省存储空间;所以执行效率是很重要的一个考量标准.
接下来我们要讨论的:时间.空间复杂度分析就可以衡量执行效率,它是整个数据结构和算法学习的精髓.
二.为什么需要复杂度分析?
1. 测试结果非常依赖测试环境
不同硬件的测试环境会对产生不同的测试结果;例如,我们用不同处理器的电脑执行同一段代码,处理器核数越多,执行速度肯定越快;
2.测试结果受数据规模的影响很大
对同一个排序算法,待排序数据的有序度不一样,排序执行的时间会有很大差别;极端情况下,如果数据是有序的,那么排序算法不需要任何操作,执行时间会非常快;除此之外,数据规模太小,测试结果无法反应出算法的性能;
综上所述,我们需要一个不用具体的测试数据,就能粗略的估算算法执行效率的方法,这就是时间.空间复杂度分析;
三.大O复杂度分析
我们来看看下面这段代码:
int cal(int n) {
int sum = 0;
int i = 1;
for(;i<n;++i){
sum=sum+i;
}
return sun;
}
假设每一行代码执行的时间是一样的,为unit_time,那么在这个假设基础上,这段代码的执行时间是多少?
第2,3行分别需要1个unit_time,第4,5行都运行了n遍,所以需要(2n+2)*unit_time
可以看出:执行时间Tn与每行代码的执行时间成正比
我们将这个规律总结为一个公式:大O时间复杂度表示法
其中:T(n):代码执行的时间;f(n):每行代码执行的次数总和;O:表示成正比
它并不表示代码段真正的执行时间,而是代表一种趋势;
四.时间复杂度分析
前面介绍了大O时间复杂度分析的由来和表示方法,现在我们来看看如何分析一段代码的时间复杂度
1.只关注循环次数最多的一段代码
大O表示一种趋势,我们通常会忽略常量,低阶,系数,只记录一个最大阶的量级就行;所以我们在分析一段代码的时间复杂度时,只需关注循环最多的这段代码就行;
栗子:
int cal(int n) {
int sum = 0;
int i = 1;
for(;i<n;++i){
sum=sum+i;
}
return sun;
}
其中,第2,3行都是常量级的执行时间,和n没关系,只关注循环次数最多的第4,5行,所以总执行时间为O(n);
2.加法法则:总复杂度等于量级最大的那段代码的复杂度
栗子:
分析这段代码,总共分为三部分,分别计算sum_1,sum_2,sum_3;
第一部分代码,循环100次,常量级别,跟n的规模无关;
第二部分代码,循环n次,T(n)=O(n);
第三部分代码, 循环n2次,T(n)=O(n2);
综合来看,我们取最大的量级,这段代码的执行时间为O(n2);
3.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
栗子:
观察该代码段,由于f()函数本身不是一个简单函数,所以总时间T(n)=O(n*n);
五.几种常见的时间复杂度实例分析
首先,我们来看下常见的时间复杂度实例。
对于上面提到的时间复杂度量级,可以分为两类:多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2n)和O(n!)。
当数据规模n越来越大,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。
下面我们看看几种常见的多项式时间复杂度:
1.O(1)
首先我们要明确一个概念,O(1)只是常量级时间复杂度的一种表示方法,并不是指只执行力一行代码。比如下面的代码,即便有3行,它的时间复杂度也是O(1),而不是O(3)。
int i = 8;
int j = 6;
int sum = i +j;
综上所述,只要代码的执行时间不随n的增大而增长,这样代码的时间复杂度我们都记做O(1);换句话说,只要算法中不存在循环语句、递归语句、即使有成千上万的代码,其时间复杂度也是O(1)。
2.O(logn)、O(nlogn)
我们来看如下栗子,探讨对数阶时间复杂度。
i = 1;
while(i <= n){
i = i * 2;
}
根据我们以前所讲的时间复杂度分析方法,第三行代码的执行次数是最多的,所以我们重点关注这行代码;分析代码得知,变量i从1开始取,每循环一次就乘以2,当大于n时,循环结束.如果我们将它一一列出来,就是等比数列的样子;
因此,我们只要知道x值是多少,就知道这行代码执行了多少次。通过2x=n求解x,这就是我们高中学习的对数概念。所以,这段代码的时间复杂度就是O(log2n)。
现在,我们将代码稍微改下:
i = 1;
while(i <= n){
i = i * 3;
}
根据刚才的思路,这段代码的时间复杂度为O(log3n)。
实际上,不管是以2为底,还是以3为底,我们都可以将对数阶时间复杂度表示为O(logn),下面我们来分析原因?
我们知道,对数之间是可以互相转换的,log3n=log32log2n;所以O(log3n)=O(C log2n),其中C= log32是一个常量。基于我们前面学习的理论:在采用大O标记复杂度时候,可以忽略系数,即O(log3n)=O(log2n);因此,在对数阶时间复杂度表示中,我们忽略对数的底,统一表示为O(logn)。
如果我们理解了O(logn),那么O(nlogn)就很容易理解了,如果一段代码的时间复杂度为O(logn),循环执行了n遍,那么它的时间复杂度就是O(nlogn)。
3.O(m+n)、O(m*n)
下面,我们来看看代码的复杂度由两个数据规模来决定。
栗子:
int cal(int m,int n){
int sum_1 = 0;
int i =1;
for (;i<m;++i) {
sum_1=sum_1+i;
}
int sum_2 = 0;
int j=1;
for (;j<n;++j) {
sum_2 = sum_2 +j;
}
return sum_1+sum_2;
}
从代码中可以看出,m和n是表示两个数据规模。我们无法事先评估m和n谁的量级大,所以这段代码的时间复杂度就是O(m+n)。
六.空间复杂度分析
前面我们讲的时间复杂度,表示算法的执行时间和数据规模之间的增长关系。而空间复杂度,表示算法的存储空间与数据规模之间的增长关系;
下面通过一个栗子来分析:[代码仅供分析]
void print(int n){
int i = 0;
int[] a = new int[n];
for (i;i<n;i++) {
a[i] = i * i;
}
for (i=n-1;i>=0;--i) {
print out a[i]
}
}
跟时间复杂度一致,我们可以看到,第二行代码中,我们申请了一个空间存储变量i;但是它是常量阶的,跟数据规模n没有关系,所以忽略;第3行申请了一个大小为n的int类型数组,初次置为没有占用更多的空间,所以该段代码的空间复杂度为O(n)
内容小结;
复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率和数据规模之间的增长关系。
下面我们来看看常见的复杂度执行效率: