什么是时间复杂度?什么是空间复杂度?

在对一个算法的好坏的评估中,最常用到的就是时间复杂度和空间复杂度。那么问题来了,什么叫做时间复杂度,什么叫做空间复杂度呢?时间复杂度是用来评估一段代码执行时间的,空间复杂度是用来评估一段代码执行时所需要申请的资源的。简单来说,时间复杂度越少,空间复杂度少,这段代码的效果也越好。但是既要马儿跑,又要马儿不吃草通常来说是达不到的。因此我们需要通过对代码时间复杂度和空间复杂度的评估来确定什么样的背景采用什么样的代码。

  1. 时间复杂度:一般地,一行代码执行地时间非常非常短(纳秒级别),因此我们粗略地认为执行一行代码所花费地时间为一个单位常数(即1),不管是加减乘除还是读写寄存器的值。
int a = 5;
int b = 3;
int c = 4;

以上只有3条代码,因此这段代码执行所耗费的时间为3,假设上面的代码规模为n(假设n为1000000),由于它只需要执行3条代码。因此,我们称这一类型的代码的时间复杂度为常数级别。用O(1)表示,即代码量不随规模n的改变而改变。相信有不少初学者还是没明白为什么明明是3条代码不是O(3)却是O(1)呢?但是不要急,慢慢看下去你就明白了。

for(int i = 0; i < n; i++){
	std::cout << i << std::endl;
}

以上代码看似只有两条代码,但是实际上for循环中的代码需要执行n遍(n为规模),这时当n为1是,它只需要执行1条cout代码,花费时间为1,当n为1000000时,就需要花费时间为1000000.因此,这里花费时间和n的规模是呈线性关系的。我们用O(n)表示。还有一种情况就是:

for(int i = 0; i < n; i++){
	int b = i;
	std::cout << i + b << std::endl;
}

这时按照上面的推论,则这段代码的执行时间应当为2n,所以它的时间复杂度为O(2n)???。这是不对的,试想一下,当n为无穷大时,n和2n有区别吗,答案是没有区别,都为无穷大。所以O(n)在某种程度上可以代替O(2n)。同样地,现在解释一下为什么常数级别地时间复杂度可以直接用O(1)来替代。因为任何一个常熟相比于无穷大和1相比于无穷大地结果都是一样地。举个通俗一点的例子,假设你有10000块钱,我只有1块钱,你的财富是我的10000倍,但是相比于我🐴爸爸,你我都是穷鬼。这就是无穷大的作用。

那除了常数和线性还有其他的时间复杂度吗?答案是当然有。除此之外常见的还有O(n²),O(n³)… 统称为幂级,以及O(lgn)——对数级

O(n²)级应该很容易推理出来吧?很快就能想到的就是循环嵌套,for中有for,无中生for(跑题了跑题了)。同理O(n³)也很容想到——for中有for中有for。

比较难以理解的就是对数级,之所以会产生这样的时间复杂度就是因为执行的代码数量是根据一定的规则产生跳跃性的执行。一个简单的例子:

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

显然,上面的代码的时间复杂度并不是O(n)。假设当n为8时,如果是线性的,那么应该需要执行8条语句。但实际上只执行了3条语句(i = 1, i = 2, i = 4)。因此这并不是一个线性级的,而是一个对数级。即2^m = n,所以m=lgn,这里的底数为2。那这里为什么不写出具体的底数呢?因为具体的底数要看具体的代码,其次在无穷大面前一切都是渣渣,所以以2为底数和以200为底数没区别。这样的话我们统称为O(lgn)。

到这里为止,O(nlgn)的时间复杂度应该非常好理解了,不需要再举任何例子了吧?

以上的代码都是非常非常easy的,实际上很少碰到这种简单的代码,比如下面这段代码:

for(int i = 0; i < n; i++){
	for(int j = 0; j < i; j++){
		std::cout << i + j << std::endl;
	}
}

上面代码中的时间复杂度乍一看好像是O(n²),但是仔细看一下好像又不是O(n²),细细想一下,好像又是O(n²),那到底是不是呢?答案是是的。可以想一下,当i==0时,代码执行0行代码执行;当i == 1时,1行代码执行;当 i == 2 时,2行代码执行…当i == n时n行代码执行。因此总共执行的代码为1 + 2 + 3 + 4 +…+n,以上是个等差数列,其和为(n²-n)/ 2 = n² / 2 - n / 2。因为n是无穷大的,所以相比于n² 这一项,n可以忽略,因此上面的n² / 2 - n / 2约等于n² / 2。因为n是无穷大的,所以n² / 2越等于n²。因此,上面的时间复杂度仍然为O(n²)。其实,上面的代码就是插入排序算法的最坏情况下的时间复杂度。

总结:对于多项式,时间复杂度永远取最高的那一项,并且忽略所有系数。

那么空间复杂度又是什么呢?空间复杂度主要是用来描述一段代码对存储空间消耗的描述。

for(int i = 0; i < n; i++)
{
	std::cout << i << std::endl;
}

上面这段代码中虽然有一个for循环,但是唯一涉及到空间申请的只有一处,就是int i = 1,因此只申请了一次空间即在栈中为i分配了(4或8个字节)空间。所以,空间复杂度为O(1)。同理:

class T{
};
for(int i = 0; i < n; i++){
	T* t = new T();
}	

以上这段代码,每次都要申请一个sizeof(T)的空间。因此以上代码的空间复杂度为O(n)。同理,只要在上面时间复杂度的代码基础上加一个申请空间的动作,其空间复杂度就和时间复杂度一致了。这里要介绍一种特殊的情况,就是递归

int factorial(int n){
	if(n == 1)
		return 1;
	else
		return n * factorial(n - 1);
}

上述代码是用于求n的阶乘。其时间复杂度很明显应该为O(n),同时其空间复杂度也为O(n),因为在递归种,每次进入函数时都要分配一个参数变量n的地址空间。因此其空间复杂度也为O(n)。

一般情况下我们比较习惯性的使用递归,因为其代码简单,容易书写容易阅读。但是对于那种存储器不是特别多的机器(如树莓派,单片机等)最好是避免采用递归算法。采用以时间换空间的方法,如上述代码可改为:

int sum = 1;
for(int i = n; i > 0; i--){
	sum *= i;
}

以上代码也是求n!,但是其空间复杂度只是O(1),时间复杂度仍为O(n).

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值