数据结构与算法一复杂度分析

前言

知识让生活更具能量。希望我们在以后学习的路上携手同行。您的点赞、评论和打赏都是对我最大的鼓励。

今天来介绍一下大O时间复杂度分析,空间复杂度分析。他是学习数据结构和算法的必不可少的一环。

大O时间复杂度表示法

首先问大家一个问题,我们写了一段代码如何知道他的用时是多少呢?大家可能会说跑一下不就行了吗? 是的,跑一下确实可以知道用时,但是不同的机器配置用时都是不一样的,如果一段代码还需要调用外部数据的话,那这得到的时间就更加不稳定了。这个时候我们的时间复杂度分析就派上用场了。 时间复杂度的分析其实就是分析代码执行用的时间。
如何在不运行代码的情况下, 来“猜测”得到一段代码的运行时间呢?我们带着这个问题,来看下面的这一段代码。

int cal(int n) {
	int sum = 0;
	int i = 1;
	for ( ; i <=n ;++i){
		sum = sum + i ;
	}
	return sum;
}

从 CPU的角度来看,这段代码的每一行都执行着类似的操作:读数据-运算-写数据。 我们假设执行每行代码所用的时间是一样的,为time;
第2 ,3 行代码分别需要1个time的执行时间。第 4 ,5 行都运行了n遍,所以需要2n*time的执行时间。所以这段代码的总执行时间就是(2n+2)*time。 我们把所有代码的执行总时长叫做T(n) ,在上面这个公式中可以看到,总时长T(n)与每行代码的执行次数成正比。 在这里在给大家强调一个概念就是 代码执行次数n 代码执行次数n 代码执行次数n 重要的事情说三遍。

我们在来看下面这段代码。

int call( 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;
		}
	}
}

我们按照同样的法来分析这段代码的运行总时长T(n)是多少呢?

第2、3、4行代码,每行都需要1个time的执行时间,第5,6行代码每行都循环执行了n遍,需要2n*time个时间。 第7,8 每行都循环执行了n2 遍, 所以需要2n2 *time时间。所以,整段代码总的执行时间T(n)= (2n2+2n +3) *time。

尽管我们不知道time的具体值,但通过这两段代码执行时间的推导过程 ,我们得到了一个非常重要的规律。那就是,所有代码的执行时间T(n)与每行代码的执行次数n成正比。

我们可以把这个规律总结成一个公式:T(n) = O(f(n)).

下面我们来解释一下这个公式 ,其中,T(n)我们已经讲过了,它表示代码执行的时间;n表示数据规模的大小; f(n)表示每行代码执行次数的总和 (请把这句话多读几遍)。因为这是一个公式 ,所以用f(n)来表示。公式中的O,表示代码的执行时间T(n)与f(n)表达式成正比。

所以,第一个例子中的T(n) = O(2n+2), 第二个例子就是T(n) = O(2n2 +2n+3)。这就是大O时间复杂度表示法。大O时间复杂度表示法实际上并不是具体的数值,而表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度,简称时间复杂度。

当n很大时,你可以把它相像成10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大O表示法表示刚讲的那两段代码的时间复杂度。就可以记为: T(n) = O(n) ; T(n) = O(n2)。

时间复杂度分析

我们下面的内容主要是说用什么套路来"猜"一段代码的时间复杂度。你准备好了吗?
如果上面的内容还没有完全吸收,没关系。在看一遍!!!

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

我刚才说了,大O这种复杂度表示法只是表示一种变化趋势。我们通常会忽略掉公式中的常量、低阶,系数,只需要记录一个最大的量级就可以了。这段核心代码执行次数n的量级,就是整段需要分析的时间复杂度。
为了便于你理解,我还拿前面的例子来说明。

int cal(int n ) {
	int sum = 0;
	int i = 0 ;
	for(; i <=n ; ++i ){
		sum = sum + i;
	}
	return sum;
}

其中第2、3行代码都是常量级的执行时间,与n的大小无关,所以对于复杂度并没有影响。循环执行次数最多的是第4、5行代码。这两行代码被执行了n次,所以总的时间复杂度就是O(n)。

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

分析下面的代码

