C语言算法复杂度详解:时间复杂度与空间复杂度一篇讲透
目录
- 前言
- 一、为什么要学习复杂度?
- 二、什么是算法效率?
- 三、时间复杂度是什么?
- [四、大 O 渐进表示法](#四、大 O 渐进表示法)
- [五、如何推导大 O 阶?](#五、如何推导大 O 阶?)
- 六、最好、平均、最坏情况
- 七、常见时间复杂度总览
- 八、O(1):常数阶
- 九、O(N):线性阶
- 十、O(N+M):两个变量的线性阶
- 十一、O(N²):平方阶
- 十二、O(logN):对数阶
- 十三、O(NlogN):线性对数阶
- 十四、O(2^N):指数阶
- 十五、递归算法的时间复杂度
- 十六、空间复杂度是什么?
- 十七、常见空间复杂度分析
- 十八、递归为什么会影响空间复杂度?
- 十九、复杂度常见排序
- 二十、常见算法复杂度速查表
- [二十一、OJ 练习一:消失的数字](#二十一、OJ 练习一:消失的数字)
- [二十二、OJ 练习二:旋转数组](#二十二、OJ 练习二:旋转数组)
- 二十三、复杂度分析常见误区
- 全文总结
前言
学数据结构和算法的时候,很多同学一开始最容易忽略的就是:
我写出来的代码,到底快不快?占不占内存?
比如斐波那契数列,递归写法非常简洁:
c
long long Fib(int N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
这段代码看起来非常优雅。
但是问题来了:
代码短,就一定好吗?
不一定。
有些代码看起来很短,但运行起来非常慢。
有些代码看起来多写了几行,但效率却高得多。
所以评价一个算法,不能只看代码长不长,也不能只看能不能跑出结果。
我们通常要从两个维度衡量算法效率:
text
1. 时间复杂度:运行得快不快
2. 空间复杂度:额外占用内存多不多
这一篇文章就系统梳理算法复杂度的核心知识点,包括:
- 什么是时间复杂度
- 什么是空间复杂度
- 什么是大 O 表示法
- 如何分析循环和递归复杂度
- 常见复杂度有哪些
- 二分查找为什么是 O(logN)
- 冒泡排序为什么是 O(N²)
- 递归斐波那契为什么是 O(2^N)
总的来说:
复杂度不是为了精确计算程序跑了几秒,而是为了估计算法随着输入规模变大时,效率会如何变化。
一、为什么要学习复杂度?
我们先看一个很现实的问题。
假设有两个算法都能解决同一个问题。
算法 A:
text
输入 10 个数据,用时 1 秒
输入 100 个数据,用时 10 秒
输入 1000 个数据,用时 100 秒
算法 B:
text
输入 10 个数据,用时 1 秒
输入 100 个数据,用时 100 秒
输入 1000 个数据,用时 10000 秒
当数据规模很小时,好像差别不大。
但是当数据量越来越大,差距会越来越夸张。
这就是复杂度要研究的问题:
当输入规模 N 变大时,算法运行时间和内存消耗会怎么增长?
复杂度解决的不是"现在跑几秒"
程序实际运行时间会受到很多因素影响:
- 电脑配置
- CPU 性能
- 编译器优化
- 操作系统状态
- 输入数据特点
- 编程语言差异
所以我们不能只用"跑了几秒"来评价算法。
复杂度更关注:
随着 N 增大,操作次数的增长趋势。
也就是说,它不关心这台机器上具体跑了 0.01 秒还是 0.02 秒,
而是关心:
text
随着n的变化整个时间消耗到底如何变化?
二、什么是算法效率?
算法效率主要包括两个方面:
text
1. 时间效率
2. 空间效率
1. 时间效率
时间效率衡量的是:
算法运行得快不快。
对应的概念就是:
text
时间复杂度
2. 空间效率
空间效率衡量的是:
算法运行时额外占用内存多不多。
对应的概念就是:
text
空间复杂度
时间和空间有时可以互相交换
很多算法优化里,经常会出现一种情况:
用空间换时间。
比如哈希表。
为了查找更快,我们额外开一块空间来存储映射关系。
也可能反过来:
用时间换空间。
比如不额外开数组,而是每次重新计算。
所以算法设计不是单纯追求"时间最少"或者"空间最少",而是根据实际场景做平衡。
三、时间复杂度是什么?
时间复杂度的定义可以简单理解为:
算法中基本操作的执行次数,和问题规模 N 之间的关系。
这里有两个关键词:
text
基本操作
问题规模 N
1. 什么是基本操作?
基本操作通常是算法中最核心、最频繁执行的语句。
比如:
c
++count;
如果我们分析某个循环中 ++count 执行了多少次,就可以大致判断这个算法的运行成本。
2. 什么是问题规模 N?
问题规模就是输入数据的大小。
比如:
- 数组长度是
N - 链表节点个数是
N - 字符串长度是
N - 矩阵大小是
N × N
复杂度研究的是:
当 N 变大时,基本操作执行次数如何增长。
例子:计算 Func1 的执行次数
c
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
我们来数 ++count 执行了多少次。
第一段双层循环:
text
N * N = N²
第二段循环:
text
2N
第三段 while:
text
10
所以总次数:
text
F(N) = N² + 2N + 10
当 N 区域无穷的时候,N² 是最主要的影响项。
所以这个函数的时间复杂度是:
text
O(N²)
四、大 O 渐进表示法
我们刚才得到:
text
F(N) = N² + 2N + 10
但是实际分析复杂度时,我们通常不会保留这么精确的式子。
因为复杂度关注的是增长趋势。
当 N 很大时:
text
N² 比 2N 和 10 影响大得多
所以我们会把它简化成:
text
O(N²)
这就是大 O 渐进表示法。
大 O 到底表示什么?
大 O 用来描述:
当输入规模趋近于很大时,算法运行成本的增长级别。
它不关心低阶项,也不关心常数倍。
比如:
text
3N² + 4N + 5
最终写成:
text
O(N²)
因为最高阶项是 N²。
为什么可以忽略低阶项和常数?
看一个例子。
text
F(N) = N² + 2N + 10
当 N = 10:
text
N² = 100
2N = 20
10 = 10
低阶项还稍微有点存在感。
当 N = 1000:
text
N² = 1000000
2N = 2000
10 = 10
这时候 2N + 10 和 N² 比起来几乎可以忽略。
所以大 O 只保留对增长趋势最重要的部分。
五、如何推导大 O 阶?
推导大 O 一般有三步:
text
1. 用常数 1 取代运行时间中的所有加法常数
2. 只保留最高阶项
3. 如果最高阶项有常数系数,去掉常数系数
例子 1
text
F(N) = 2N + 10
第一步,常数 10 忽略:
text
2N
第二步,最高阶项是:
text
2N
第三步,去掉常数系数 2:
text
O(N)
例子 2
text
F(N) = 3N² + 4N + 5
只保留最高阶项:
text
3N²
去掉常数系数:
text
O(N²)
例子 3
text
F(N) = 100
常数操作次数,不随 N 变化。
所以时间复杂度是:
text
O(1)
注意:
O(1) 不代表只执行 1 次,而是表示执行次数是常数级别。
即使执行 100 次、1000 次,只要和 N 无关,都是 O(1)。
六、最好、平均、最坏情况
有些算法在不同输入下,执行次数不一样。
比如在数组中查找一个值 x。
数组长度为 N。
最好情况
如果第一个元素就是目标值:
text
查找 1 次就成功
时间复杂度:
text
O(1)
最坏情况
如果目标值在最后一个位置,或者根本不存在:
text
需要查找 N 次
时间复杂度:
text
O(N)
平均情况
如果目标值可能出现在任意位置,平均大概查找:
text
N / 2 次
时间复杂度:
text
O(N)
实际中一般看最坏情况
实际分析算法时,一般更关注:
text
最坏时间复杂度
因为它代表算法性能的上界。
比如顺序查找,我们一般说它的时间复杂度是:
text
O(N)
而不是最好情况下的 O(1)。
七、常见时间复杂度总览
常见时间复杂度从低到高大致如下:
text
O(1)
O(logN)
O(N)
O(NlogN)
O(N²)
O(N³)
O(2^N)
O(N!)
更直观地说:
| 复杂度 | 名称 | 常见场景 |
|---|---|---|
| O(1) | 常数阶 | 数组下标访问 |
| O(logN) | 对数阶 | 二分查找 |
| O(N) | 线性阶 | 遍历数组 |
| O(NlogN) | 线性对数阶 | 高效排序,如归并、快排平均 |
| O(N²) | 平方阶 | 冒泡排序、选择排序 |
| O(N³) | 立方阶 | 三层嵌套循环 |
| O(2^N) | 指数阶 | 递归斐波那契、暴力枚举子集 |
| O(N!) | 阶乘阶 | 全排列暴力搜索 |
八、O(1):常数阶
如果一个算法的执行次数和输入规模 N 无关,那么它就是常数阶。
例如:
c
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
虽然循环执行了 100 次,但 100 和 N 没关系。
所以时间复杂度是:
text
O(1)
常见 O(1) 操作
text
1. 数组下标访问 a[i]
2. 普通变量赋值
3. 两个数相加
4. 判断一次 if 条件
5. 顺序表尾删 size--
注意:
O(1) 不是"一次",而是"常数次"。
九、O(N):线性阶
如果操作次数和 N 成正比,就是线性阶。
例如:
c
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
执行次数:
text
F(N) = 2N + 10
根据大 O 规则:
text
O(N)
常见 O(N) 操作
text
1. 遍历数组
2. 遍历链表
3. 顺序查找
4. 计算字符串长度 strlen
例如 strchr 查找字符时,最坏情况下要从头扫描到结尾,所以是:
text
O(N)
十、O(N+M):两个变量的线性阶
有时候算法有两个输入规模。
例如:
c
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++k)
{
++count;
}
for (int k = 0; k < N; ++k)
{
++count;
}
printf("%d\n", count);
}
执行次数:
text
F(N, M) = N + M
所以时间复杂度是:
text
O(N + M)
这里不能简单写成 O(N),因为还有一个独立变量 M。
十一、O(N²):平方阶
最典型的平方阶就是双层循环。
例如:
c
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
++count;
}
}
外层执行 N 次,内层每次执行 N 次。
所以总次数:
text
N * N = N²
时间复杂度:
text
O(N²)
冒泡排序的时间复杂度
冒泡排序代码:
c
void BubbleSort(int* a, int n)
{
assert(a);
for (int end = n; end > 0; --end)
{
int exchange = 0;
for (int i = 1; i < end; ++i)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
冒泡排序的比较次数大致是:
text
N + (N - 1) + (N - 2) + ... + 1
这是等差数列:
text
N * (N + 1) / 2
去掉低阶项和常数后:
text
O(N²)
冒泡排序最好情况
如果数组本来就有序,第一趟检查时没有发生交换,直接退出。
最好情况:
text
O(N)
冒泡排序最坏情况
如果数组完全逆序,需要完整执行多轮比较和交换。
最坏情况:
text
O(N²)
实际分析时一般关注最坏情况,所以说冒泡排序是:
text
O(N²)
十二、O(logN):对数阶
O(logN) 最经典的例子是二分查找。
二分查找要求数组有序。
代码:
c
int BinarySearch(int* a, int n, int x)
{
int begin = 0;
int end = n - 1;
while (begin <= end)
{
int mid = begin + ((end - begin) >> 1);
if (a[mid] < x)
begin = mid + 1;
else if (a[mid] > x)
end = mid - 1;
else
return mid;
}
return -1;
}
为什么二分查找是 O(logN)?
每查找一次,搜索范围都会减半。
假设一开始有 N 个数据:
text
第 1 次:N
第 2 次:N / 2
第 3 次:N / 4
第 4 次:N / 8
...
直到变成 1。
也就是:
text
N / 2^k = 1
变形得到:
text
2^k = N
所以:
text
k = log₂N
因此二分查找的时间复杂度是:
text
O(logN)
为什么复杂度里 log 默认可以不写底数?
在算法复杂度中,通常不关心 log 的底数。
因为:
text
log₂N = log₁₀N / log₁₀2
不同底数之间只差一个常数倍。
而大 O 会忽略常数,所以统一写成:
text
O(logN)
十三、O(NlogN):线性对数阶
O(NlogN) 常见于高效排序算法,比如:
- 归并排序
- 快速排序平均情况
- 堆排序
可以这样理解:
text
每一层处理 N 个数据
一共有 logN 层
所以总复杂度是 NlogN
以归并排序为例:
text
原数组规模 N
拆成两个 N/2
再拆成四个 N/4
...
一共拆 logN 层
每一层合并总共处理 N 个元素
所以总时间复杂度:
text
O(NlogN)
图解:
text
第 1 层: N
第 2 层: N/2 + N/2
第 3 层: N/4 + N/4 + N/4 + N/4
...
每层总量: N
层数: logN
总复杂度: NlogN
十四、O(2^N):指数阶
指数阶增长非常快。
最经典例子是递归斐波那契:
c
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
每次计算 Fib(N),都会继续拆成:
text
Fib(N - 1)
Fib(N - 2)
这会形成一棵递归树。
递归树
text
Fib(5)
├── Fib(4)
│ ├── Fib(3)
│ │ ├── Fib(2)
│ │ └── Fib(1)
│ └── Fib(2)
└── Fib(3)
├── Fib(2)
└── Fib(1)
可以看到,很多计算被重复执行。
比如 Fib(3)、Fib(2) 会被算很多次。
所以递归斐波那契的时间复杂度是:
text
O(2^N)
这也是为什么递归斐波那契看起来优雅,但效率很差。
十五、递归算法的时间复杂度
递归算法分析时,核心看两点:
text
1. 递归调用了多少次
2. 每次递归内部做了多少工作
1. 阶乘递归
c
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
递归调用过程:
text
Fac(N)
Fac(N - 1)
Fac(N - 2)
...
Fac(1)
Fac(0)
一共递归 N 次左右。
每次只做常数次操作。
所以时间复杂度:
text
O(N)
2. 斐波那契递归
c
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
每次递归都分裂成两个子问题。
递归树规模接近指数级。
所以时间复杂度:
text
O(2^N)
阶乘 vs 斐波那契
| 算法 | 递归形式 | 时间复杂度 |
|---|---|---|
| 阶乘递归 | 一条链 | O(N) |
| 斐波那契递归 | 二叉递归树 | O(2^N) |
图解:
text
阶乘递归:
Fac(5)
|
Fac(4)
|
Fac(3)
|
Fac(2)
|
Fac(1)
斐波那契递归:
Fib(5)
├── Fib(4)
│ ├── Fib(3)
│ └── Fib(2)
└── Fib(3)
├── Fib(2)
└── Fib(1)
阶乘是一条链,斐波那契是一棵树。
十六、空间复杂度是什么?
空间复杂度衡量的是:
算法运行过程中额外占用的空间随输入规模 N 增长的情况。
注意,这里一般关注的是:
text
额外空间
不是输入本身占了多少空间。
例子
如果一个函数接收一个数组:
c
void PrintArray(int* a, int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
}
这个数组是外部传进来的输入,不算额外空间。
函数内部只创建了少量变量:
c
int i;
所以空间复杂度是:
text
O(1)
空间复杂度也用大 O 表示
和时间复杂度一样,空间复杂度也使用大 O。
常见空间复杂度包括:
text
O(1)
O(N)
O(N²)
十七、常见空间复杂度分析
1. 冒泡排序空间复杂度
冒泡排序只使用了少量额外变量:
c
int exchange;
int i;
int end;
不管 N 多大,额外变量数量基本不变。
所以空间复杂度:
text
O(1)
2. 动态开辟 N 个空间
c
long long* Fibonacci(size_t n)
{
if (n == 0)
return NULL;
long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
这里动态开辟了 n + 1 个 long long 空间。
所以空间复杂度是:
text
O(N)
3. 开辟二维数组
如果算法内部开辟:
c
int dp[N][N];
那么额外空间和 N² 成正比。
空间复杂度:
text
O(N²)
常见于一些动态规划问题。
十八、递归为什么会影响空间复杂度?
递归函数每调用一次,系统都会为它开辟一个新的函数栈帧。
栈帧中会保存:
- 函数参数
- 局部变量
- 返回地址
- 部分寄存器信息
所以递归深度会影响空间复杂度。
阶乘递归空间复杂度
c
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
递归调用深度:
text
N
每层栈帧使用常数空间。
所以空间复杂度:
text
O(N)
递归栈帧
text
调用 Fac(5)
+----------------+
| Fac(5) 栈帧 |
+----------------+
| Fac(4) 栈帧 |
+----------------+
| Fac(3) 栈帧 |
+----------------+
| Fac(2) 栈帧 |
+----------------+
| Fac(1) 栈帧 |
+----------------+
| Fac(0) 栈帧 |
+----------------+
递归越深,栈上叠的栈帧越多。
所以空间复杂度不是 O(1),而是:
text
O(N)
递归斐波那契的空间复杂度是多少?
递归斐波那契时间复杂度是 O(2^N),但是空间复杂度不是 O(2^N)。
为什么?
因为递归调用虽然很多,但不是所有调用同时存在。
递归栈的最大深度大约是:
text
N
所以空间复杂度是:
text
O(N)
这一点非常容易混。
十九、复杂度常见排序
一般来说,增长速度从慢到快可以这样排:
text
O(1) < O(logN) < O(N) < O(NlogN) < O(N²) < O(N³) < O(2^N) < O(N!)
可以理解成:
text
常数阶 最稳
对数阶 很优秀
线性阶 可以接受
NlogN 常见高效排序
平方阶 数据大时会慢
立方阶 更慢
指数阶 很容易爆炸
阶乘阶 暴力全排列,非常夸张
粗略增长对比
假设 N = 100:
| 复杂度 | 大概操作量 |
|---|---|
| O(1) | 1 |
| O(logN) | 约 7 |
| O(N) | 100 |
| O(NlogN) | 约 700 |
| O(N²) | 10000 |
| O(N³) | 1000000 |
| O(2^N) | 极其巨大 |
| O(N!) | 更夸张 |
这个表不是为了精确计算,而是为了建立直觉。
二十、常见算法复杂度速查表
| 场景 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 数组下标访问 | O(1) | O(1) |
| 顺序查找 | O(N) | O(1) |
| 遍历链表 | O(N) | O(1) |
| 二分查找 | O(logN) | O(1) |
| 冒泡排序 | O(N²) | O(1) |
| 选择排序 | O(N²) | O(1) |
| 插入排序 | O(N²) | O(1) |
| 归并排序 | O(NlogN) | O(N) |
| 快速排序平均 | O(NlogN) | O(logN) |
| 快速排序最坏 | O(N²) | O(N) |
| 堆排序 | O(NlogN) | O(1) |
| 递归阶乘 | O(N) | O(N) |
| 递归斐波那契 | O(2^N) | O(N) |
| 动态规划斐波那契数组版 | O(N) | O(N) |
| 动态规划斐波那契滚动变量版 | O(N) | O(1) |
二十一、OJ 练习一:消失的数字
题目大意:
给定一个包含
0 ~ N中 N 个数的数组,找出缺失的那个数字。
例如:
text
输入:[3, 0, 1]
输出:2
解法一:求和法
0 ~ N 的总和是:
text
N * (N + 1) / 2
用理论总和减去数组实际总和,就得到缺失数字。
c
int missingNumber(int* nums, int numsSize)
{
int n = numsSize;
int total = n * (n + 1) / 2;
for (int i = 0; i < numsSize; i++)
{
total -= nums[i];
}
return total;
}
时间复杂度:
text
O(N)
空间复杂度:
text
O(1)
解法二:异或法
异或有几个性质:
text
a ^ a = 0
a ^ 0 = a
把 0 ~ N 和数组里的所有数都异或一遍,重复出现的数字会抵消掉,剩下的就是缺失数字。
c
int missingNumber(int* nums, int numsSize)
{
int x = 0;
for (int i = 0; i <= numsSize; i++)
{
x ^= i;
}
for (int i = 0; i < numsSize; i++)
{
x ^= nums[i];
}
return x;
}
时间复杂度:
text
O(N)
空间复杂度:
text
O(1)
二十二、OJ 练习二:旋转数组
题目大意:
给定一个数组,将数组中的元素向右轮转 k 个位置。
例如:
text
输入:nums = [1, 2, 3, 4, 5, 6, 7], k = 3
输出:[5, 6, 7, 1, 2, 3, 4]
解法一:每次右旋一步
右旋一步:
text
[1,2,3,4,5,6,7]
右旋 1 步:
[7,1,2,3,4,5,6]
右旋 k 次即可。
但是这种做法时间复杂度是:
text
O(N * K)
如果 K 很大,效率较低。
解法二:三次逆置法
这是更优雅的做法。
以:
text
[1,2,3,4,5,6,7], k = 3
为例。
第一步:逆置前 n-k 个元素:
text
[1,2,3,4,5,6,7]
前 4 个逆置:
[4,3,2,1,5,6,7]
第二步:逆置后 k 个元素:
text
[4,3,2,1,7,6,5]
第三步:整体逆置:
text
[5,6,7,1,2,3,4]
代码:
c
void Reverse(int* nums, int left, int right)
{
while (left < right)
{
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
left++;
right--;
}
}
void rotate(int* nums, int numsSize, int k)
{
k %= numsSize;
Reverse(nums, 0, numsSize - k - 1);
Reverse(nums, numsSize - k, numsSize - 1);
Reverse(nums, 0, numsSize - 1);
}
时间复杂度:
text
O(N)
空间复杂度:
text
O(1)
二十三、复杂度分析常见误区
误区一:O(1) 就是执行一次
不是。
O(1) 表示执行次数和 N 无关。
执行 10 次、100 次,只要是固定次数,都是 O(1)。
误区二:看到一层循环就是 O(N)
不一定。
例如:
c
for (int i = 1; i < N; i *= 2)
{
++count;
}
虽然只有一层循环,但每次 i 都乘 2。
执行次数是:
text
logN
所以时间复杂度是:
text
O(logN)
误区三:看到两层循环就是 O(N²)
也不一定。
例如:
c
for (int i = 0; i < N; i++)
{
for (int j = 0; j < 10; j++)
{
++count;
}
}
内层固定执行 10 次。
总次数:
text
10N
所以时间复杂度是:
text
O(N)
误区四:递归空间复杂度只看变量个数
递归要看栈帧深度。
即使每层只使用常数个变量,如果递归了 N 层,空间复杂度也是:
text
O(N)
误区五:时间复杂度和空间复杂度总是同步变化
不是。
比如递归斐波那契:
text
时间复杂度:O(2^N)
空间复杂度:O(N)
它们并不一定相同。
复杂度分析小口诀
text
单层循环看增长:
i++ 多半是 O(N)
i *= 2 多半是 O(logN)
多层循环看乘法:
N 套 N 是 O(N²)
N 套 M 是 O(NM)
顺序结构看最大:
O(N² + N) 取 O(N²)
递归分析看树和深度:
一条链多半 O(N)
二叉展开可能 O(2^N)
空间复杂度看额外空间:
常数变量 O(1)
开 N 个空间 O(N)
递归 N 层 O(N)
全文总结
1. 算法效率主要看两个维度
text
时间复杂度:算法运行快不快
空间复杂度:额外占用空间多不多
2. 时间复杂度不是精确时间
它不是计算程序运行了几秒,而是分析:
text
基本操作执行次数和输入规模 N 的关系
3. 大 O 表示法关注增长趋势
推导规则:
text
1. 忽略常数项
2. 只保留最高阶项
3. 去掉最高阶项系数
4. 实际中常看最坏时间复杂度
因为最坏情况代表算法性能上界。
5. 常见时间复杂度排序
text
O(1) < O(logN) < O(N) < O(NlogN) < O(N²) < O(N³) < O(2^N) < O(N!)
6. 空间复杂度看额外空间
如果只用了常数个变量:
text
O(1)
如果额外开了 N 个空间:
text
O(N)
如果递归深度是 N:
text
O(N)
7. 复杂度分析的意义
复杂度不是为了让我们背公式,而是为了让我们判断:
text
这个算法在数据规模变大时还能不能扛得住?
复杂度分析本质上是在问:
当数据越来越大时,我的算法会不会崩?
如果一个算法在 N 很小时能跑,不代表它在 N 很大时还能跑。
所以学复杂度,不是为了考试时写一个 O(...),
而是为了真正学会判断:
text
一个算法到底适不适合解决这个问题。
写代码之前先想复杂度,
这就是从"能写代码"走向"会设计代码"的第一步。