空间复杂度(Space Complexity)
空间复杂度是衡量算法在运行过程中临时占用存储空间(内存)随输入规模增长的变化趋势的指标。和时间复杂度类似,它也用大O符号(Big-O Notation)表示,关注的是额外空间的占用,不包括输入数据本身占用的空间。
为什么需要空间复杂度?
- 内存限制:计算机内存有限,算法占用空间过大会导致程序崩溃(如递归过深导致栈溢出)。
- 优化资源:在内存紧张的设备(如嵌入式系统、手机)中,需优先选择省空间的算法。
- 与时间复杂度权衡:有时可以用空间换时间(如哈希表),反之亦然。
常见空间复杂度类型(从低到高)
大O表示 | 名称 | 例子 | 说明 |
---|---|---|---|
O(1) | 常数空间 | 变量交换、迭代计算 | 只占用固定大小的额外空间 |
O(n) | 线性空间 | 数组拷贝、递归调用栈 | 空间与输入规模成正比 |
O(n²) | 平方空间 | 二维矩阵、动态规划表 | 空间随输入规模平方增长 |
O(log n) | 对数空间 | 递归二分查找 | 递归深度为log n(如分治算法) |
如何计算空间复杂度?
- 关注额外空间:不包括输入数据本身占用的空间。
- 例如:排序算法的输入是数组,但计算空间复杂度时不统计数组本身,只统计算法额外分配的变量、数组、递归栈等。
- 分析变量与数据结构:
- 基本变量(如int、float)算O(1)。
- 动态分配的数据结构(如数组、哈希表)算O(n)。
- 递归调用:递归深度消耗的栈空间需计入。
经典例子分析
1. O(1) 常数空间
def swap(a, b):
temp = a # 仅用到一个临时变量temp,空间固定
a = b
b = temp
2. O(n) 线性空间
def copy_array(arr):
new_arr = [0] * len(arr) # 分配了一个与输入数组等长的新数组
for i in range(len(arr)):
new_arr[i] = arr[i]
return new_arr
3. O(n²) 平方空间
def generate_matrix(n):
matrix = [[0] * n for _ in range(n)] # 分配n×n的二维矩阵
return matrix
4. O(log n) 对数空间(递归)
def recursive_binary_search(arr, target, left, right):
if left > right:
return -1
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return recursive_binary_search(arr, target, mid+1, right) # 递归深度为log n
else:
return recursive_binary_search(arr, target, left, mid-1)
空间复杂度 vs 时间复杂度
- 时间换空间:某些算法通过重复计算减少内存占用(如斐波那契数列的递归解法)。
- 空间换时间:如哈希表通过预分配空间实现快速查找。
例子:斐波那契数列
# O(2ⁿ) 时间,O(1) 空间(尾递归优化前)
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
# O(n) 时间,O(n) 空间(动态规划)
def fib_dp(n):
dp = [0, 1] + [0] * (n-1)
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
# O(n) 时间,O(1) 空间(优化版)
def fib_optimized(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n+1):
a, b = b, a + b
return b
实际应用中的权衡
- 内存充足时:优先选择时间复杂度低的算法(如快速排序)。
- 内存紧张时:选择空间复杂度低的算法(如堆排序是原地排序,空间O(1))。
- 递归的陷阱:递归可能简洁但消耗栈空间(如树遍历的递归 vs 迭代实现)。
常见误区
- 误区1:认为“原地算法”一定不需要额外空间。(实际可能仍需O(1)的临时变量)
- 误区2:忽略递归调用的栈空间。(递归深度大时可能导致栈溢出)
- 误区3:混淆输入空间和额外空间。(如输入数组不算在空间复杂度内)