1、空间复杂度
- 空间复杂度指的是临时占用存储空间大小的量度;
- 空间复杂度计算的是变量的个数,也采用大O渐进表示法;
- 由于函数在运行的时候所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译器已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定!
示例1:冒泡排序
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = 0; end > 1; --end)
{
int exchange = 0;
for (size_t i = 0; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange = 0);
break;
}
}
这里只创建了3个变量,为常数个,因此空间复杂度为O(1)。
示例2:斐波那契数列1
long long* Fibonacci(size_t n)
{
if (n == 0)
return Fibonacci;
long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
其空间复杂度为O(n);
示例3:递归
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1) * N;
}
累计调用Fac从Fac(N) ~ Fac(0)共调用n + 1次(要创建函数栈帧),因此空间复杂度为O(n)
下面F1()和F2()打印的值一样吗?
void F1()
{
int b = 0;
printf("%p\n",&b);
}
void F2()
{
int a = 0;
printf("%p\n",&a);
}
int main()
{
F1();
F2();
return 0;
}
Vs下打印的结果相同,在同一块区域进行相同的函数栈帧的创建和销毁!
示例4:斐波那契数列2(加法)
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
空间复杂度为:O(N)!
原因:递归的过程中空间是不断创造和销毁的过程,调用Fib()时先在一侧调用完后返回,此时空间被销毁,然后再重新创建新的空间的过程!
GTP解释:
-
1. 递归栈空间
在递归算法中,空间复杂度通常与递归调用的深度有关。每次进行递归调用时,程序会为当前函数调用分配一个栈帧,栈帧会记录函数调用的参数、返回地址和局部变量等信息。
对于 Fibonacci 函数:
- 在最坏的情况下,递归深度会达到
N
(例如Fib(N)
需要调用Fib(N-1)
和Fib(N-2)
,一直递归下去),最深的时候调用栈的深度是N
。 -
2. 空间复杂度的计算
- 因此,空间复杂度是 O(N),这是因为在递归过程中最深的栈帧深度为
N
,而且每个栈帧占用的空间是常量级别的。
时间不可重复利用,空间可以重复利用!
示例5:转轮数组
. - 力扣(LeetCode)
直接旋转暴力求解无法通过!
解法一:
解决代码:
void reverse(int* nums,int begin,int end)
{
while(begin < end)
{
int tmp = nums[begin];
nums[begin] = nums[end];
nums[end] = tmp;
++begin;
--end;
}
}
void rotate(int* nums, int numsSize, int k) {
if (k > numsSize)
k = k%numsSize;
reverse(nums,0,numsSize-1-k);
reverse(nums,numsSize-k,numsSize-1);
reverse(nums,0,numsSize-1);
}
把++/--放在前面!
这个代码的时间复杂度为O(N),空间复杂度为O(1)
线上OJ分为两种:
- IO型:通过scanf输入条件,结果通过printf输出,实现一个完整的程序
- 接口型:输入条件,参数,结果,返回值返回,实现一个函数,只有部分程序
- 编译错误指的是语法错误,运行错误不通过可能是由于时间复杂度
解法二:以空间换时间
创建一块空间,将后k个拷贝到前面去,将前k个拷贝到后面去!(1G大概为10亿字节)
示例代码:
void rotate(int* nums, int numsSize, int k)
{
if (k > numsSize)
k = k%numsSize;
int* tmp = (int*)malloc(sizeof(int)*numsSize);
memcpy(tmp,nums + numsSize -k,sizeof(int)*k);
memcpy(tmp+k,nums,sizeof(int)*(numsSize-k));
memcpy(nums,tmp,sizeof(int)*numsSize);
}
free(tmp);
tmp = NULL;
时间复杂度和空间复杂度都为O(N)!
二、顺序表和链表
1、线性表
线性表是n个具有相同特征的数据元素的有限序列!
常见的线性表有:顺序表、链表、栈、队列、字符串等
线性表在逻辑上是线性结构,也就是连续的一条直线,但是物理结构(内存中存储)不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储!
2、顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般分为:静态顺序表和动态顺序表
1、静态顺序表:使用定长数组存储元素;
typedef int SLDataType;
#define N 100000
struct SeqList
{
SLDateType a[N];
int size;
}
2、动态顺序表:使用动态开辟的数组存储;
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size; //有效数据个数
int capacity; //空间容量
}SL;
typeef struct SeqList的意思为:将struct SeqList重命名为SL!
当有效数据个数 = 空间容量的时候!进行扩容!
对数据的操作分四种:增删查改!
void SeqList(SL s); // 初始化
void SeqDestory(Sl s); // 删除
全局变量会自己初始化(不初始化也不会报错);局部变量需要自己进行初始化!
需要注意的是,写顺序表初始化的时候,需要传入的是结构体的地址!这样子才能对传入的结构体进行改变,否则只是对临时拷贝的结构体进行改变。
凡是看到错误:无法解析符号 ---- 只有声明没有定义!
顺序表的销毁:将空间释放,然后将指针置为空,再将有效字节和空间大小设置为0;
free空间的时候只能从该空间的起始位置释放,如果从中间位置或者空指针释放会报错!
尾删的时候不能释放单个空间!(创建的空间只能一起释放!)