[NLPCamp] 时间复杂度与空间复杂度-算法工程师不可忽略的重要指标

引言:

算法复杂度是算法工程师容易忽略的指标,尤其对于转入AI行业的算法工程师们,可能对算法复杂度的概念更是陌生,但是实际情况中,算法的复杂度会直接影响算法的执行效率,是非常重要的评估指标,本文从时间复杂度和空间复杂度的概念出发,结合实际的一些代码分析时间复杂度与空间复杂度,并总结了一些评估时间复杂度和空间复杂度的一点小小的经验,如果有说错的地方,希望大神们帮忙指正、赐教。

正文:

在实现算法的时候,通常会从两方面考虑算法的复杂度,即时间复杂度和空间复杂度。顾名思义,时间复杂度用于度量算法的计算工作量;空间复杂度用于度量算法占用的内存空间。

 

渐进时间复杂度:

理论上来说,时间复杂度是算法运算所消耗的时间,但是对于一个算法来说,评估运行时间是很难的,因为针对不同大小的输入数据,算法处理所要消耗的时间是不同的,因此通常关注的是时间频度,即算法运行计算操作的次数,记为T(n),其中n称为问题的规模。同样,因为n是一个变量,n发生变化时,时间频度T(n)也在发生变化,我们称时间复杂度的极限情形称为算法的“渐近时间复杂度”,记为O(n)。因为我们没办法用通常的评估方式来评估所有算法的时间复杂度,因此使用渐进时间复杂度表示算法的时间复杂度,下文中的时间复杂度均表示渐进时间复杂度。

我们在这里放一个python算法的例子来解释一下:

def f(n):
	a,b=0,0#运行消耗t0时间
	for i in range(n):#运行一次平均消耗t1时间
		a = a + rand()#运行一次平均消耗t2时间
	for j in range(n):#运行一次平均消耗t3时间
		b = b + rand()#运行一次平均消耗t4时间

 

在这个例子中,我们分别计算f(n)函数的时间复杂度与空间复杂度。根据代码上执行的平均时间假设,计算出来执行f(n)的时间为T(n)=t0+(t1+t2)*n+(t3+t4)*n=t0+(t1+t2+t3+t4)*n;而函数中申请了两个变量a,b,占用内存空间为2。上述T(n)是我们对函数f(n)进行的准确时间复杂度的计算。但实际情况中,输入规模n是影响算法执行时间的因素之一。在n固定的情况下,不同的输入序列也会影响其执行时间。当n值非常大的时候,T(n)函数中常数项t0以及n的系数(t1+t2+t3+t4)对n的影响也可以忽略不计了,因此这里函数f(n)的时间复杂度我们可以表示为O(n)。我们再来看空间复杂度,跟时间复杂度表示类似,也可以用极限的方式来表示空间复杂度(但是貌似没有渐进空间复杂度的说法),因为这里只声明了2个变量,因此空间复杂度也是常数阶的,因此这里空间复杂度计算为O(1)。

 

时间复杂度与的计算方法:

因为我们计算的是极限状态下的时间复杂度,因此存在两种特性:

1.按照函数数量级角度来说,相对增长低的项对相对增长高的项产生的影响很小,可忽略不计。

2.最高项系数对最高项的影响也很小,因此也可以忽略不计。针对第1点,常见的时间复杂度有:常数阶:常数阶:O(1),对数阶:O(log_2 n),线性阶:O(n),k次方阶:O(n^K),指数阶:O(2^n)。

根据上述两种特性,总结时间复杂度的计算方法:

1.只取相对增长最高的项,去掉低阶项;

2.去掉最高项的系数;

3.针对常数阶,取时间复杂度为O(1)。

 

空间复杂度:

空间复杂度指的是算法在内存上临时占用的空间,包括程序代码所占用的空间,输入数据占用的空间和变量占用的空间。在递归运算时,由于递归计算是需要使用堆栈的,所以需要考虑堆栈操作占用的内存空间大小。空间复杂度的计算也遵循渐进原则,即参考时间复杂度与空间复杂度计算方法项。

 

空间复杂度的计算方法:

1.通常只考虑参数表中为形参分配的存储空间和为函数体中定义的局部变量分配的存储空间(比如变量a=0在算法中空间复杂度为O(1);list_a=[0,1,....,n]的空间复杂度为O(n);set(list_a)的空间复杂度为O(1))。

