时间复杂度和空间复杂度

算法复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢
空间复杂度主要衡量一个算法运行所需要的额外空间


时间复杂度

在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
换一种易于理解的说法:算法时间复杂度以算法中基本操作重复执行的次数(简称为频度)作为算法的时间度量。简单来说就是算法中的基本操作的执行次数

大O的渐进表示法

通常用大写O()来体现算法时间复杂度,我们称之为大O的渐进表示法
推导大O阶的方法:
1.用常数1取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。
得到的结果就是大O阶。
下面介绍一下常见的时间复杂度。

常数阶

    int a = 10, sum = 0;	//执行1次
	sum = a + 1;			//执行1次
	printf("sum = %d", sum);//执行1次

这个算法的执行次数函数是f(n)=3。根据我们推导大O阶的方法,第一步就是把常数项3改为1。在保留最高阶项时发现并没有最高阶项,所以这个算法的时间复杂度为O(1)。
如果这个算法中的语句sun = a + 1有10句,即:

    int a = 10, sum = 0;	//执行1次
	sum = a + 1;			//执行第1次
	sum = a + 1;			//执行第2次
	sum = a + 1;			//执行第3次
	sum = a + 1;			//执行第4次
	sum = a + 1;			//执行第5次
	sum = a + 1;			//执行第6次
	sum = a + 1;			//执行第7次
	sum = a + 1;			//执行第8次
	sum = a + 1;			//执行第9次
	sum = a + 1;			//执行第10次
	printf("sum = %d", sum);//执行1次

那么这个算法的时间复杂度又该是什么呢?
事实上无论n为多少,上面两段代码仅仅只是3次和12次执行的差异。这种与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶。
不管这个常数是多少,都记作O(1),而不能是O(3)、O(12)等其他任何数字。

对数阶

下面这段代码时间复杂度又是多少呢?

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

首先两个变量,循环条件 c o u n t < n count<n count<n,每循环一次count的值就会变成原来的二倍,就距离n更近了一步。也就是说,有多少个2相乘后大于n,就会退出循环。设有x个2相乘后跳出循环,由 2 x = n 2^x=n 2x=n得到 x = log ⁡ 2 n x=\log_{2}^{n} x=log2n。所以这个算法的时间复杂度为O( log ⁡ 2 n \log_{2}^{n} log2n)。称为对数阶。

线性阶

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

分析算法的复杂度,关键就是要分析循环结构的运行情况
这段代码中的循环语句会被执行n次,随着n的增加或减少,执行次数就会发生变化,就是说循环内语句的执行次数会随着n的变化而变化,所以说这段代码的时间复杂度是O(n)。

nlogn阶

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

内部while循环我们已经清楚它的时间复杂度为O( log ⁡ 2 n \log_{2}^{n} log2n),for循环将此时间复杂度的代码循环n遍,那么它的时间复杂度就是O( n l o g 2 n nlog_{2}^{n} nlog2n)。

平方阶

    int sum = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			sum += 10;
		}
	}

这段代码是一个循环嵌套,内循环刚才分析过是O(n),外层循环每循环一次,内层循环循环n次。所以说对于外层的循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以这段代码的时间复杂度为O( n 2 n^2 n2)。

    int sum = 0;
	for (int i = 0; i < m; i++)
	{
		for (int j = 0; j < n; j++)
		{
			sum += 10;
		}
	}

如果外循环的循环次数变为m,时间复杂度就变为O( m × n m\times n m×n)。

    int sum = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = i; j < n; j++)
		{		//j = i
			sum += 10;
		}
	}

此时内层循环的 j = i j = i j=i,当 i = 0 i = 0 i=0时,内层循环执行n次,当 i = 1 i = 1 i=1时,内层循环执行 n − 1 n - 1 n1次, ⋯ ⋯ \cdots\cdots i = n − 1 i = n - 1 i=n1时,内层循环执行了一次。
所以总的执行次数为 : : n + ( n − 1 ) + ( n − 2 ) + ⋯ + 1 n + (n - 1) + (n - 2) + \cdots +1 n+(n1)+(n2)++1
可以看出这是个等差数列,根据等差数列公式可以得出 : :
n + ( n − 1 ) + ( n − 2 ) + ⋯ + 1 = n ( n + 1 ) 2 = n 2 2 + n 2 n + (n - 1) + (n - 2) + \cdots +1 = \frac{n(n+1)}{2} = \frac{n^2}{2} + \frac{n}{2} n+(n1)+(n2)++1=2n(n+1)=2n2+2n
这时候就用推导大O阶的方法 : :
第一条,没有加法常数所以不考虑;
第二条,只保留最高阶项,所以保留 n 2 2 \frac{n^2}{2} 2n2
第三条,去除这个项相乘的常数,也就是除以 1 2 \frac{1}{2} 21
最终得出这段代码的时间复杂度为O( n 2 n^2 n2)。

