二.复杂度分析(上):如何分析、统计算法的执行效率和资源消耗

在这里插入图片描述

一.数据结构和算法所要解决的问题

数据结构和算法本身解决的是“快”和“省”的问题,即如何让代码运行的更快,让代码更省存储空间;所以执行效率是很重要的一个考量标准.
接下来我们要讨论的:时间.空间复杂度分析就可以衡量执行效率,它是整个数据结构和算法学习的精髓.

二.为什么需要复杂度分析?

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)

内容小结;
复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率和数据规模之间的增长关系。
下面我们来看看常见的复杂度执行效率:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值