算法的时间复杂度和空间复杂度计算

本文探讨了算法的复杂性,包括时间复杂度和空间复杂度,并通过实例展示了如何衡量算法效率。时间复杂度用O(f(n))表示,如线性O(n)、平方O(n^2)、指数O(2^n)等。空间复杂度关注算法运行所需存储空间。好算法的标准包括正确性、易读性、健壮性、高效性和低存储。递归算法要考虑栈空间。最后,强调设计算法时避免爆炸性复杂度。
摘要由CSDN通过智能技术生成

14天阅读挑战赛
努力是为了不平庸~。
数据结构+算法=程序。 数据结构是程序的骨架,算法是程序的灵魂。

算法复杂性

假如有这么一道题,要求序列之和:

− 1 , 1 , − 1 , 1 , . . . , ( − 1 ) n -1,1,-1,1,...,(-1)^n 1111...,(1)n

第一种实现方式:

int sum1(int n)
{
	int sum = 0;
	for (int i = 1; i <= n; i++)
		sum += pow(-1, i);
	return sum;
}

但是,为什么不这样运算呢:

0
-1
1
0
-1
1
0
-1
1
0 or -1
...
(-1)^n

这就数学家高斯使用的方式,对应的代码实现:

int sum2(int n)
{
	if (n % 2 == 0)
		return 0;
	
	return -1;

}

来看两者的执行时间比较:

#include <stdio.h>
#include <sys/time.h>
#include <math.h>

size_t get_time()
{
	struct timeval tm;
	gettimeofday(&tm, NULL);
	return tm.tv_usec;
}

int sum1(int n)
{
	int sum = 0;
	for (int i = 1; i <= n; i++)
		sum += pow(-1, i);
	return sum;
}

int sum2(int n)
{
	if (n % 2 == 0)
		return 0;
	
	return -1;

}

int main(int argc,char **argv)
{
	size_t start = get_time();

	int sum = sum1(10000);

	size_t end = get_time();

	printf("n=10000,sum1=%d, run %lu ms\n", sum,end - start);
	
	start = get_time();

	sum = sum2(10000);

	end = get_time();

	printf("n=10000,sum2=%d, run %lu ms\n", sum, end - start);
	return 0;
}

执行结果如下:

n=10000,sum1=0, run 377 ms
n=10000,sum2=0, run 0 ms

可以看到,第二种算法非常高效。第一种算法需要执行n次,而第二种算法只需要执行1次。

算法的定义

算法是对特定问题求解方法的一种描述。算法具有以下特性:
(1)有穷性。算法是由若干条指令组成的又穷序列,总能结束,不可能永不停止。
(2)确定性。每条语句都有确定含义,无歧义。
(3)可行性。可以通过有限次运算来实现。
(4)输入/输出。有零个或多个输入,以及一个或多个输出。

“好”算法的标准

(1)正确性。满足需求,能正常运行无错误,能通过测试。
(2)易读性。遵循命名规则,恰当的注释。
(3)健壮性。对非法数据及操作有较好的反应和处理。
(4)高效性。指算法运行效率高,即算法运行消耗的时间短。
(5)低存储。算法所需的存储空间小。

时间复杂度

算法时间复杂度是指算法运行所需的时间。我们将算法基本运算的执行次数作为时间复杂度的衡量标准。

int sum=0;                  //运行1次
int total=0;                //运行1次
for(int i=1;i<=n;i++){      //运行n+1次,最后一次判断不满足循环条件
  sum=sum+i;                //运行n次
  for(j=1;j<=n;j++)         //运行n×(n+1)次
    total=total+i*j;        //运行n×n次
}

