一、算法效率
如何衡量算法的好坏?
先看一个例子:斐波那契数列的递归实现
cpp
long long Fib(int N) {
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
代码非常简洁,但这样真的好么?当N比较大时,程序运行会非常慢。那我们该如何科学地衡量算法的好坏呢?
算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般 是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
二、时间复杂度
什么是时间复杂度?
时间复杂度 不是算法运行的具体时间(那得跑起来才知道),而是一个函数,描述算法执行次数与问题规模N的关系。
基本思想:找到基本操作 与问题规模N之间的数学表达式。
示例 :计算Func1中++count的执行次数
cpp
void Func1(int N) {
int count = 0;
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
++count; // N * N 次
}
}
for (int k = 0; k < 2 * N; ++k) {
++count; // 2N 次
}
int M = 10;
while (M--) {
++count; // 10 次
}
printf("%d\n", count);
}
执行次数函数为:
F(N)=N2+2N+10F(N)=N2+2N+10
-
N = 10 时,F(N) = 130
-
N = 100 时,F(N) = 10210
-
N = 1000 时,F(N) = 1002010
但我们真的需要这么精确吗?不需要!我们只关心量级 ,这就是大O渐进表示法。
大O渐进表示法
推导大O阶的方法:
-
用常数1取代运行时间中的所有加法常数
-
只保留最高阶项
-
如果最高阶项存在且系数不为1,去掉系数
Func1的时间复杂度 :
O(N2)O(N2)
可以看到,大O表示法去掉了对结果影响不大的项,简洁明了。
最好、平均、最坏情况
在长度为N的数组中搜索一个元素x:
-
最好情况:1次找到 ------ O(1)
-
最坏情况:N次找到 ------ O(N)
-
平均情况:N/2次找到 ------ O(N)
实际中我们一般关注最坏情况,因为这是算法性能的保证。
冒泡排序 ------ O(N²)
cpp
void BubbleSort(int* a, int n) {
assert(a);
for (size_t end = n; end > 0; --end) {
int exchange = 0;
for (size_t i = 1; i < end; ++i) {
if (a[i-1] > a[i]) {
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
-
最好情况(已有序):N次比较 → O(N)
-
最坏情况(逆序):(N*(N+1)/2次比较 → O(N²)
三、空间复杂度
空间复杂度 不是程序占用了多少字节,而是变量的个数,同样用大O渐进表示法。
注意:函数运行时需要的栈空间(参数、局部变量等)在编译时已确定,我们只考虑运行时额外申请的空间。
冒泡排序 ------ O(1)
cpp
void BubbleSort(int* a, int n) {
assert(a);
for (size_t end = n; end > 0; --end) {
int exchange = 0;
for (size_t i = 1; i < end; ++i) {
if (a[i-1] > a[i]) {
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
只用了end、exchange、i等常数个变量 → O(1)
阶乘递归 ------ O(N)
cpp
long long Fac(size_t N) {
if(N == 0)
return 1;
return Fac(N-1) * N;
}
递归调用了N次,开辟了N个栈帧,每个栈帧常数空间 → O(N)
四、常见复杂度对比
| 大O表示 | 名称 | 例子 |
|---|---|---|
| O(1) | 常数阶 | 数组随机访问 |
| O(logN) | 对数阶 | 二分查找 |
| O(N) | 线性阶 | 遍历数组 |
| O(NlogN) | NlogN阶 | 归并排序、快排 |
| O(N²) | 平方阶 | 冒泡排序 |
| O(N³) | 立方阶 | 矩阵乘法 |
| O(2^N) | 指数阶 | 斐波那契递归 |
复杂度增长趋势 :
O(1) < O(logN) < O(N) < O(NlogN) < O(N²) < O(2^N)
总结
本文系统性地介绍了算法时间复杂度的核心概念与分析方法:
📝 核心知识点回顾
| 知识点 | 关键内容 |
|---|---|
| 算法效率 | 时间和空间两个维度衡量算法好坏 |
| 时间复杂度 | 算法执行次数与问题规模N的关系,不是具体时间 |
| 大O渐进表示法 | 只保留最高阶项,去掉常数和系数 |
| 三种情况 | 最好、平均、最坏(一般关注最坏情况) |
| 常见复杂度 | O(1) < O(logN) < O(N) < O(NlogN) < O(N²) < O(2^N) |
💡 重要结论
-
时间复杂度不是算时间,而是算操作次数的增长趋势
-
大O表示法关注量级而非细节,让我们能快速比较算法优劣
-
递归算法的时间复杂度 看递归次数,空间复杂度看栈帧深度
-
同一个问题,不同算法的时间复杂度可能天差地别(如斐波那契数列递归O(2^N) vs 循环O(N))
🎯 学习建议
掌握了时间复杂度分析,你就掌握了评价算法的第一把尺子 。在后续学习各种数据结构(顺序表、链表、栈、队列、树等)时,都要养成先分析复杂度的习惯。
