算法的时间复杂度和空间复杂度

一、算法效率

如何衡量算法的好坏?

先看一个例子:斐波那契数列的递归实现

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取代运行时间中的所有加法常数

  2. 只保留最高阶项

  3. 如果最高阶项存在且系数不为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;
    }
}

只用了endexchangei等常数个变量 → 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)

💡 重要结论

  1. 时间复杂度不是算时间,而是算操作次数的增长趋势

  2. 大O表示法关注量级而非细节,让我们能快速比较算法优劣

  3. 递归算法的时间复杂度 看递归次数,空间复杂度看栈帧深度

  4. 同一个问题,不同算法的时间复杂度可能天差地别(如斐波那契数列递归O(2^N) vs 循环O(N))

🎯 学习建议

掌握了时间复杂度分析,你就掌握了评价算法的第一把尺子 。在后续学习各种数据结构(顺序表、链表、栈、队列、树等)时,都要养成先分析复杂度的习惯。

相关推荐
西门吹-禅2 小时前
node js 性能处理
开发语言·javascript·ecmascript
我不是8神2 小时前
go-zero微服务框架总结
开发语言·微服务·golang
Ronaldinho Gaúch2 小时前
算法题中的日期问题
开发语言·c++·算法
Chary20162 小时前
Opencascade VTK 集成服务 VIS
算法
麦德泽特2 小时前
机器人赛事系统架构:基于UDT和MQTT的低延迟、高可靠通信
c语言·开发语言·安全·系统架构·机器人
lsx2024062 小时前
TypeScript 循环
开发语言
楠秋9203 小时前
代码随想录算法训练营第三十一天|56. 合并区间 、 738.单调递增的数字、968.监控二叉树
数据结构·算法·leetcode·贪心算法
utmhikari3 小时前
【架构艺术】治理后端稳定性的一些实战经验
java·开发语言·后端·架构·系统架构·稳定性·后端开发
csbysj20203 小时前
Swift 条件语句
开发语言