前言
如果没有看过上篇的博客,可以看下,有助于理解这篇。地址如下:https://blog.csdn.net/qq_69218005/article/details/130049547
3.空间复杂度
空间复杂度对一个算法在运行过程中临时占用额外存储空间大小的量度。
算法的空间复杂度通常用大 O O O符号表示。例如,如果一个算法需要使用n个元素的数组来存储数据,则它的空间复杂度为 O ( n ) O(n) O(n)。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
3.1 实例1:
// 计算BubbleSort的空间复杂度?
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 == 0)
break;
}
}
在BubbleSort中,只使用了常数级别的额外空间来存储循环中的一些变量,如end、exchange、i等。
这些变量在算法执行过程中不会随着输入规模n的增加而增加,因此空间复杂度为 O ( 1 ) O(1) O(1)。
3.2 实例2:
// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
if (n == 0)
return NULL;
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;
}
本案例用malloc函数创建了一个长度为n+1的动态数组fibArray,用于存储斐波那契数列的前n项。
在每次循环迭代中,都需要使用一个额外的数组元素来存储当前项的值,因此需要消耗 O ( n ) O(n) O(n)的空间。
3.3 实例3:
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
阶乘递归算法使用了递归实现,每次递归调用都会在栈上创建一个新的栈帧,其中包含了递归调用的参数和局部变量。
在此递归过程中,会创建 N N N个栈帧,因此需要消耗 O ( N ) O(N) O(N)的空间。
3.4 实例4:
// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
因此Fib的空间复杂度不是 O ( 2 N ) O(2^N) O(2N),而是 O ( N ) O(N) O(N)。时间是一去不复返的,是累积的;空间是可以重复利用的,是不累积的。
空间的销毁本质是归还使用权,并不是把空间毁灭掉。举个例子,你去酒店开房,使用时间一到,你要向酒店退房。
房间(看做空间)还在,而你对房间的使用权已经归还酒店(看做操作系统)了。
4. 常见复杂度对比
5. 复杂度的oj练习
5.1 消失的数字OJ
链接:https://leetcode-cn.com/problems/missing-number-lcci/
题目描述:数组nums
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在 O ( n ) O(n) O(n)时间内完成吗?
在数组nums
中查找缺失的整数可以使用以下三种方法:
方法一:排序法
先排序,依次查找,如果下一个数不等于上一个数+1,则上一个数+1就是缺失的数。
冒泡排序 O ( N 2 ) O(N^2) O(N2)+循环 N N N次比较,时间复杂度为 O ( N 2 ) O(N^2) O(N2)
快速排序时间复杂度为 O ( n log n ) O(n \log n) O(nlogn)
因此使用排序法最低也要 O ( n log n ) O(n \log n) O(nlogn),大于 O ( N ) O(N) O(N)
方法二:求和差值法
如果数组nums
中没有缺失整数,那么数组中所有元素的和应该等于从0到n的所有整数的和,即sum(n) = n * (n+1) / 2
。
因此,我们可以先计算出从0到n的所有整数的和,然后减去数组nums
中所有元素的和,得到的差值就是缺失的整数。
这种方法的时间复杂度为 O ( n ) O(n) O(n),因为需要遍历数组nums
两次,一次用于计算数组元素的和,一次用于计算从0到n的所有整数的和(也可
以直接用高斯公式求和)。
下面是代码实现:
int missingNumber(int* nums, int numsSize) {
int sum = numsSize * (numsSize + 1) / 2;
int arraySum = 0;
for (int i = 0; i < numsSize; i++) {
arraySum += nums[i];
}
return sum - arraySum;
}
方法三:位运算异或法
异或运算(^)可以将两个相同的数异或后得到0,而任何数与0异或后等于它本身。
下面是代码实现:
int missingNumber(int* nums, int numsSize) {
int x = 0; // 定义变量x,用于存储异或结果
for (int i = 0; i < numsSize; i++) {
x ^= nums[i]; // 对数组中的所有数字进行异或运算,得到结果x
}
for (int i = 0; i <= numsSize; i++) {
x ^= i; // 对0到numsSize的所有数字进行异或运算,得到结果y
}
return x; // 返回x和y的异或结果,即缺失数字
}
具体分析:
-
对数组中的所有数字进行异或运算,得到一个结果x。由于异或运算满足交换律和结合律,因此可以将所有相同的数字异或运算抵消,最终只剩下缺失数字和0之间的异或结果。
-
对0到numsSize的所有数字进行异或运算,得到一个结果y。由于0到numsSize是连续的数字,因此y实际上是0到numsSize的所有数
字的异或结果。
-
最后将x和y进行异或运算,得到的结果就是缺失的数字。
这个算法的时间复杂度为 O ( n ) O(n) O(n),其中n为数组的长度。这个时间复杂度是由两个for循环的时间复杂度决定的,因为这两个for循环的循环
次数都是n或n+1,而每次循环只进行了常数次操作,因此时间复杂度为 O ( n ) O(n) O(n)。
5.2 旋转数组OJ
链接:https://leetcode-cn.com/problems/rotate-array/
给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
思路一 暴力求解 旋转K次:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
时间复杂度: O ( n 2 ) O(n^2) O(n2) n ∗ k n*k n∗k, 最坏的情况 k = n − 1 k=n-1 k=n−1
空间复杂度: O ( 1 ) O(1) O(1) 无需开辟占用额外的空间
代码实现:
void rotate(int* nums, int numsSize, int k){
//暴力求解
k = k % numsSize; // 防止k大于numsSize
for (int i = 0; i < k; i++) {
int temp = nums[numsSize - 1];
for (int j = numsSize - 1; j > 0; j--) {
nums[j] = nums[j - 1];
}
nums[0] = temp;
}
}
思路二 空间换时间:
开辟一个临时的tmp空间,把逆序的内容拷贝到tmp中,然后把tmp的内容拷贝到a中
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
代码实现:
void rotate(int* nums, int numsSize, int k) {
k %= numsSize;
int* tmp = (int*)malloc(k * sizeof(int));//开辟临时空间存放k个元素
for (int i = 0; i < k; i++) {
tmp[i] = nums[numsSize - k + i]; //把旋转的k个元素放到tmp中
}
for (int i = numsSize - 1; i >=k; i--) {
//把n-k个元素往后移
nums[i] = nums[i - k];
}
for (int i = 0; i < k; i++) {
//把tmp中旋转的k个元素放到nums中
nums[i] = tmp[i];
}
free(tmp);
}
//用memcpy和memmove
void rotate(int* nums, int numsSize, int k) {
/* if (nums == NULL || numsSize <= 1 || k == 0) {
return;
}*/
k = k % numsSize;
/* if (k == 0) {
return;
}*/
int* tmp = (int*)malloc(k * sizeof(int));
if (tmp == NULL) {
return;
}
memcpy(tmp, nums + numsSize - k, k * sizeof(int));
memmove(nums + k, nums, (numsSize - k) * sizeof(int));
memcpy(nums, tmp, k * sizeof(int));
free(tmp);
}
memmove函数可以处理源内存和目标内存区域部分重叠的情况
思路三 三段逆置法:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
4 3 2 1 5 6 7 前n-k个逆置
4 3 2 1 7 6 5 后k个逆置
5 6 7 1 2 3 4 整体逆置
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1) 没有占用额外的空间
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) {
if (k > numsSize)
k %= numsSize;
reverse(nums, 0, numsSize - k - 1);//逆转n-k
reverse(nums, numsSize-k, numsSize - 1);//后k逆转
reverse(nums, 0, numsSize - 1);//整体逆序
}