目录
1. 概述
众所周知,衡量算法效率的标准为:时间复杂度 和 空间复杂度。
通俗地来讲,时间复杂度就是一个 程序要被执行的次数,它是一个近似值,而不是执行的时间。空间复杂度,是程序执行过程中所 占用的最大内存。
2. 分析时间复杂度和空间复杂度
接下来通过两段代码来说明下如何计算一个程序的 时间复杂度 以及 空间复杂度:
/* 以 32 位机为例 */
// 时间复杂度: 共执行 2n + 5 次,用大 O 表示法记为 O(n);空间复杂度: 4n + 12 字节,用大 O 表示法记为 O(n)
int SumMemory(int n) {
int i = 0; // 时间复杂度: 执行 1 次;空间复杂度: i 为 int 型,占 4 个字节
int ret = 0; // 时间复杂度: 执行 1 次;空间复杂度: 4 个字节
int *data = (int *)malloc(n * sizeof(int)); // 时间复杂度: 执行 1 次;空间复杂度: 4 * n 个字节
// 时间复杂度: 循环 n 次,因此执行 n 次;空间复杂度: 0,因为这部分内容是在 CPU 中运行,不占用内存
for (i = 0; i < n; i++) {
data[i] = i + 1;
}
// 时间复杂度: 循环 n 次,因此执行 n 次;空间复杂度: 0,因为这部分内容是在 CPU 中运行,不占用内存
for (i = 0; i < n; i++) {
ret += data[i];
}
free(data); // 时间复杂度: 执行 1 次;空间复杂度: 0,因为这部分内容是在 CPU 中运行,不占用内存
data = NULL; // 时间复杂度: 执行 1次;空间复杂度: 4 个字节
return ret;
}
这段代码一共执行了 2n + 5 次,当 n 趋于无穷大时,5 对于 2n 的影响少之又少,因此可以将其删掉,本质上来说,复杂度的计算是个近似运算,那么 n 的系数也可以置为 1,只需比较 n 的幂次即可。
/* 以 32 位机为例 */
// 时间复杂度: 共执行 n + 2 次,用大 O 表示法记为 O(n);空间复杂度: 8 个字节,用大 O 表示法记为 O(1)
int SumLoop(int n) {
int i = 0; // 时间复杂度: 执行 1 次;空间复杂度: i 为 int 型,占 4 个字节
int ret = 0; // 时间复杂度: 执行 1 次;空间复杂度: 4 个字节
// 时间复杂度: 循环 n 次,因此执行 n 次;空间复杂度: 0,因为这部分内容是在 CPU 中运行,不占用内存
for (i = 0; i < n; i++) {
ret += i;
}
return ret;
}
这段代码一共执行了 n + 2 次,当 n 趋于无穷大时,2 对于 n 的影响少之又少,因此可以将其删掉,本质上来说,复杂度的计算是个近似运算,只需比较 n 的幂次即可。
/* 以 32 位机为例 */
// 时间复杂度: 执行 2 次,用大 O 表示法记为 O(1);空间复杂度: 4 个字节,用大 O 表示法记为 O(1)
int SumFormula(int n) {
int ret = 0; // 时间复杂度:执行 1 次;空间复杂度: 4 个字节
if (n > 0) {
ret = (1 + n) * n / 2; // 时间复杂度: 加减乘除运算只是一个运算指令,因此执行 1 次;空间复杂度: 0,因为这部分内容是在 CPU 中运行,不占用内存
}
return ret;
}
这段代码一共执行了 2 次,本质上来说,复杂度的计算是个近似运算,将 2 看做 1 即可。
3. 总结
总结下推导 O(n) 的方法:
- 首先用常数 1 取代所有的加法常数;
- 只保留 n 的最高幂次;
- 若最高幂次存在且不为 1,则将这个最高幂次的系数置为 1 即可。
4. 小试牛刀
最后分析下 折半查找 和 菲波那切数列数列 的时间复杂度以及空间复杂度:
4.1 折半(二分)查找
// 以 32 位机为例
// 空间复杂度:12 个字节,用大 O 表示法记为 O(1)
// 时间复杂度:O(log2N)
int BinarySearch(int * pArr, int key, int len) {
int left = 0; // 空间复杂度:4 个字节
int right = len - 1; // 空间复杂度:4 个字节
int mid = 0; // 空间复杂度:4 个字节
assert(NULL != pArr);
while (left <= right) {
mid = (left & right) + ((left ^ right) >> 1);
if (pArr[mid] == key) {
return mid;
} else if (pArr[mid] > key) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
时间复杂度的分析:假设有 n 个元素,经过第一次查找后,查找区间缩短为 n / 2,经过第二次查找后,查找区间缩短为 n / 4,…… 那么经过 k 次查找后,查找区间变为 n / 2^k(ps: n 除上 2 的 k 次方)。不难发现,k 其实就是循环次数,那么最终找到元素 key 时,n / 2^k = 1(ps: 这里等于 1 就表示找到了元素 key),因此可以得出最坏情况下该算法的时间复杂度为 O(log2n)。(ps: log2n表示以 2 为底,n 的对数)
综上所述:折半(二分)查找 的时间复杂度为 O(log2n),空间复杂度为 O(1)。
4.2 递归实现斐波那契数列
int Fibonacci(int n) {
if (n <= 2) {
return 1;
} else {
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
}
时间复杂度的分析:菲波那切数可以看做一个树结构,1 分为 2,2 分为 4,4 分为 8,... 而要去求第 n 个斐波那契数,就需要被分解成 2^n - 1 个数字,也就是需要执行 2^n - 1 次,用大 O 表示法记为 O(2^n)。
空间复杂度的分析:计算第 n 个菲波那切数,会调用 n - 2 次 Fibonacci() 函数,每次调用都会开辟栈空间,那么就会占用 4 * (n - 2)个字节,用大 O 表示法记为 O(n)。
综上所述:递归实现菲波那切数列 的时间复杂度为 O(2^n),空间复杂度为 O(n)。