上述算法运行次数: T ( n ) = 1 + 1 + n + 1 + n + n ∗ ( n + 1 ) + n ∗ n = 2 n 2 + 3 n + 3 T(n)=1+1+n+1+n+n*(n+1)+n*n=2n^2+3n+3 T(n)=1+1+n+1+n+n(n+1)+nn=2n2+3n+3
当n足够大时,算法的运行时间取决于最高项,小项和常数项可以忽略不计。
用极限表示为:
lim ⁡ n → ∞ T ( n ) f ( n ) = C ≠ 0 , C 为不等于 0 的常数 \lim \limits_{n \to ∞} \frac {T(n)}{f(n)}=C\neq0,C为不等于0的常数 nlimf(n)T(n)=C=0C为不等于0的常数
当 n 足够大时, T ( n ) 和 f ( n ) 近似相等,可以用 O ( f ( n ) ) 当n足够大时,T(n)和f(n)近似相等,可以用O(f(n)) n足够大时,T(n)f(n)近似相等,可以用O(f(n))表示时间复杂度渐近上限,衡量算法的时间复杂度。上述算法的时间复杂度就可以表示为 O ( f ( n ) ) = O ( n 2 ) O(f(n))=O(n^2) O(f(n))=O(n2)

i=1;             //运行1次
while(i<=n){     //可假设运行x次
  i=i*2;         //可假设运行x次
}

上述算法中,假设运行了x次才退出循环,i的值依次是 2 , 2 2 , 2 3 , . . . . . . , 2 x 2,2^2,2^3,......,2^x 22223......2x,i>=n结束,即 2 x = n 2^x=n 2x=n,那么 x = log ⁡ 2 n x=\log_2n x=log2n,运行次数是 1 + 2 log ⁡ 2 n 1+2\log_2n 1+2log2n,因此算法复杂度为 O ( f ( n ) ) = O ( l o g 2 n ) O(f(n))=O(log_2n) O(f(n))=O(log2n)

渐近复杂度是对算法运行次数的粗略估计,大致反映问题规模增长趋势。在计算渐近时间复杂度时,可以只考虑对算法运行时间贡献大的语句,忽略运算次数少的语句,比如循环语句中处于循环最内层的语句。

注意,不是所有的算法都能直接计算运行次数。
比如在数组中顺序查找元素并返回其下标,如果找不到返回-1。

int findx(int x){     //在a[n]数组中顺序查找x
    for(int i=0;i<n;i++){
        if(a[i]==x)
            return i; //查找成功,返回其下标i
    }
    return -1;        //查找失败,返回-1 
}

对于上述算法,很难计算其唯一的时间复杂度。
因此,有些算法可以分为最好、最坏和平均情况分别求算法的渐近复杂度。但是,算法通常考察的是最坏的情况,最坏情况对衡量算法的好坏具有实际意义。

空间复杂度

空间复杂度是指算法占用的空间大小,即算法在运行过程中占用了多少存储空间。算法占用的存储空间包括:
(1)输入/输出数据。
(2)算法本身。
(3)额外需要的辅助空间。

输入/输出数据是必须的,算法本身能缩减的空间很小,所以辅助空间才是衡量算法空间复杂度的关键因素。

void swap(int x,int y){    //交换x与y 
     int temp;
     temp=x;               //辅助空间 
     x=y;                  
     y=temp;               
}

上述算法中两数的交互过程如下图:

1
3
2
x
temp
y

使用了辅助空间temp,空间复杂度为 O ( 1 ) O(1) O(1)

注意,在递归算法中,每次递推都需要一个栈空间来保存调用记录,因此在分析算法的空间复杂度时需要递归栈的辅助空间。例如下面计算N的阶乘算法:

int func(int n){ //计算n的阶乘
    if(n==0||n==1) 
        return 1; 
    else 
        return n*func(n-1); 
}

假设n=5,其递推和回归过程如下:

递推
func(1)
5*func(4)
4*func(3)
3*func(2)
2*func(1)
回归
3*func(2)=6
func(1)=1
2*func(1)=2
4*func(3)=24
5*func(4)=120

上述过程是逻辑思维的推理,在计算机中使用栈存放上述过程,即后进先出的模式。

进栈
5*func(4)
func(1)
2*func(1)
3*func(2)
4*func(3)
进栈
5*func(4)
2*func(1)
3*func(2)
4*func(3)
进栈
5*func(4)
3*func(2)
4*func(3)
进栈
5*func(4)
4*func(3)
进栈
5*func(4)
出栈,func(5)=120
5*func(4)
出栈,func(4)=24
5*func(4)
4*func(3)
出栈,func(3)=6
5*func(4)
3*func(2)
4*func(3)
出栈,func(2)=2
5*func(4)
2*func(1)
3*func(2)
4*func(3)
出栈,func(1)=1
5*func(4)
func(1)
2*func(1)
3*func(2)
4*func(3)

