一、图书馆的故事:空间规划的艺术
有两家图书馆要存放 1000 本书:
-
图书馆 A:为每本书都准备一个独立的书架(1000 个书架)
-
图书馆 B:把书分类放在 10 个大书架上,每个书架有 100 层
假设每个书架占地 1 平方米:
-
图书馆 A 需要 1000 平方米
-
图书馆 B 只需要 10 平方米
如果书的数量增加到 10000 本:
-
图书馆 A 需要 10000 平方米(空间随书的数量线性增长)
-
图书馆 B 只需要 100 平方米(空间增长速度慢得多)
这就是空间复杂度的本质:
算法在运行过程中需要的内存空间如何随数据量的增加而变化。
二、常见空间复杂度的故事比喻
1. O (1):固定大小的储物柜
超市的储物柜有固定数量的格子,无论今天有 10 个顾客还是 100 个顾客,需要的格子数量都是一样的:
java
public class Locker {
private static final int SIZE = 100; // 固定100个格子
private boolean[] lockers = new boolean[SIZE]; // 占用固定空间
public boolean rentLocker() {
for (int i = 0; i < SIZE; i++) {
if (!lockers[i]) {
lockers[i] = true;
return true; // 租到格子
}
}
return false; // 没有空格子
}
}
空间复杂度:O(1)
特点:无论处理多少数据,占用的内存空间都是固定的。
2. O (n):为每个人发一件 T 恤
公司要为所有员工发 T 恤,员工数量越多,需要的 T 恤数量就越多:
java
public class TShirtDistributor {
public void distributeTShirts(int employeeCount) {
String[] tShirts = new String[employeeCount]; // 为每个员工创建一件T恤
for (int i = 0; i < employeeCount; i++) {
tShirts[i] = "Size: M, Color: Blue"; // 初始化T恤
}
// 分发T恤...
}
}
空间复杂度:O(n)
特点:需要的内存空间与数据量 n 成线性关系。
3. O (n²):学校的座位安排表
学校要为每个班级的每个学生安排座位,用一个二维数组记录:
java
public class School {
public void arrangeSeats(int classCount, int studentPerClass) {
String[][] seats = new String[classCount][studentPerClass]; // 二维数组
for (int i = 0; i < classCount; i++) {
for (int j = 0; j < studentPerClass; j++) {
seats[i][j] = "Class-" + i + ", Seat-" + j; // 安排座位
}
}
}
}
空间复杂度:O(n²)
特点:如果班级数和学生数都增加,需要的内存空间会呈平方级增长。
4. O (log n):递归查找家谱
在家族树中查找某个祖先,每次只需要记住当前分支的信息:
java
public class FamilyTree {
public boolean findAncestor(Person person, String targetName) {
if (person == null) return false;
if (person.getName().equals(targetName)) return true;
// 递归查找父节点和母节点
return findAncestor(person.getFather(), targetName) ||
findAncestor(person.getMother(), targetName);
}
}
空间复杂度:O(log n)
特点:递归调用会使用栈空间,但由于每次只处理一个分支,空间复杂度是对数级的。
三、如何计算空间复杂度:抓住关键内存分配
计算空间复杂度时,只需关注算法运行过程中创建的额外数据结构,忽略输入数据本身:
java
public void complexMethod(int[] array) {
// 输入数组array不算在空间复杂度内,因为它是输入数据
// 操作1:O(1) - 固定大小的变量
int sum = 0;
// 操作2:O(n) - 创建一个与输入数组大小相同的新数组
int[] copy = new int[array.length];
for (int i = 0; i < array.length; i++) {
copy[i] = array[i];
}
// 操作3:O(n²) - 创建一个二维数组
int[][] matrix = new int[array.length][array.length];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length; j++) {
matrix[i][j] = i + j;
}
}
// 总空间复杂度:O(1) + O(n) + O(n²) → 保留最高阶项 → O(n²)
}
四、时间复杂度 vs 空间复杂度:天平的两端
很多时候,我们需要在时间和空间之间做出权衡。例如:
问题:判断一个数组中是否有重复元素
方法 1:暴力查找(时间 O (n²),空间 O (1))
java
public boolean hasDuplicates(int[] array) {
for (int i = 0; i < array.length; i++) {
for (int j = i + 1; j < array.length; j++) {
if (array[i] == array[j]) return true;
}
}
return false;
}
特点:不需要额外空间,但时间效率低。
方法 2:使用哈希表(时间 O (n),空间 O (n))
java
public boolean hasDuplicates(int[] array) {
Set<Integer> set = new HashSet<>();
for (int num : array) {
if (set.contains(num)) return true;
set.add(num);
}
return false;
}
特点:时间效率高,但需要额外的哈希表空间。
五、优化空间复杂度的技巧:从图书馆故事中学到的
假设要处理一个包含 100 万个整数的数组,计算每个数的平方:
原始方法(O (n) 空间) :
创建一个新数组存储所有平方结果:
java
public int[] calculateSquares(int[] array) {
int[] result = new int[array.length]; // 需要O(n)空间
for (int i = 0; i < array.length; i++) {
result[i] = array[i] * array[i];
}
return result;
}
优化方法(O (1) 空间) :
直接在原数组上修改,不需要额外空间:
java
public void calculateSquaresInPlace(int[] array) {
for (int i = 0; i < array.length; i++) {
array[i] = array[i] * array[i]; // 直接修改原数组
}
}
六、递归算法的空间复杂度:调用栈的陷阱
递归算法会使用系统栈空间,可能导致空间复杂度升高。例如:
斐波那契数列(递归实现) :
java
public int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2); // 递归调用
}
空间复杂度:O(n)
原因:递归深度最大为 n,每次递归调用需要保存局部变量,因此栈空间为 O (n)。
迭代实现:
java
public int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
空间复杂度:O(1)
原因:只需要固定的几个变量,不使用递归栈。
七、总结:空间复杂度是内存使用的指南针
空间复杂度描述了算法的内存使用趋势,帮助我们在设计算法时做出明智选择:
-
优先选择低空间复杂度的算法(O(1) > O(log n) > O(n) > O(n²))
-
警惕递归算法的栈空间开销
-
在时间和空间之间寻找平衡点
记住:好的算法就像高效的图书馆管理员,用最小的空间存放最多的书籍。