复杂度分析

一、如何分析、统计算法的执行效率和资源消耗

1)为什么需要复杂度分析?

首先我先给大家介绍一下事后统计法:将代码执行一遍,通过统计、监控,就能得到算法执行的时间和占用的内存大小。但是这种统计方法有很大的局限性。

1.测试结果非常依赖测试环境

2.测试结果受数据规模的影响很大

所以需要一个不用具体的测试数据就能粗略地估计算法的执行效率的方法——时间、空间复杂度分析方法。

2)大O复杂度表示法

算法的执行效率,粗略地讲,就是算法代码的执行时间。我们假设每行代码执行的时间都一样,都是1个单位时间,从而算出一段代码总的执行时间为多少个单位时间,然后将公式中的低阶、常量、系数这三个不左右增长趋势的部分忽略,只记录最大量级的表示法。例:T(n)=2n+2就可以记为T(n)=O(n),T(n)=n2+2就可以记为T(n)=O(n2)

这就是大O时间复杂度表示法,大O时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,也叫做渐进时间复杂度,简称时间复杂度。

3)时间复杂度分析

如何分析时间复杂度,三个方法

1.只关注循环执行次数最多的一段代码

2.加法法则:总复杂度等于量级最大的那段代码的复杂度

3.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

4)几种常见时间复杂度实例分析

复杂度分析_第1张图片

罗列的这些复杂度量级,粗略地分为多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2n)和O(n!)。

当数据规模n越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实非常低效的算法。

  1. O(1)

    O(1)只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。只要代码的执行时间不随着n的增大而增长,这样代码的时间复杂度我们都记作O(1)。一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行代码,其时间复杂度也是O(1)。

  2. O(logn)、O(nlogn)

    数阶时间复杂度非常常见,但是也是最难分析的一种。举个例子说明一下。

    int i=1;
    while(i<=n){
    	i=i*2;
    }
    

    从代码中可以看出,变量的值从1开始取,每循环一次就可以乘以2。当大于n时,循环结束。所以2x=n,所以x=log2n,这段代码的时间复杂度就是O(log2n)。实际上,不管是以2为底,还是以3为底,我们可以把所有对数阶的时间复杂度都记为O(logn)。

    而O(nlogn)就是在循环内嵌套了一个时间复杂度为O(logn)的代码。O(nlogn)是一种非常常见的算法时间复杂度。比如,归并排序、快速排序的时间复杂度都是O(nlogn)。

  3. O(m+n)、O(m*n)

    for(int i=0;i<m;i++){
    	System.out.println("m");
    }
    for(int j=0;j<n;j++){
    	System.out.println("n");
    }
    

    从代码中可以看出,m和n是表示两个数据规模。我们无法事先评估m和n谁的量级大,所以我们在表示复杂度的时候,就不能简单的利用加法法则,省略掉其中一个。所以上面的代码的复杂度是O(m+n)。

    这样我们的加法法则就不正确了,需要将加法规则改为:T(n)=T1(m)+T2(n)=O(m)+O(n)=O(m+n),但是乘法法则继续有效:T(n)=T1(m) x T2(n)=O(m) x O(n)=O(m*n)。

5)空间复杂度分析

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。那么空间复杂度的全称就是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。

public void print(int n){
	int[] list=new int[n];
}

这里申请了一个大小为n的int类型数组,所以整段代码的空间复杂度就是O(n)。

常见的空间复杂度就是O(1),O(n),O(n2)。

6)内容小结

复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增长关系,可以粗略地表示,越高阶复杂度地算法,执行效率越低。常见的复杂度并不多,从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)。

7)实际问题

1.如何理解存储一个二进制数,输入规模(空间复杂度)是O(logn)bit

​ 比如8用二进制数表示就是3个bit,16用二进制表示4个bit,以此类推n用二进制表示就是 logn 个bit。

二、浅析最好、最坏、平均、均摊时间复杂度

1)最好、最坏情况时间复杂度

//n表示数组array的长度
int find(int[] array,int n,int x){
	int pos=-1;
	for(int i=0;i<n;i++){
		if(array[i]==x)pos=i;
	}
	return pos;
}