从上图的进栈、出栈可以看到,子问题一步步压进栈,直到可解得到返回值,再一步步出栈,最终得到递归结果。运算过程中使用了n个栈空间作为辅助空间,因此阶乘递归算法的空间复杂度为 O ( n ) O(n) O(n)

再回到上述的算法代码中,n的阶乘仅比n-1的阶乘多了一次乘法运算 ( f u n c ( n ) = n ∗ f u n c ( n − 1 ) ) (func(n)=n*func(n-1)) (func(n)=nfunc(n1)),使用 T ( n ) T(n) T(n)表示 f u n c ( n ) func(n) func(n)的时间复杂度,则可以表示为:
T ( n ) = T ( n − 1 ) + 1 T(n)=T(n-1)+1 T(n)=T(n1)+1
= T ( n − 2 ) + 1 + 1 =T(n-2)+1+1 =T(n2)+1+1
. . . . . . ...... ......
= T ( 1 ) + . . . + 1 + 1 =T(1)+...+1+1 =T(1)+...+1+1
= n =n =n
即时间复杂度也是 O ( n ) O(n) O(n)

算法时间复杂度的种类

先看看一个例子:一个64格的棋盘,按照第1个格子放1粒麦子,第2个格子放2粒麦子,第3个格子放4粒麦子,第4个格子放8粒麦子,以此类推,每个格子里麦子的粒数是前一个格子的两倍,把64个格子放满究竟需要多少粒麦子?

乍一看,很少的感觉,那么来用数学计算一下。
把每个格子所需的麦子数加起来,总和为S,则: S = 1 + 2 1 + 2 2 + 2 3 + . . . + 2 63 S=1+2^1+2^2+2^3+...+2^{63} S=1+21+22+23+...+263
上述等式等号两边都乘以2,等式依旧成立: 2 S = 2 1 + 2 2 + 2 3 + . . . + 2 63 + 2 64 2S=2^1+2^2+2^3+...+2^{63}+2^{64} 2S=21+22+23+...+263+264
两个等式相减,得: S = 2 64 − 1 = 18446744073709551615 S=2^{64}-1=18446744073709551615 S=2641=18446744073709551615
按照一颗麦粒平均重量越41毫克,则总麦粒的总重量为: 18446744073709551615 × 41 = 756 , 316 , 507 , 022 , 091 , 616 , 215 ( 毫克 ) ≈ 7563000 (亿千克) 18446744073709551615\times 41= 756,316,507,022,091,616,215(毫克) \approx7563000(亿千克) 18446744073709551615×41=756,316,507,022,091,616,215(毫克)7563000(亿千克)

是不是很大,我们称这样的函数为爆炸性增量函数。如果算法的时间复杂度也是爆炸性增量,比如 O ( 2 n ) O(2^n) O(2n),后果不敢想象,随着n不断增大,导致程序、系统、服务之间宕机。

常见的算法时间复杂度有以下几类:
(1)常数阶。算法的运行次数是一个常数,比如2,10,18,100。时间复杂度通常用 O ( 1 ) O(1) O(1)表示。
(2)多项式阶。很多算法的时间复杂度是多项式,通常是 O ( n ) 、 O ( n 2 ) 、 O ( n 3 ) O(n)、O(n^2)、O(n^3) O(n)O(n2)O(n3)
(3)指数阶。算法的运行效率极差,时间复杂度通常是 O ( 2 n ) 、 O ( n ! ) 、 O ( n n ) O(2^n)、O(n!)、O(n^n) O(2n)O(n!)O(nn)等。
(4)对数阶。算法的运行效率较高,通常用 O ( log ⁡ n ) 、 O ( n log ⁡ n ) 、 O ( log ⁡ 2 n ) O(\log n)、O(n\log n)、O(\log_2 n) O(logn)O(nlogn)O(log2n)等表示。

