一、大 O 复杂度表示法
1.1 时间复杂度
假设每行代码运行的时间为 unit_time ,那么所有代码的总执行时间 T(n)与每行代码的执行次数 f(n) 成正比,那么可得:T(n) = O(f(n)),此处大 O 表示的不是代码的具体执行时间,而是代码的执行时间虽数据规模增长的变化趋势。
- 加法法则:算法的总时间复杂度等于算法中量级最大的复杂度,通常情况下公式中的低阶、常量和系数都可以忽略,例如:O(n2 + n + 2) = O(n2)
- 乘法法则:嵌套代码的时间复杂度等于嵌套代码内外的时间复杂度,最简单的例子就是嵌套循环的时间复杂度计算。
常见的时间复杂度量级有以下几种:
- 常量级:O(1)
- 对数级:O(logn) - 对数的底可以进行转换,因此可以忽略对数的底
- 线性级:O(n)
- 线性对数级:O(nlogn)
- 平方级:O(n2),O(n3)…
- 指数级:O(2^n)
- 阶乘级:O(n!)
1.2 空间复杂度
空间复杂度与时间复杂度一样也可以使用大 O 表示法,常见量级有以下几种:
- 常量级:O(1)
- 线性级:O(n)
- 平方级:O(n2),O(n3)…
二、示例
通过以下的代码片段来计算此函数的时间复杂度
// 查找数据在数组中的位置,数据不在数组中时返回 -1
int find(vector<int>& nums, int num) {
int n = nums.size();
for (int i = 0; i < n; i++ ) {
if (nums[i] == num) {
return i;
}
}
return -1;
}
2.1 最好情况时间复杂度
从代码可以看出当 num 在数组中的位置不一样时,循环的执行次数时不一样的。顾名思义,最好情况时间复杂度即是代码运行最好的情况,此时我们想要查找的数据位于数组的第一位,循环只执行一次,时间复杂度为 O(1);
2.2 最坏情况时间复杂度
与最好情况时间复杂度相反,最坏情况时间复杂度即是代码运行最坏的情况,此时我们想要查找的数据位于数组的最后一位,循环只执行 n 次,时间复杂度为 O(n);
2.3 平均时间复杂度
平均时间复杂度即是计算每种情况出现的可能性以及其对应的时间复杂度,对于示例中的代码片段,我们假设对于要查找的数据在数组中与不在数组中的概率都为1/2,且出现在 0~n-1 的概率也是一样的,则平均时间复杂度为:(1 * (1/2n) + 2 * (1/2n) + … + n * (1/2n) + n * (1/2)) = (3n + 1)/4;此复杂度用大 O 表示法时仍为 O(n);
2.4 均摊时间复杂度
//在数组中插入一个值,若数组已经满了,则将数组的所有位置全部变为零后将数据放在数组首位
int[] array = new int[n];
int count = 0;
void insert(int val) {
if (count == array.length()) {
for (int i = 0; i < count; i++) {
array[i] = 0;
}
count = 0;
}
array[count] = val;
count ++;
}
对于此代码片段:
- 最好情况时间复杂度:O(1)
- 最坏情况时间复杂度:O(n)
- 平均时间复杂度:一共有 n+1 种情况,对应数组长度为 0 到 n-1 和数组满了,每种的概率都是 1/(n + 1),可求得复杂度为:1 * (1/(n+1)) + 1 * (1/(n+1)) + … + 1 * (1/(n+1)) + n * (1/(n+1)) = O(1)
在此代码片段中我们可以求其均摊时间复杂度:对于每一种情况来说,其出现的概率时相同的,而且只有其中一种情况的复杂度为 O(n),剩余的都为 O(1) ,因此我们可以将此次耗时操作均摊到所有的操作上,即将 O(n) 的复杂度均摊到 n 次操作上,此时可得均摊时间复杂度为 O(1)。