一句话总结
时间复杂度是你的代码运行有多快,空间复杂度是你的代码需要多少内存,就像做饭——时间是你炒菜要几分钟,空间是你的灶台能放几个锅。
一、时间复杂度:代码的“运行效率”
时间复杂度衡量的是一个算法的运行时间随着输入数据量(n)的增长而变化的趋势,通常用大O符号表示。我们关注的是当 n 趋近于无穷大时的渐近趋势,而忽略常数、低阶项和系数,这使得我们能从宏观上评估算法的优劣。
1. O(1) 常数时间
无论输入数据量多大,算法都只需固定数量的操作步骤。这是最理想的算法性能。
- 代码示例:获取数组的第一个元素。
fun getFirst(arr: IntArray): Int? = arr.firstOrNull()
- 分析:无论数组中有10个还是100万个元素,
firstOrNull()操作都只执行一次,因此其时间复杂度为 O(1)。
2. O(log n) 对数时间
每增加一次操作,输入数据量就会指数级地减小。典型的例子是二分查找。
- 代码示例:在有序数组中查找一个元素。
- 分析:假设数组有16个元素,我们只需4次查找就能定位目标(log_216=4)。每增加一倍的数据,只多一次查找。这种效率在处理海量数据时尤为重要。
3. O(n) 线性时间
算法的执行时间与输入数据量呈线性关系。当数据量翻倍,执行时间也大致翻倍。
- 代码示例:遍历数组寻找最大值。
fun findMax(arr: IntArray): Int? {
if (arr.isEmpty()) return null
var max = arr[0]
for (num in arr) {
if (num > max) max = num
}
return max
}
- 分析:算法需要遍历数组中的每个元素,执行操作的次数与数组大小 n 成正比,因此时间复杂度为 O(n)。
4. O(n log n) 线性对数时间
这是许多高效排序算法的性能瓶颈,例如快速排序和归并排序。
- 分析:这类算法通常将大问题分解成子问题,并在每个子问题上进行线性时间的操作,其效率优于平方时间,但逊于线性时间。
5. O(n²) 平方时间
算法的执行时间与输入数据量的平方成正比。通常由两层嵌套循环引起,随着数据量增长,性能会急剧下降。
- 代码示例:使用嵌套循环检查数组中是否存在重复元素。
fun hasDuplicate(arr: IntArray): Boolean {
for (i in arr.indices) {
for (j in i+1 until arr.size) {
if (arr[i] == arr[j]) return true
}
}
return false
}
- 分析:对于一个大小为 n 的数组,内层循环的执行次数近似于 n 次,外层循环也执行 n 次,总操作次数约为 n2,因此时间复杂度为 O(n2)。
二、空间复杂度:代码的“内存占用”
空间复杂度衡量的是一个算法在执行过程中,额外所需的内存空间,也称为辅助空间。它不包括输入数据本身占用的空间。
1. O(1) 常数空间
无论输入数据量多大,算法都只使用固定大小的额外内存。
- 代码示例:计算数组元素的平均值。
Kotlin
fun average(arr: IntArray): Double {
var sum = 0.0
for (num in arr) {
sum += num
}
return sum / arr.size
}
- 分析:算法只使用了
sum和arr两个变量,其内存占用与数组大小无关,因此空间复杂度为 O(1)。
2. O(n) 线性空间
算法所需的额外内存空间与输入数据量呈线性关系。
- 代码示例:将一个数组的元素平方后存入新数组。
Kotlin
fun squareArray(arr: IntArray): IntArray {
val result = IntArray(arr.size)
for (i in arr.indices) {
result[i] = arr[i] * arr[i]
}
return result
}
- 分析:该函数创建了一个与输入数组大小相同的
result数组,因此额外空间与输入数据量 n 呈正比,空间复杂度为 O(n)。
3. O(n) 递归空间
递归算法的空间复杂度与递归深度有关。每层递归调用都会在函数调用栈上创建一个新的栈帧,如果递归深度与 n 成正比,那么空间复杂度就是 O(n)。
三、时间和空间的权衡
在实际开发中,时间复杂度和空间复杂度往往是一对矛盾体,我们常常需要进行取舍。
- 空间换时间:使用额外内存来减少计算时间。例如,在检查数组是否有重复元素的例子中,我们可以牺牲 O(n) 的空间复杂度,使用一个
HashSet来存储已遍历的元素,从而将时间复杂度从 O(n2) 优化到 O(n)。 - 时间换空间:通过更多的计算来减少内存占用。例如,某些情况下,我们可以选择不缓存中间结果,而是每次需要时重新计算,以节省内存。
四、评估与优化注意事项
- 最坏情况与平均情况:复杂度通常指最坏情况下的性能,但许多算法(如快速排序)在平均情况下表现更好。
- 隐藏的成本:在高级语言中,一些看似简单的操作可能隐藏着额外的开销。例如,在Python中连接字符串或在Kotlin中使用
filter、map等高阶函数时,可能会在后台创建新的集合,从而增加额外的空间复杂度。 - 大O符号是趋势,不是精确值:当评估算法性能时,我们应专注于其随数据量增长的趋势,而不是精确的执行步骤数。例如,O(100n+50) 的算法与 O(n) 的算法,在数据量足够大时,前者并不一定比后者慢。