指数阶增量随着n的增加而急剧增加,而对数阶增长缓慢。它们的关系如下:
O ( 1 ) < O ( log ⁡ n ) < O ( n ) < O ( n log ⁡ n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(\log n)<O(n)<O(n\log n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

设计算法时,需要注意算法复杂度增量问题,避免爆炸级增量。

总结

  1. 将程序执行次数作为时间复杂度衡量标准。
  2. 时间复杂度通常用渐进上界符号 O ( f ( n ) ) O(f(n)) O(f(n))表示。
  3. 衡量算法的好坏通常考察算法的最坏情况。
  4. 空间复杂度只计算辅助空间。
  5. 递归算法的空间复杂度需要计算递归使用的栈空间。
  6. 计算算法时要尽量避免爆炸级增量复杂度。

通过本节,对算法有初步的认识,算法不是凭空造出来的,而是来源于生活中的某些问题。算法的本质是高效的解决实际问题。

### 回答1: PHP 作为一种编程语言,并没有固定的算法时间复杂度空间复杂度。这些复杂度取决于所编写的算法实现,而不是编程语言本身。 例如,PHP 中的排序算法可能具有不同的时间复杂度空间复杂度,如冒泡排序、选择排序、插入排序、快速排序等。具体算法时间复杂度空间复杂度取决于算法的实现方式。 因此,在使用 PHP 进行算法开发时,需要特别注意算法时间复杂度空间复杂度,选择适合自己需求的算法,以获得更好的性能和效率。 ### 回答2: PHP算法时间复杂度是指算法执行所需的时间与问题规模的增长率之间的关系。常见的时间复杂度有常数时间O(1)、对数时间O(log n)、线性时间O(n)、平方时间O(n^2)等。在PHP中,根据具体的算法实现方式,时间复杂度可以不同。 在PHP中,一般来说,使用循环的算法通常会有较高的时间复杂度。例如,一个遍历数组并求和的算法,其时间复杂度为O(n),其中n是数组的长度。另外,PHP还提供了一些内置函数和数据结构,如排序函数sort()和二分查找函数array_search()等,它们的时间复杂度通常是比较高效的。 PHP算法空间复杂度是指算法所需的额外空间与问题规模的增长率之间的关系。常见的空间复杂度有常数空间O(1)、线性空间O(n)、平方空间O(n^2)等。在PHP中,空间复杂度通常是由变量、数组和函数调用所需的额外空间来衡量的。 在PHP中,空间复杂度较高的算法通常是由于需要创建额外的数据结构或临时变量来存储中间结果。例如,一个需要创建一个与输入规模n相关的数组来存储计算结果的算法,其空间复杂度为O(n)。 综上所述,PHP算法时间复杂度空间复杂度可以根据具体的算法实现方式而有所不同,但通常可以通过分析循环次数、临时变量的数量和额外数据结构的大小来进行评估和比较。在编写PHP算法时,我们应该尽量选择高效的时间复杂度和较低的空间复杂度,以提高算法的性能和效率。 ### 回答3: PHP算法时间复杂度空间复杂度取决于具体使用的算法数据结构时间复杂度是用来表示算法执行所需时间的度量,通常以大O表示。在PHP中,常见的时间复杂度包括O(1)、O(log n)、O(n)、O(n log n)和O(n^2)等。具体的算法实现会决定时间复杂度的大小。 空间复杂度是用来表示算法在执行过程中所需的额外空间的度量,也通常以大O表示。在PHP中,常见的空间复杂度包括O(1)、O(n)、O(n^2)等。具体的算法实现决定了空间复杂度的大小。 例如,对于PHP的数组排序算法,使用快速排序算法时间复杂度为O(n log n),空间复杂度为O(log n)。这是因为快速排序算法的平均时间复杂度为O(n log n),但需要额外的递归调用栈空间。另外,对于PHP的线性查找算法时间复杂度为O(n),空间复杂度为O(1),这是因为在执行过程中不需要额外的空间存储数据。 总而言之,PHP算法时间复杂度空间复杂度是评估算法性能和资源消耗的重要指标,具体取决于所使用的算法数据结构
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值