这段代码是在一个无序的数组(array)中,查找变量x出现的位置。如果没有找到,就返回-1。很明显,这段代码的复杂度是O(n),但是我们在数组中查找一个数据,并不需要每次都把整个数组都遍历一遍,因为有可能中途找到就可以提前结束循环了。所以优化一下后

//n表示数组array的长度
int find(int[] array,int n,int x){
	int pos=-1;
	for(int i=0;i<n;i++){
		if(array[i]==x){
            pos=i;
            break;
        }
	}
	return pos;
}

这时候问题就来了,优化之后代码的时间复杂度还是O(n)吗?

这段代码的最好情况时间复杂度就是O(1),最坏情况时间复杂度就是O(n)。

2)平均情况时间复杂度

最好情况时间复杂度和最坏情况时间复杂度对应的都是在极端情况下的代码复杂度,发生的概率不大,为了更好的表示平均情况下的复杂度,我们应该使用平均情况时间复杂度,简称平均时间复杂度

我们就上面的例子来分析,要查找的变量x在数组中的位置,有n+1中情况:在数组中有0~n-1个位置和不在数组中,我们把每种情况下,查找需要遍历的元素个数累加起来,然后再除以n+1,就可以得到需要遍历的元素个数的平均值,即:

复杂度分析_第2张图片

大O标记法,省略掉系数、低阶、常量,所以得到的平均时间复杂度就是O(n)。

但是这样不是很准确,因为我们得考虑各种情况发生的概率,假设在数组中和不在数组中的概率都是1/2,而具体到数组中每一个数字的话就是有0~n-1中选择,所以就是1/2 * 1/n也就是1/(2n),所以平均时间复杂度的计算应该是

复杂度分析_第3张图片

这个是加权平均值,也叫做期望值,所以平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度。通过计算上面那段代码的加权平均时间复杂度仍然是O(n)。

实际上,在大多数情况下,我们并不需要区分最好、最坏、平均情况时间复杂度三种情况。只有同一块代码在不同情况下,时间复杂度有量级的差距,我们才会使用这三种复杂度表示法来区分。

3)均摊时间复杂度

//n表示数组array的长度
//array.length=n
int count=0;
int[] array=new int[n];
void insert(int val){
	if(count==array.length){
        int sum=0;
        for(int i=0;i<array.length;++i){
            sum=sum+array[i];
        }
        array[0]=sum;
        count=1;
    }
    array[count]=val;
    ++count;
}

这段代码实现一个往数组插入数据的功能。当数组满了之后,for循环计算出和,并清空数组,将求和之后的sum值放在数组下标为0的位置,然后再插入新的数据。所以最好情况时间复杂度是O(1),也就是在数组中有空闲空间,直接插入到下标为count的位置;最坏情况时间复杂度是O(n),因为数组满了,就要循环求和再插入。

平均时间复杂度是O(1)。假设数组的长度是n,根据数据插入的位置的不同,我们可以分为n种情况,每种情况的时间复杂度是O(1),除此之外,还有一种情况,就是数组没有空闲空间的时候插入一个数据,这个时候的时间复杂度是O(n)。而且,这n+1种情况发生的概率是一样的,都是1/(n+1)。所以,平均时间复杂度就是:

图片

但是这一题其实并不需要这么复杂的考虑,我们来对比一下这一段代码和上面find()的区别,还是很大的。

首先,find()函数在极端情况下,复杂度才是O(1),但insert()函数在大部分情况下都是O(1)。

第二个,对于insert()函数来说,O(1)时间复杂度的插入和O(n)时间复杂度的插入,出现的频率是非常有规律的,一般都是一个O(n)后面跟着n-1个O(1),循环往复。

对于insert()这种特殊的情况,我们应该用摊还分析法。一个O(n)后面跟着n-1个O(1),循环往复,那么我们把耗时多的那次操作均摊到接下来的n-1次耗时少的操作上,那么均摊时间复杂度就是O(1)

你可能感兴趣的:(数据结构与算法,数据结构,算法,java)