目录
1.什么是空间复杂度
2.空间复杂度的计算
3.有复杂度要求相关OJ题
一.什么是空间复杂度
和时间复杂度类似,空间复杂度也是一个数学表达式,是指在一个算法在运行过程中临时占用的空间大小,而这个空间大小并不是具体到计算函数调用额外创建了多少字节的空间,因为这并没有很大的实际意义,空间复杂度计算的实际是额外创建的变量的个数!空间复杂度的计算方式和时间复杂度的计算方式相似,使用的也是大O的渐进表示法。
2.空间复杂度的计算
接下来我们来分析几个算法的空间复杂度:
//分析冒泡排序的空间复杂度
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void BubbleSort(int* a, int sz)
{
assert(a);
for (int i = 0; i < sz; i++)
{
int exchange = 0;
for (int j = 0; j < sz - 1 - i; ++j)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
exchange = 1;
}
}
if (0 == exchange)
{
break;
}
}
}
根据空间复杂度的定义,我们只需关注为了进行冒泡排序而额外开辟的空间,显然我们开辟的变量的个数是常数个,根据大O渐进表示法的规则可以看出,冒泡排序算法的空间复杂度是O(1)
案例二:计算斐波那契数组的元素:
//计算斐波那契数列数组的空间复杂度
long long* Fib(size_t n)
{
if (0 == n)
{
return NULL;
}
long long* FibArray = (long long*)malloc(sizeof(long long) * (n + 1));
if (NULL == FibArray)
{
return;
}
FibArray[0] = 1;
FibArray[1] = 1;
for (int i = 2; i < n+1; i++)
{
FibArray[i] = FibArray[i - 1] + FibArray[i - 2];
}
return FibArray;
}
我们同样根据定义来分析空间复杂度,这里首先malloc了一个可以放n+1个long long类型变量的空间,所以根据大O渐进表示法的规则,可以看出这里的空间复杂度是O(n)!
案例三:计算阶乘函数的空间复杂度
//计算阶乘的空间复杂度
long long Fac(size_t n)
{
if (0 == n)
{
return 1;
}
return n * Fac(n - 1);
}
函数在递归的时候会开辟新的栈帧空间,我们观察到阶乘函数一共递归了n次,也就是说总共开了n层栈帧空间,每次额外创建的变量是常数个,所以这个阶乘函数的空间复杂度是O(N)
案例四:计算递归写法的斐波那契数列的空间复杂度:
long long Fib(int N)
{
if (N < 3)
{
return 1;
}
return Fib(N - 1) + Fib(N - 2);
}
在时间复杂度的那篇博客里,我们分析了这种写法的时间复杂度是o(2^n),那么这个种写法的空间复杂度是否也是o(2^n),答案并不是!因为函数调用后空间是可以重复利用的!我们不妨来看这样一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
void f1()
{
int a = 10;
printf("a:: %p\n", &a);
}
void f2()
{
int b = 10;
printf("b:: %p\n", &b);
}
int main()
{
f1();
f2();
return 0;
}
我们知道函数调用结束后会释放对应的开辟的栈帧空间,假如说空间不是可重复利用的话,a和b的打印结果应该不一样,程序运行结果如下:
程序的运行结果表明:a和b的地址是相同的,那么也就可以反应一件事情:函数调用栈帧的销毁并不是真实地让一块空间消失,而是这块空间的使用权限被收回了!换言之,这块空间始终都存在
得知了空间可以重复利用的特性,我们就可以计算斐波那契数列递归写法的空间复杂度了:
我们知道这种递归写法的结束条件就是n==1或n==2,那么也就是说当调用到fib(2)的时候,函数返回到上一层后,上一层函数继续进行右路的递归的空间是复用返回结果那块空间,画个图:
从图片我们可以清晰的看到,实际上斐波那契数列的双路递归最终都只用了一侧递归所开辟的栈帧空间,那么开的栈帧空间就是调用fib(n)--->fib(1)也就是最多开辟了n个栈帧,所以双路递归斐波那契数列的空间复杂度是O(n)!
3.有复杂度要求相关OJ题
1.消失的数字:https://leetcode-cn.com/problems/missing-number-lcci/
这里介绍一下什么是OJ,OJ的全称是Online Judge就是在线判题平台,有两种类型:1.IO型:这种题目就是完完整整地写出一个程序,包括头文件主函数以及输入输出和解题逻辑 2.接口型:这种题型只需要完成对应函数的功能就可以了。
而LeetCode的题目都是接口型的题目,所以我们只要完成对应的函数就可以了。
这是对应的题目,要求我们在O(n)的时间内找到这个消失的数字,也就是我们只能线性遍历数组
方法一:做差法
注意到,这个数组是包含0-n的所有数字中少了一个数,那我们不妨将0-n的数据求和得到sum1,接着对数组元素求和得到sum2,最后做差即可,代码如下:
int missingNumber(int* nums, int numsSize){
//等差数列求和,使用移位运算效率会略优
int sum1=((numsSize)*(numsSize+1))>>1;
int sum2=0;
for(int i=0;i<numsSize;++i)
{ //对数组元素求和
sum2+=nums[i];
}
//做差即可得到缺失的数字
return sum1-sum2;
}
这样做确实是一种很不错的方式,但是存在一定的缺陷:当n过大的时候数据容易溢出,所以这种做法有一定的风险性,不过力扣上是可以通过的,接下来我们使用方法二:异或
方法二:异或
异或有两个性质:1.0和任意数异或不影响结果 2.相同的数异或相消
我们知道本题里只有1个数丢失,那么将数组里的数和0异或完后再与0-n的数异或得到的就是丢失的数字
int missingNumber(int* nums, int numsSize){
int ret=0;
for(int i=0;i<numsSize;++i)
{
ret^=nums[i];
}
for(int i=0;i<=numsSize;++i)
{
ret^=i;
}
return ret;
}
这种方式的不仅同样是O(n)的复杂度,并且保证数据不会溢出!
2.旋转数组 https://leetcode-cn.com/problems/rotate-array/submissions/
通过对测试用例的分析,我们不难可以想到对数组元素进行挪动从而实现右旋,但是这样做的话时间复杂度是O(N^2),但是数据范围是10^5次方,在O(N^2)的时间复杂度下就会超时,所以这种算法是不符合要求的!那么就有这样一很厉害的算法来实现这道题
逆置前n-k个,再逆置后k个,最后整体逆置
画图分析
那么根据这段算法,我们直接上手写代码
void reverse(int* nums,int left,int right)
{
while(left<right)
{
int tmp=nums[left];
nums[left]=nums[right];
nums[right]=tmp;
++left;
--right;
}
}
void rotate(int* nums, int numsSize, int k){
//逆置前n-k个
reverse(nums,0,numsSize-k-1);
//逆置后k个
reverse(nums,numsSize-k,numsSize-1);
//整体逆置
reverse(nums,0,numsSize-1);
}
接下来我们提交观察结果:
我们发现结果出错了,为什么呢?注意到这里的数组只有1个元素,但是却要右旋2次,那么在这种情况下调用reverse函数就会出现越界访问的情况!所以我们要处理这种k>numsSize的情况,注意到右旋numSize次得到的是和原数组一样的数组,也就是我们真正的等效k就是k%numsSize,所以我们把代码改进成这样
void reverse(int* nums,int left,int right)
{
while(left<right)
{
int tmp=nums[left];
nums[left]=nums[right];
nums[right]=tmp;
++left;
--right;
}
}
void rotate(int* nums, int numsSize, int k){
//逆置前n-k个
k%=numsSize;
reverse(nums,0,numsSize-k-1);
//逆置后k个
reverse(nums,numsSize-k,numsSize-1);
//整体逆置
reverse(nums,0,numsSize-1);
}
这样处理以后,我们的代码就正确了。
最后,我想说数据结构的学习比起学习C语言要难上很多,学好数据结构 的要领就是多思考、多画图、多写代码、多调试,这样你才能学好数据结构!希望大家共同勉励!!!!