立方阶

   int sum = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			for (int k = 0; k < n; k++)
			{
				sum++;
			}
		}
	}

最里层for循环循环语句时间复杂度为O(1),循环n次,时间复杂度为O(n),一共有三层循环,所以时间复杂度为O( n 3 n^3 n3),称为立方阶。
不一定两层循环时间复杂度就是O( n 2 n^2 n2),三层循环就是O( n 3 n^3 n3),这里只是两个简单的例子。

指数阶

long long Fib(size_t N)
{
	if (N < 3)
		return 1;
	return Fib(N - 1) + Fib(N - 2);
}

递归实现斐波那契数列的时间复杂度就是指数阶。
假设要计算Fib(5),只需要计算Fib(4)和Fib(3)的和。只需要计算一次即 2 0 2^0 20,但是并不知道Fib(4)和Fib(3)的值,所以还要计算一次Fib(4)和Fib(3),计算两次即 2 1 2^1 21,依次递归下去。
在这里插入图片描述

时间复杂度: 2 0 2^0 20 + 2 1 2^1 21 + 2 2 2^2 22 + ⋯ \cdots ⋯ \cdots + 2 ( N − 2 ) 2^{(N-2)} 2(N2) = 2 ( N − 1 ) − 1 2^{(N-1)}-1 2(N1)1
但是我们可以清楚的看到这并不是一棵满二叉树,即后面部分计算次数并不是 2 3 2^3 23,应该在原式的基础上再减去一个值:
2 ( N − 1 ) − 1 − 2^{(N-1)}-1- 2(N1)1 ____,把这个值减去之后看这个表达式可以发现增长最快的一项就是前面的 2 ( N − 1 ) 2^{(N-1)} 2(N1),所以时间复杂度就是O( 2 n 2^n 2n)。

除了以上几种时间复杂度外,还有O(n!),O( n n n^n nn),这两种时间复杂度太大了,除非n是很小的值,否则将会是噩梦般的运行时间。

空间复杂度

算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)= O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度

    int x = 10;
	int y = 20;
	++x;
	++y;

这段代码需要的空间就是常数量,变量x或变量y的值无论多大都不会影响空间的分配,所以空间复杂度O(1)。

    int* arr = (int*)malloc(sizeof(int) * N);
	for (int i = 0; i < N; ++i)
	{
		arr[i] = i;
	}

这段代码动态开辟了N个整形大小的空间,在64位平台下,int型大小为4个字节。所以当N为2时,开辟了两个整形空间占8个字节,当N为4时,开辟了4个整形空间占16个字节。占用空间的大小随着N的变化而变化,所以空间复杂度为O(n)。

上面的代码动态开辟了一位数组,当动态开辟二位数组时空间复杂度为多大呢?

    int** arr = (int**)malloc(sizeof(int*) * N);
	for (int i = 0; i < N; ++i)
	{
		*(arr + i) = (int*)malloc(sizeof(int) * N);
	}
	for (int i = 0; i < N; ++i)
	{
		for (int j = 0; j < N; ++j)
		{
			*((*(arr + i)) + j) = i + j;
		}
	}

每一行为N个整形大小,一共有N行,所以这段代码的空间复杂度为O( n 2 n^2 n2)。

    long long Fib(size_t N)
	{
		if (N < 3)
			return 1;
		return Fib(N - 1) + Fib(N - 2);
	}

函数的调用是通过栈来实现的,上面这段代码递归了N次,开辟了N个栈帧,每个栈帧使用常数个空间,所以空间复杂度为O(n)。

总结

对于一个算法,其时间复杂度和空间复杂度往往是相互影响的,当追求一个较好的时间复杂度时,可能会导致占用较多的存储空间,即可能会使空间复杂度的性能变差,反之亦然。不过,通常情况下,鉴于运算空间较为充足,通常以算法的时间复杂度作为算法优劣的衡量指标。
常见的时间复杂度所耗时间的大小排列:
O(1) < O( log ⁡ 2 n \log_{2}^{n} log2n) < O(n) < O(n l o g n log^n logn) < O( n 2 n^2 n2) < O( n 3 n^3 n3) < O( 2 n 2^n 2n) < O(n!) < O( n n n^n nn)
在这里插入图片描述

评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

北川_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值