【数据结构】时间复杂度与空间复杂度

【前言】时间复杂度和空间复杂度非常重要,是数据结构必须要了解的部分,不仅是学校考试,大厂笔试,甚至于和人描述你的算法优异,都可以从复杂度上入手。

目录

1.时间复杂度

2.空间复杂度

3.刷题练习


1.时间复杂度

算法效率有两种,一种叫时间效率,一种叫空间效率。时间效率被称为时间复杂度,空间效率被称为空间复杂度。

算法中基本操作的执行次数,为算法的时间复杂度。

void func1(int N) {
	int count = 0;
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < N; j++) {
			++count;
		}
	}
	for (int k = 0; k < 2 * N; k++) {
		++count;
	}
	int M = 10;
	while (M--) {
		++count;
	}
	printf("%d", count);

}

那么这个函数基本操作执行了多少次呢?

F(N)=N的平方+2*N+10

但是随着N的增大,我们可以发现这个表达式中N的平方对结果的影响是最大的。

时间复杂度是一个估算,是去看表达式中影响最大的那一项。

大O的渐进表示法:

1.用常数1取代运行时间中所有加法常数。

2.在修改后的运行次数函数中,只保留最高阶项

3.如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到结果就是大O阶了。

可以得到O(N的平方)

再来看一个例子

void func2(int N, int M) {
	int count = 0;
	for (int k = 0; k < M, k++) {
		++count;
	}
	for (int k = 0; k < N; k++) {
		++count;
	}



}

其时间复杂度为O(N+M),因为其中的N与M是未知的。

若假设M远大于N,则为O(M)。

若假设M与N差不多大,则为O(M)或者O(N)。因为此时相当于2倍的M或者2倍的N。按照渐进法就可以取掉2倍了。

若NM给出了具体数值,则遵循渐进法第一条,确定的常数次其时间复杂度为O(1)。

还有些算法需要分情况计算。

存在着最好,平均,与最坏的情况。

最坏:任意输入规模的最大运行次数    O(N)

平均:任意输入规模的期望运行次数    O(N/2)

最好:任意输入规模的最小运行次数    O(1)

例如:在长度为N中的数组寻找一个数据x,可能会在任意的位置找到。所以针对这个情况会有如上的三种复杂度。

如果长度不变,则为O(1)

如果长度线性变长  则为O(N)

但是当三种情况都存在,应当考虑最坏的情况

可以思考一下冒泡排序法的时间复杂度,情况都是答案为O(N的平方)

第一趟冒泡,随着指针向后推移,会进行N次比较

第二趟冒泡,随着指针向后推移,会进行N-1次比较

以此反复,会进行总数为1到N的一个等差数列求和的次数的比较。再通过大O阶算法就可以得到结果了。

二分查找法

假设有一个有序的数组,其值为0,1,2,3,4,5,6,7,8,9

那么我们先从中间找,取到4,如果4就是我们要的值,那么O(1),此为最好的情况。

如果我们要找的值比4更大,就可以缩放区间,将区间定位为5-9这个位置。然后再取5-9的中间,如此反复,进行折半查找

那么假设找了X次,每一次折半都会使区间变化,第一次为n(假设n个元素),折半后为n/2,X次折半后为(n/2)^X ,在最坏的情况下,最后区间为1,即有(n/2)^X=1 ,

即得到了X=log2n(格式有误,2为底数,n为真数)

X也就是二分查找的时间复杂度,可以得到O(logn)(n为真数,底数省略,也可以不省略)

再看一道题

long long factorial(size_t N)
{
	return N < 2 ? N : factorial(N - 1) * N;
}

比如我们输入的N为5,则通过三目运算返回factorial(4)*5,其实质就是一个递归调用,此时需要取计算factorial(4)的值,就又如此反复的循环起来了。

下一个得到的就是factorial(3)*4,直到factorial(2)*3,,factorial(1)*2 ,这样递归也就结束了,那么开始逐层退出,运算factorial(1)的值,可以算出为1,有了factorial(1)的值,就可以返回计算factorial(1)*2的值,同理往上就可以得到factorial(4)*5的值。

逐层进入(递归)

层次实参调用形式需要计算的表达式需要等待的结果
1n=5factorial(5)factorial(4) * 5factorial(4) 的结果
2n=4factorial(4)factorial(3) * 4factorial(3) 的结果
3n=3factorial(3)factorial(2) * 3factorial(2) 的结果
4n=2factorial(2)factorial(1) * 2factorial(1) 的结果
5n=1factorial(1)1

逐层退出

层次调用形式需要计算的表达式返回值表达式的值
 
5factorial(1)11
4factorial(2)factorial(1) * 2factorial(1) 的返回值,也就是 12
3factorial(3)factorial(2) * 3factorial(2) 的返回值,也就是 26
2factorial(4)factorial(3) * 4factorial(3) 的返回值,也就是 624
1factorial(5)factorial(4) * 5factorial(4) 的返回值,也就是 24120

这样就得到了5!。 