int cal(int n) {
	# 第一段
	int sum_1 = 0;
	int p = 1;
	for (; p<100; ++p){
		sum_1 = sum_1 + p;
	}
	
	#第二段
	int sum_2 = 0;
	int q =1;
	for(; q < n; ++q){
		sum_2  =  sum_2 +q;
	}
	# 第三段
	int sum_3 =0;
	int i = 1;
	int j = 1;
	for(; i<=n; ++j){
		j = 1;
		for(; j<=n; ++j){
     		sum_3 = sum_3 + i *j;
     	}
	}
	return sum_1 + sum_2+sum_3;
}

这个代码总共为分三段,我们分别计算出它们的时间复杂度,再取一个量级最大的的作为整段代码的复杂度。

第一段代码的时间复杂度是多少呢?这段代码循环执行了100次,所以是一个常量的执行时间,跟n的规模无关。
这里我要强调一下,即使这段代码循环10000次、100000次,只要是一个已知的数,跟n无关。
时间复杂度是一个算法执行效率与数据规模增长的变化趋势, 所以不管常量的执行时间多大,我们都可以忽略掉。

那第二段代码和第三段代码的时间复杂度是多少呢? 答案是O(n)和O(n2)。综合这三段代码的时间复杂度我们取其中最大的量级,所以最终的结果就是O(22)。

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

老样子先看代码

int cal(int n) {
	int ret = 0;
	int i = 1;
	for(; i<n; ++i){
	    # 调用f函数
		ret = ret + f(i);
	}
}

int f(int n){
	int sum =0 ;
	int i = 1;
	for (; i<n; ++i){
 		sum = sum +i;
	}
	return sum;
}

我们单独看cal()函数。假设f()只是一个普通的操作,那第4-6行的时间复杂度T1(n) 的值为 O(n)。但f()函数本身不是一个简单的操作,这的时间复杂度T2(n)的值 为O(n),所以,整个cal()函数的时间复杂度T(n) = T1(n)T2(n) =O(nn) = O(n2)。

常见时间复杂度案例分析

下面我们来介绍一下几种常见的时间复杂度量级。

多项式量级

1. 常量阶O(1)

首先你必有要明确一个概念,O(1)只是常量级时间复杂度的表示方法。并不是代码表着代码只执行一行。当有多行代码的时候仍然可以是O(1)。

	int i = 1;
	int j =2;
	int sum = i +j ;

只要代码的执行时间不随n的增大而增长,这样代码的时间复杂度我们都记作O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使代码有很多行,其时间复杂度也是O(1)。

2.对数阶O(logn)、 线性对数阶O(nlogn)

对数阶时间复杂度非常常见,同时也是最难分析的一种时间复杂度。

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

根据我们前面讲的复杂度分析方法,第三行代码是循环执行次数最多的。所以,我们只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。

从代码中可以看出,变量i的值从1开始取,每循环一次就乘2。当大于n时,循环结束。可以看到i的值变化为
在这里插入图片描述
所以,我们要知道x值是多少,就知道这行代码执行的次数了。通过2x=n求解得到x= log2n,所以这段代码的时间复杂度是x= log2n。
如果我们把代码改成:

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

这段代码的时间复杂度为x= log3n。 基于我们前面的一个理论:在采用大O标记复杂度的时候,可以忽略系数,即O(cf(n)) = O(f(n))。 所以x= log2n 时间复杂度 就等于x= log3n 时间复杂度了。
在对数阶时间复杂度的表示方法里,我们忽略对数的"底",统一表示为O(logn)。
一段代码的时间复杂度是O(nlogn)其实就是我们将时间复杂度为O(logn)的代码循环执行了n遍。 比如归并排序,快速排序的时间复杂度都是O(nlogn)。

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

直接上代码!!!!

public static 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)。

非项式量级

非项式量级只有两个: O(2n)和O(n!)。当数据规模n越来越大时,非多项式量级算法的执行时间会急剧增加,是非常低效的算法。在工作中不会使用。

空间复杂度

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

先看代码

public void (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(a[i])
	}
}

从上面的代码中我们可以看到,我们申请了一个数组a。而a数组的大小是根据n来决定的。而其他的变量占用大小都是固定的。 所以整段代码的空间复杂度就是O(n)。

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

小小的总结

我们上面说了两个基本的概念:
空间复杂度全称是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
时间复杂度是一个算法执行效率与数据规模增长的变化趋势,
越高阶的复杂度执行效率越低

在这里插入图片描述

以上内容均为读书所得, 想看更多内容请关注微信公众号。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值