时间复杂度
1. 含义
- 算法在特定输入下的运行时间:指执行的基本操作数或步数
- 时间复杂度是定性描述该算法运行时间的函数,可以用来估计该算法的运行时间
- 当输入规模足够大的时候,这个函数的低阶项和最高阶项的常数项对函数值的影响相较于最高阶项指数对函数值的影响来说可以忽略不计,也就是说只对运行时间的次数做描述
2. 渐进记号
渐进记号描述函数的增长趋势。五个渐进记号如下:
2.1. 定义
等号代替属于
为了方便,当
可以更快速地用极限定义:
其中
2.2. 性质
2.2.1. 传递性
2.2.2. 自反性
2.2.3. 对称性和转置对称性
2.3 渐进记号的运算规则
2.3.1 加法规则
如果
例子:
2.3.2 乘法规则
如果
例子:
2.3.3 幂次规则
如果
3. 时间复杂度的表示
- 时间复杂度可以用渐进记号表示,最常用的是大 O 记号。
- 大多数情况下,我们只关注算法的最坏情况运行时间:
- 对大部分算法来说,最坏情况经常出现;
- 平均情况运行时间很可能等于最坏情况运行时间;
- 有例外,比如快速排序。
4. 例子
常数时间复杂度:
。如果一个算法的执行时间不随输入规模
的变化而变化,那么该算法的时间复杂度为 。例如,访问数组中的单个元素A[i]
,或者执行一次加法运算。常数时间复杂度
也可写成 。(为什么?)线性时间复杂度:
。如果一个算法需要遍历一次输入数据,其时间复杂度通常是线性的。例如,在一个大小为
的数组中查找最大值:cint findMax(int arr[], int n) { int max_val = arr[0]; for (int i = 1; i < n; i++) { if (arr[i] > max_val) { max_val = arr[i]; } } return max_val; }
1
2
3
4
5
6
7
8
9在这个例子中,
for
循环执行了 次。循环体内的操作(比较和可能的赋值)都是常数时间的。因此,总的运行时间与 成正比,记为 。二次时间复杂度:
。一个典型的例子是简单的排序算法,如冒泡排序,它包含嵌套循环。
cvoid bubbleSort(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { swap(arr[j], arr[j + 1]); } } } }
1
2
3
4
5
6
7
8
9外层循环执行
次,内层循环的执行次数依赖于外层循环的迭代变量 。在最坏情况下(数组完全逆序),总的比较和交换次数约为 ,这是一个关于 的二次多项式 。根据时间复杂度的定义,我们忽略低阶项和常数系数,因此时间复杂度为 。函数的增长不一定能比较:
考虑
和 。为什么此时 和 不成立?因为函数
的值在 之间周期性震荡:- 当
时(例如 ), 。在这种情况下, 的增长速度远于 - 当
时(例如 ), 。在这种情况下, 增长速度远慢于 。
因为
和 的增长速度关系不断变化,无法找到一个从 开始一直成立的上下界关系,所以两者无法用 或 号进行比较。- 当
阶乘的对数:
上界证明: 由 斯特林公式,
。又因为 和 均可视为较 小的项,所以 。下界证明:
在阶乘的展开式
中,后一半的项(从 到 )共有 项,并且每一项都大于等于 。因此,它们的乘积 。两边取对数,得
。根据定义,存在常数
(例如 ),当 足够大时,有 。因此,
。结论:由上界和下界证明可得,
。
经常有人疑惑
对数的底数是几无关紧要,因为:根据换底公式,不同底数的对数之间只差一个常数,而计算时间复杂度的时候不考虑常数。
5. 空间复杂度
5.1 定义
算法的空间复杂度指该算法执行过程中所需要消耗的存储空间资源(不要把输入数据所占的存储空间包括在内)。
5.2 表示方法
与时间复杂度表示方法相同。