2.递归函数情况下,空间复杂度等于一次递归调用分配的临时存储空间的大小乘被调用的次数。

我们这里以递归方法实现的斐波那契数列为例:

def fib(n):
    if n < 3: 
        return 1
    else:
        return fib(n-2)+fib(n-1)

斐波那契数列的序列依次为1,1,2,3,5,8,13.....特点是,当数列的长度大于等于3时,数列中任意位置的元素值等于该元素前两位元素之和。

1.计算时间复杂度:O(2^n)。计算方法:通过归纳证明的方法,我们尝试计算数列中第8个元素的值的递归调用次数,为了方便观察,我把外层括号替换为了大括号。fib(8)=fib(7)+fib(6)={fib(6)+fib(5)}+{fib(5)+fib(4)}=({fib(5)+fib(4)}+{fib(4)+fib(3)})+({fib(4)+fib(3)}+{fib(3)+fib(2)})=.....这里太多我就不一一写出了。不难发现,每次调用递归时,递归调用次数都是以程序中调用递归次数即2的指数形式增长的。第一层递归时,调用了2次fib(n)函数;第二层递归时,第一层的2次递归调用分别又要调用2次,即调用了2^2次;第三层递归调用了2^3次,以此规律,不难算出时间复杂度为O(2^n)

2.计算空间复杂度:O(n)。计算方法:我们同样使用归纳证明的方法,尝试推导数列中第6个元素值的内存占用情况。调用函数fib(6),此时因为有形参n传递,在栈中为n申请内存资源,我们为了方便,以fib(6)表示栈中元素。此时栈中有fib(6);我们根据函数内的递归调用关系,为了计算fib(6),我们需要fib(5)和fib(4)的值,此时发生形参传递,栈中有fib(6),fib(5),fib(4);为了计算fib(4),我们需要fib(3)和fib(2)的值,此时栈中有fib(6),fib(5),fib(4),fib(3),fib(2),但是由于fib(2)=1,此时fib(2)函数计算完成,fib(2)出栈,此时栈中有fib(6),fib(5),fib(4),fib(3)。为了计算fib(3),需要fib(2)和fib(1)的值,此时栈中有fib(6),fib(5),fib(4),fib(3),fib(2),fib(1)。但是fib(2)=1,fib(1)=1,计算完成,fib(2)和fib(1)出栈。此时得到fib(3)的值为2,fib(3)出栈;由此出栈顺序,fib(4),fib(5),fib(6)也会随计算完成出栈。不难发现,在此次递归计算的过程中,内存中最多消耗了6个内存资源,由归纳证明法得出fib(n)的空间复杂度为O(n)。

 

Master Theorem(主方法):

Master Theorem是为了计算含有递归调用的算法的时间复杂度的。因为算法中如果含有递归调用算法的情况下使用归纳证明的方法计算时间复杂度是相当困难的,因此我们需要利用Master Theorem来帮我们计算复杂情况下的算法的时间复杂度,Master Theorem定理如下:

Master Theorem的一般形式是T(n) = a T(n / b) + f(n), a >= 1, b > 1。递归项f(n)理解为一个高度为log_b n 的a叉树, 这样总时间频次为 (a ^ log_b n) - 1, 右边的f(n)假设为 nc 那么我们对比一下这两项就会发现 T(n)的复杂度主要取决于 log_b a 与 f(n) 的大小。(log_b a表示以b为底,a的对数)因此我总结了使用Master Theorem的三种case的简单判断:

1.计算log_b a的值,比较n^(log_b a)与f(n)的大小。

2.若n^(log_b a)>f(n),时间复杂度为O(n^(log_b a)) (case 1)

3.若n^(log_b a)<f(n),时间复杂度为O(f(n)) (case 3)

4.若n^(log_b a)=f(n),时间复杂度为O(n^(log_b a)*(log n)^k+1) (case 2) (其中k值为f(n)中如果有log对数项时,该对数项的指数为k,例如,如果f(n)=log n ,k=1;f(n)=n,k=0)

 

可能公式理解起来有点困难,举几个例子来加深理解:

常规套路呢,就是比较n^(log_b a)与f(n)的值了,比较出来就可以套用上述公式,但是一定要注意公式中的限制条件,如a必须为常数项等。

以上是关于时间复杂度和空间复杂度计算的所有内容了,写的比较渣,如果有写错或者理解错的地方,还请大神们多多指教。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值