文章摘要
空间复杂度衡量算法运行时所需的额外内存空间,用O(1)、O(n)等表示。通过生活化比喻帮助理解:做饭时案板空间(O(1)用一个碗反复盛菜,O(n)每个菜一个碗)、考试草稿纸使用等。常见空间复杂度包括O(1)(常数变量)、O(n)(线性空间)和O(n²)(平方空间)。递归的空间复杂度取决于调用深度。优化方法包括原地操作、使用一维数组和将递归转为循环。口诀总结空间复杂度与"碗/草稿纸"的关系,便于记忆。
一、什么是空间复杂度?
- 空间复杂度(Space Complexity)衡量的是:算法运行时,最多需要多少额外的内存空间(不包括输入本身)。
- 单位通常用 O(1)、O(n)、O(n²) 等。
二、生活化比喻
1. 做饭的案板
- 你做饭时,案板上能放多少菜、碗、刀具,就是你“做饭算法”的空间复杂度。
- 如果你只用一把刀切完所有菜,案板上只放一把刀,空间复杂度 O(1)。
- 如果你要把所有菜都切好分别放在碗里,案板上要放 n 个碗,空间复杂度 O(n)。
2. 考试用草稿纸
- 你做题时,草稿纸上写的内容越多,空间复杂度越高。
- 只用一行算式反复擦写,O(1)。
- 每道题都写下来,O(n)。
三、常见空间复杂度举例
1. O(1) —— 常数空间
只用极少的变量,不随输入规模变化。
int Sum(int[] arr)
{
int sum = 0; // 只用一个变量
for (int i = 0; i < arr.Length; i++)
sum += arr[i];
return sum;
}
比喻:你只用一个碗,反复盛菜。
2. O(n) —— 线性空间
需要一个和输入规模成正比的额外空间。
int[] CopyArray(int[] arr)
{
int[] copy = new int[arr.Length]; // 新开一个数组
for (int i = 0; i < arr.Length; i++)
copy[i] = arr[i];
return copy;
}
比喻:你有多少菜,就要准备多少个碗。
3. O(n²) —— 平方空间
需要一个二维数组,空间随输入的平方增长。
int[,] matrix = new int[n, n];
比喻:你要准备 n 行 n 列的碗,桌子都快放不下了。
四、递归的空间复杂度
递归调用会用到系统栈空间,每递归一层,栈就多一层。
比如:
void Print(int n)
{
if (n == 0) return;
Print(n - 1);
Console.WriteLine(n);
}
- 递归深度为 n,空间复杂度 O(n)。
比喻:每递归一层,就在桌子上多摞一张草稿纸。
五、常见算法空间复杂度对比
算法 | 空间复杂度 | 说明 |
---|---|---|
冒泡排序 | O(1) | 只用几个变量 |
归并排序 | O(n) | 需要辅助数组 |
快速排序 | O(log n) | 递归栈空间(平均情况) |
哈希表查找 | O(n) | 需要存储所有元素 |
动态规划(1维) | O(n) | 只存一行/一列 |
动态规划(2维) | O(n²) | 需要整个表格 |
六、口诀总结
- 空间复杂度 = 算法运行时“最多需要多少额外碗/草稿纸”
- O(1):只用几个碗,反复用
- O(n):有多少菜就要多少碗
- O(n²):桌子上摆满了碗,空间爆炸
- 递归要算栈空间,每递归一层多一张草稿纸
七、生活场景再补充
- 搬家打包:只用一个箱子反复装(O(1)),每样东西一个箱子(O(n)),每个房间一堆箱子(O(n²))。
- 考试答题:只用一张草稿纸反复擦写(O(1)),每道题一张草稿纸(O(n))。
八、如何优化空间复杂度?
- 能原地操作就原地操作(比如原地排序)
- 能用一维数组就不用二维
- 能用滚动数组就不用全量存储
- 递归能转循环就转循环
你可以直接把你想分析的C#代码贴出来,我会帮你详细分析空间复杂度。如果你还没有具体代码,下面我举几个常见的C#算法代码,逐行分析空间复杂度,你可以参考。
例1:原地反转数组
void Reverse(int[] arr)
{
int left = 0, right = arr.Length - 1;
while (left < right)
{
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
空间复杂度分析:
- 只用了3个变量(left, right, temp),不管数组多大,变量个数不变。
- 空间复杂度:O(1)(常数空间)
例2:归并排序
void MergeSort(int[] arr, int left, int right)
{
if (left >= right) return;
int mid = (left + right) / 2;
MergeSort(arr, left, mid);
MergeSort(arr, mid + 1, right);
Merge(arr, left, mid, right);
}
void Merge(int[] arr, int left, int mid, int right)
{
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right)
temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
for (int t = 0; t < temp.Length; t++)
arr[left + t] = temp[t];
}
空间复杂度分析:
- 每次合并都新建一个长度为区间长度的临时数组
temp
,总共需要O(n)的额外空间。 - 递归调用的栈深度是O(log n),但主导空间消耗是临时数组。
- 空间复杂度:O(n)
例3:递归求斐波那契数列
int Fib(int n)
{
if (n <= 1) return n;
return Fib(n - 1) + Fib(n - 2);
}
空间复杂度分析:
- 没有用额外数组,但递归调用会占用栈空间,最大递归深度为n。
- 空间复杂度:O(n)
例4:用List存储所有子集
List<List<int>> Subsets(int[] nums)
{
List<List<int>> res = new List<List<int>>();
int n = nums.Length;
int total = 1 << n; // 2^n
for (int mask = 0; mask < total; mask++)
{
List<int> subset = new List<int>();
for (int i = 0; i < n; i++)
{
if ((mask & (1 << i)) != 0)
subset.Add(nums[i]);
}
res.Add(subset);
}
return res;
}
空间复杂度分析:
- 最终结果
res
要存储所有子集,子集总数是2ⁿ,每个子集平均长度n/2。 - 空间复杂度:O(n * 2ⁿ)
例5:动态规划(二维DP)
int UniquePaths(int m, int n)
{
int[,] dp = new int[m, n];
for (int i = 0; i < m; i++) dp[i, 0] = 1;
for (int j = 0; j < n; j++) dp[0, j] = 1;
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
dp[i, j] = dp[i - 1, j] + dp[i, j - 1];
return dp[m - 1, n - 1];
}
空间复杂度分析:
- 需要一个m×n的二维数组。
- 空间复杂度:O(mn)
例6:哈希表计数
Dictionary<int, int> CountNumbers(int[] arr)
{
Dictionary<int, int> dict = new Dictionary<int, int>();
foreach (int num in arr)
{
if (!dict.ContainsKey(num))
dict[num] = 0;
dict[num]++;
}
return dict;
}
空间复杂度分析:
- 最坏情况下,数组中每个数都不同,哈希表要存n个键值对。
- 空间复杂度:O(n)
总结口诀
- 只用常量变量:O(1)
- 用数组/集合存输入相关数据:O(n)
- 递归要算栈空间:最大递归深度
- 存所有组合/子集/排列:O(2ⁿ)或O(n!)
- 二维表格:O(n²)
- 哈希表/字典:O(n)