🌙

时间复杂度与空间复杂度

性能分析入门

算法复杂度概念

时间复杂度 (Time Complexity)

描述算法运行时间随输入规模 n 增长而变化的趋势

我们通常使用大O表示法 (Big O notation),例如 O(n), O(n²), O(log n)。

时间复杂度关注的是算法执行的基本操作次数的增长级别,而不是具体的毫秒数,因为它会受硬件、编程语言等因素影响。

空间复杂度 (Space Complexity)

描述算法在运行过程中临时占用的额外存储空间随输入规模 n 增长而变化的趋势

空间复杂度计算的是算法本身需要的辅助空间,不包括存储输入数据所占用的空间。

例如,如果算法只需要固定数量的几个变量,与输入大小无关,则空间复杂度为 O(1)。

生活中的比喻

时间复杂度比喻:查字典找单词

方法一 (O(n)):从第一页逐字往后翻,直到找到目标单词。如果字典厚一倍 (n变大),查找时间可能翻倍(线性关系)。

方法二 (O(log n)):利用目录或字母索引,每次查找都能排除掉大约一半的页数。即使字典厚很多,查找次数增加得也很慢(对数关系)。

空间复杂度比喻:整理书架

方法一 (O(1)):原地整理,不需要额外的桌子或空间存放临时取出的书。只需要少量固定空间(比如手里拿几本书)。

方法二 (O(n)):先把所有书都搬到一张大桌子上,再按顺序放回书架。需要的额外桌面空间与书的数量成正比(线性关系)。

常见时间复杂度分类

从快到慢排序,展示不同复杂度随输入规模 n 增长的趋势:

O(1) - 常数时间

执行时间不随输入规模 n 的变化而变化。无论 n 多大,操作次数都是固定的。

例子:访问数组中指定索引的元素 arr[i],哈希表(理想情况下)的插入和查找。

O(log n) - 对数时间

执行时间随 n 的对数增长。通常出现在每次操作都将问题规模减半的算法中。

例子:二分查找在一个有序数组中查找元素。

O(n) - 线性时间

执行时间与输入规模 n 成正比。n 增大几倍,时间大致也增加几倍。

例子:遍历一个数组或链表的所有元素,线性查找。

O(n log n) - 线性对数时间

执行时间是 n 乘以 `log n`。常见于高效的排序算法。

例子:归并排序、快速排序(平均情况)、堆排序。

O(n²) - 平方时间

执行时间与 n 的平方成正比。通常涉及嵌套循环,每个循环都依赖于 n。

例子:冒泡排序、选择排序、插入排序,遍历二维数组。

O(2ⁿ) - 指数时间

执行时间随 n 呈指数增长。这类算法通常非常慢,只适用于 n 很小的情况。

例子:生成一个集合的所有子集,没有优化的递归计算斐波那契数列。

典型示例分析 (1):线性搜索 vs 二分查找

假设我们在下面的数组中查找数字 7

线性搜索 - O(n)

从数组第一个元素开始,逐个比较,直到找到目标或遍历完所有元素。

操作次数:0

二分查找 - O(log n)

前提:数组必须有序! 每次查找中间元素,根据比较结果缩小一半查找范围。

操作次数:0

典型示例分析 (2):冒泡排序 vs 快速排序

对以下无序数组进行排序:

冒泡排序 - O(n²)

重复遍历数组,比较相邻元素,如果顺序错误就交换,直到没有元素需要交换。

比较次数:0

快速排序 - O(n log n) 平均

选择一个“基准”元素,将数组分区:小于基准的放左边,大于的放右边。然后递归地对左右子数组进行排序。

比较次数:0

递归调用栈 (近似演示空间复杂度 O(log n) ~ O(n)):

空间复杂度讲解

常见空间复杂度

  • O(1) - 常数空间: 算法使用的额外空间是固定的,不随输入 n 变化。例如,只用了几个变量来进行计算。
  • O(n) - 线性空间: 算法使用的额外空间与输入 n 成正比。例如,创建了一个与输入大小相同的新数组。
  • O(log n) - 对数空间: 额外空间与 n 的对数成正比。通常在递归算法中,如果递归深度是对数级的(如二分查找的递归实现、某些分治算法),则栈空间是对数级的。
  • O(n²) - 平方空间: 额外空间与 n 的平方成正比。例如,创建了一个 n x n 的二维数组。

递归调用栈开销

递归函数在调用时,系统需要使用调用栈来保存每次调用的上下文(如局部变量、返回地址)。

递归的深度直接影响了调用栈所占用的空间。如果递归深度是 `d`,那么空间复杂度至少是 O(d)。

重要:即使函数本身没有显式申请额外内存,递归调用栈也构成空间开销。

示例:计算斐波那契数列第 n 项

递归实现 - 空间复杂度 O(n)

每次调用 `fib(k)` 都会产生新的栈帧,最大深度约为 n。

迭代实现 - 空间复杂度 O(1)

只需要固定的几个变量 (如 a, b, temp) 来存储中间结果。

当前: ?
a: ?
b: ?

选择题与练习

编程练习

练习 1:数组求和

实现一个 C++ 函数 sumArray(const std::vector& arr)`,计算并返回数组 `arr` 中所有元素的和。分析你实现的时间复杂度和空间复杂度。

练习 2:阶乘计算

分别用递归和迭代两种方式实现 C++ 函数计算非负整数 n 的阶乘 (`n!`)。比较并分析两种实现的空间复杂度差异。

总结与回顾

恭喜你完成了本次学习!以下是关键要点:

1. 时间复杂度:衡量算法执行时间随输入规模增长的趋势,常用大O表示法。

2. 空间复杂度:衡量算法所需额外存储空间随输入规模增长的趋势

3. 常见复杂度级别: O(1), O(log n), O(n), O(n log n), O(n²), O(2ⁿ) 等,效率依次降低。

4. 典型示例:理解线性搜索(O(n)) vs 二分查找(O(log n)),冒泡排序(O(n²)) vs 快速排序(O(n log n)) 的效率差异。

5. 递归与空间:递归调用会产生栈空间开销,影响空间复杂度,有时迭代是更优选择(如斐波那契)。

实践建议:在编写代码时,养成分析复杂度的习惯,尤其是在处理大数据量时,选择更优的算法能显著提升程序性能!

已复制!