C语言算法复杂度详解:时间复杂度与空间复杂度一篇讲透

C语言算法复杂度详解:时间复杂度与空间复杂度一篇讲透

指针合集
c语言基础
数据结构与算法

目录


前言

学数据结构和算法的时候,很多同学一开始最容易忽略的就是:

我写出来的代码,到底快不快?占不占内存?

比如斐波那契数列,递归写法非常简洁:

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 区域无穷的时候, 是最主要的影响项。

所以这个函数的时间复杂度是:

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²)

因为最高阶项是


为什么可以忽略低阶项和常数?

看一个例子。

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 比起来几乎可以忽略。

所以大 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 + 1long long 空间。

所以空间复杂度是:

text 复制代码
O(N)

3. 开辟二维数组

如果算法内部开辟:

c 复制代码
int dp[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 复制代码
一个算法到底适不适合解决这个问题。

写代码之前先想复杂度,

这就是从"能写代码"走向"会设计代码"的第一步。


相关推荐
傻瓜搬砖人1 小时前
c语言绿皮书第三版第十一章习题
c语言·开发语言·算法·谭浩强·绿皮书第三版
计算机安禾1 小时前
【c++面向对象编程】第3篇:类与对象(二):构造函数与析构函数
开发语言·c++·算法
小年糕是糕手1 小时前
【C++】vector 不踩坑指南:用法、底层实现与迭代器失效解析
c++·算法
SilentSamsara2 小时前
生成器完全指南:`yield` 与惰性求值的工程价值
linux·开发语言·python·算法·机器学习·青少年编程
玛卡巴卡ldf2 小时前
【LeetCode 手撕算法】(二分查找)搜索插入位置、搜索二维矩阵、查找数组相同的所有位置、搜索旋转排序数组、旋转升序数组的最小值
数据结构·算法·leetcode
谷雨不太卷9 小时前
进程的状态码
java·前端·算法
凉茶钱10 小时前
【c语言】动态内存管理:malloc,calloc,realloc,柔性数组
c语言·c++·vscode·柔性数组
散峰而望10 小时前
【算法竞赛】C/C++ 的输入输出你真的玩会了吗?
c语言·开发语言·数据结构·c++·算法·github
小龙报10 小时前
【C语言】内存里的 “数字变形记”:整数三码、大小端与浮点数存储真相
c语言·开发语言·c++·创业创新·学习方法·visual studio