如果计算的是N!,那么递归调用了N次,每次递归运算了3次(本题是三目操作),那么得到O(3N)

通过大O阶得到O(N)

到这里基本上常见的时间复杂度就这些了。

通过计算可以发现logn这个时间复杂度非常nice,就拿他和O(n)进行对比,当n等于10亿的时候,O(logn)也不过等于30,非常小。所以可见二分查找的优异非常明显。其缺点也非常明显,需要有序才能进行二分查找。 

2.空间复杂度

 空间复杂度不算空间,而是对变量的个数进行计算。

空间复杂度也使用大O阶渐进表示法。

void bubblesort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 1)
		{
			break;
		}
	}
}

 其变量个数为5,也遵循大O阶,所以为O(1)

就算是循环,在第一次时开辟了空间,循环了N次那么空间也用了N次,但需要记住的是

时间是累积的,但空间不累计。就算出了{},也就是作用域,空间被销毁,下一次进入时也仍然复辟并沿用此空间。感兴趣的可以了解一下栈帧

long long* fibarray =(long long)malloc(n+1) *sizeof(long long)

空间复杂度为O(N),因为存在malloc函数。这里的malloc开辟了一个数组,长度为n+1

再来看之前的例子,计算其空间复杂度

long long factorial(size_t N)
{
	return N < 2 ? N : factorial(N - 1) * N;
}

从factorial(10)到factorial(1)*2,每一层函数调用都要开辟栈帧

递归调用了N层,那么每次调用建立一个栈帧(每个栈帧独立使用),每个栈帧使用了常数个空间,即每一层都是O(1)。并且我们有N个栈帧,那么就可以得到空间复杂度为O(N)。

虽然空间在使用完后都会销毁,但空间复杂度本质上是在寻找最坏的情况下同时需要多少空间。

并且在调用时建立栈帧,返回时进行销毁。

3.刷题练习

题1 消失的数字

数组nums包含从0到n的所有整数,但缺了一个。请编写代码找出缺失整数。在O(n)内完成。

思路1:

先排序,再遍历,看是否后面一个数比前面大1

但是不符合题要求。哪怕是最快的排序也是O(N*logN)

思路2:

将数组中所以数加到一起,其结果记为ret1,再将0到n所有数加到一起为ret2,两者相减即得到需要寻找的数字。

思路3:异或。

比如2与3的异或,先写出2与3的二进制

2: 10

3: 11    异或的结果是01

异或的特点为相同为0相异为1按位异或。(二进制)

那么我们可以看出,相同的数字在异或之后,将会得到0

所以我们将待测数组的值依次跟0-n的所有数进行异或,剩下的值就是没有的那个数字。

而且要求有序进行。

例如:

1 3 1三个数进行异或,不管是什么顺序,最后得到的都是11,也就是3这个值。

也就是说不管两个1之间有多少其他值,最终两个1都会异或没

#include<stdio.h>
int main()
{
	int a = 0;
	int num[] = { 0,1,2,3,4,6,7,8,9};
	//a先与数组中的数进行异或。
	for (int i = 0; i < sizeof(num)/sizeof(num[1]); i++)
	{
		a ^= num[i];
	}
	// a再与0-n的所有数异或。
	for (int j = 0; j < sizeof(num) / sizeof(num[1]) + 1; j++)
	{
		a ^= j;
	}
	printf("%d", a);
}

 其实总结下来也就是不带进位的加法啦。

题2 旋转数组

输入nums=【1,2,3,4,5,6,7】,k=3

输出【5,6,7,1,2,3,4】

解释:

第一次旋转【7,1,2,3,4,5,6】

第二次旋转【6,7,1,2,3,4,5】

第三次旋转【5,6,7,1,2,3,4】

思路1

最直接的进行旋转

函数部分:

void rotate(int* nums, int numsize, int k)
{
	while (k--)
	{
		int temp = nums[numsize - 1];
		for (int i = numsize - 2; i >= 0; --i)
		{
			nums[i + 1] = nums[i];
		}
		nums[0] = temp;
	}
}

但如果给出的数组过大,可能效率十分低下。

思路2

空间换时间。进行一个整体移动。

例如k=3,将5,6,7放到一个新数组的前位,将1,2,3,4放到新数组的后面。

思路3

将后k个逆置,前n-k个逆置,再整体逆置。

void reverse(int *nums,int left,int right)
{
	int temp;
	while (left < right)
	{
		temp = nums[left];
		nums[left] = nums[right];
		nums[right] = temp;
		++left;
		--right;
	}

}
void rotate(int* nums, int numsize, int k)
{
	reverse(nums, numsize - k, nums - 1);
	reverse(nums, 0, numsize -k- 1);
	reverse(nums, 0, numsize - 1);
}

但仍然会有问题。可能出现数组越界

例如k=13,使得出现负数。

但换个思路,旋转13次等于旋转13%7,为6次,所以就相当于旋转了6次。

if (k >= numsize)
	{
		k %= numsize;
	}

添加如上的限制就可以解决。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值