算法复杂度从入门到精通:理论与实战双解析

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划程序人生

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

  • 算法复杂度从入门到精通:理论与实战双解析
    • [1. 啥是算法复杂度?](#1. 啥是算法复杂度?)
      • [① 时间复杂度:代码的跑步速度 ⏱️](#① 时间复杂度:代码的跑步速度 ⏱️)
      • [② 空间复杂度:代码的占地面积 📦](#② 空间复杂度:代码的占地面积 📦)
      • [⚖️ 时间vs空间:鱼和熊掌咋选?](#⚖️ 时间vs空间:鱼和熊掌咋选?)
    • [2. 常见复杂度类型&分析技巧](#2. 常见复杂度类型&分析技巧)
      • [① 大O渐近表示法:别纠结细节,抓重点!](#① 大O渐近表示法:别纠结细节,抓重点!)
      • [② 最坏、最好、平均情况:为啥咱们只盯最坏的?](#② 最坏、最好、平均情况:为啥咱们只盯最坏的?)
      • [③ 常见时间复杂度大盘点:从快到慢排个队](#③ 常见时间复杂度大盘点:从快到慢排个队)
      • [④ 常见空间复杂度大盘点:比时间复杂度简单多了](#④ 常见空间复杂度大盘点:比时间复杂度简单多了)
    • [3. 实战演练:两道LeetCode经典题](#3. 实战演练:两道LeetCode经典题)
    • [4. 写在最后:复杂度优化的小哲学](#4. 写在最后:复杂度优化的小哲学)

算法复杂度从入门到精通:理论与实战双解析

Hello,大家好,新文又和大家见面了,我们这次来聊聊数据结构与算法里的核心知识点------算法复杂度

咱们写代码,不光要能跑通,还得追求跑得快、占内存少。而衡量代码快不快、省不省的关键,就是时间复杂度和空间复杂度。今天咱们就掰开揉碎了讲,再配上两道经典LeetCode题实战,保证你听完就懂!🚀

1. 啥是算法复杂度?

评价一个算法好不好,咱们一般看两个维度:跑得多快占多少内存,这俩对应的就是时间复杂度和空间复杂度。

① 时间复杂度:代码的跑步速度 ⏱️

咱平时说这段代码跑了0.5秒,其实没啥参考价值------不同电脑配置、不同编程语言,跑出来的时间都不一样。

真正靠谱的衡量标准是:随着输入数据规模n变大,算法执行的基本操作次数会怎么增长。这个增长趋势,就是时间复杂度。

简单说:时间复杂度就是看你的代码,数据量翻倍时,工作量会翻几倍?

② 空间复杂度:代码的占地面积 📦

这个更直白:算法在运行过程中,临时占用的存储空间大小,随着输入规模n增长的趋势。

注意哦,是临时空间!比如你定义了一个固定大小的变量,不管n多大,它占的空间都不变;但如果定义了一个长度为n的数组,那空间就跟着n涨了。

简单说:空间复杂度就是看你的代码,数据量翻倍时,需要的临时内存会翻几倍?

⚖️ 时间vs空间:鱼和熊掌咋选?

早年间电脑内存金贵得很,大家都抠抠搜搜省空间;现在不一样了,内存又大又便宜,咱们更看重时间效率

甚至很多时候,会用以空间换时间的套路------比如多开一个数组存中间结果,避免重复计算,让代码跑得更快。

2. 常见复杂度类型&分析技巧

① 大O渐近表示法:别纠结细节,抓重点!

分析复杂度时,咱们不用算精确的操作次数,只需要用大O表示法描述增长趋势就行。它的推导规则超简单,记好这三步:

  1. 常数项直接扔
    比如代码执行次数是2n+100,100是常数项,不管n多大,它的影响都会越来越小,直接扔掉,变成2n。
  2. 只留最高次项
    比如次数是n²+5n+20,n²是最高次项,n越大,它的影响越主导,直接保留n²,扔掉5n和20。
  3. 最高次项的系数也扔掉
    比如次数是3n²,系数3对增长趋势没影响------n²的增长速度,和3n²是一个量级的,直接写成n²。

举个栗子🌰:

看这段代码的时间复杂度:

c 复制代码
void calc_sum(int n) {
    int sum = 0;                  // 1次操作
    for (int i = 0; i < n; i++) { // 循环n次
        sum += i;                 // 每次循环1次操作,共n次
    }
    printf("%d\n", sum);          // 1次操作
}

总操作次数是1 + n + n + 1 = 2n+2。按规则推导:

  • 扔掉常数项2 → 2n
  • 最高次项是n,扔掉系数2 → n
    所以时间复杂度是 O(n)

是不是超简单?记住:大O表示法只关心趋势,不关心细节。

② 最坏、最好、平均情况:为啥咱们只盯最坏的?

分析复杂度时,会遇到三种情况,举个在数组里找某个数的例子你就懂了:

  • 最好情况:运气爆棚,第一个元素就是要找的数,只需要1次操作 → 时间复杂度O(1)。
  • 最坏情况:运气贼差,遍历到最后一个元素才找到,或者压根没找到,需要n次操作 → 时间复杂度O(n)。
  • 平均情况:把所有可能的情况算个平均值,大概需要n/2次操作 → 时间复杂度也是O(n)。

划重点:咱们平时说的时间复杂度,默认都是最坏情况!

为啥?因为最坏情况能给代码的性能兜底------你能保证,代码再慢也不会超过这个速度,这才是最有参考价值的。

③ 常见时间复杂度大盘点:从快到慢排个队

不同复杂度的代码,差距可不是一点点!咱们按从快到慢排个队,记住这些常见的:

复杂度 名字 特点 典型例子
O(1) 常数阶 最快!操作次数和n没关系 访问数组的某个元素、加减乘除运算
O(log n) 对数阶 超高效!n翻倍,操作次数只加1 二分查找
O(n) 线性阶 n翻倍,操作次数也翻倍 遍历数组、单层for循环
O(n log n) 线性对数阶 比线性阶慢一点,但比平方阶快多了 快速排序、归并排序
O(n²) 平方阶 n翻倍,操作次数翻4倍 冒泡排序、两层嵌套for循环
O(2ⁿ) 指数阶 超级慢!n稍微变大,代码就跑不动了 未优化的递归斐波那契数列
O(n!) 阶乘阶 最慢的级别之一!n增大一点就无法承受 暴力解旅行商问题

复杂度增长趋势图:

复制代码
        ^
  时间  |
  / 2ⁿ  |                  . (指数阶,嗖嗖涨)
 /      |                 .
/  n²   |            . . (平方阶,涨得快)
|       |         .
|  n    |     . . (线性阶,平稳涨)
|       |  .
| log n | . (对数阶,涨得超慢)
|_______|________________> 输入规模n
    O(1) (常数阶,横着走)

再举几个经典例子加深理解:

  1. 冒泡排序 O(n²)
    它的比较次数是(n-1)+(n-2)+...+1 = n(n-1)/2,按大O规则,就是O(n²)。数据量n=1000时,操作次数大概是50万;n=2000时,直接涨到200万!

  2. 二分查找 O(log n)
    每次查找都把范围缩小一半,比如找1000个数据里的数,最多只需要10次操作;找2000个数据,也只需要11次------这就是对数阶的魅力!

  3. 递归斐波那契数列 O(2ⁿ)
    代码长这样:

    c 复制代码
    int Fib(int n) {
        return n < 2 ? n : Fib(n-1)+Fib(n-2);
    }

    这个递归会重复计算超多值,比如算Fib(5)时,Fib(3)会被算两次。n=20时还能忍,n=30时就慢得离谱了!

④ 常见空间复杂度大盘点:比时间复杂度简单多了

空间复杂度的分析更直接,主要看临时开辟的空间

  • O(1):常数空间。不管n多大,临时变量就那几个,比如定义int a, b;。
  • O(n):线性空间。临时开了一个长度为n的数组、列表,比如int temp[n];。
  • O(log n):对数空间。常见于递归算法,比如二分查找的递归实现,递归深度是log n,系统栈的空间就是O(log n)。
  • O(n²):平方空间。临时开了一个n×n的二维数组,比如int matrix[n][n];。

3. 实战演练:两道LeetCode经典题

光说不练假把式,咱们用两道高频题,看看怎么用复杂度约束解题!

练习1:找缺失的数字 (LeetCode 268)

题目 :给定一个包含[0,n]中n个数的数组nums,找出[0,n]里没出现的那个数。要求时间复杂度O(n)

示例:输入nums = [3,0,1] → 输出2

解题思路:三种思路,两种达标!
  1. 思路1:排序后遍历 → 不达标❌

    先排序数组,再遍历找空缺。但最快的排序算法(比如归并排序)时间复杂度是O(n log n),超过了题目要求的O(n),直接pass!

  2. 思路2:数学求和法 → 达标⭐
    核心原理:0到n的整数和是个等差数列,用公式总和 = n*(n+1)/2就能秒算。然后算出数组里所有数的和,两者相减,差就是缺失的数!

    复杂度:遍历一次数组求和,时间O(n);只用到几个变量,空间O(1),完美达标!

    C代码实现

    c 复制代码
    #include <stdio.h>
    
    int missingNumber(int* nums, int numsSize) {
        // 算0到n的理论总和
        int expected_sum = numsSize * (numsSize + 1) / 2;
        // 算数组的实际总和
        int actual_sum = 0;
        for (int i = 0; i < numsSize; i++) {
            actual_sum += nums[i];
        }
        // 差值就是缺失的数
        return expected_sum - actual_sum;
    }
    
    int main() {
        int nums[] = {3, 0, 1};
        int sz = sizeof(nums)/sizeof(nums[0]);
        printf("缺失的数字是:%d\n", missingNumber(nums, sz)); // 输出2
        return 0;
    }
  3. 思路3:异或运算法 → 更优⭐⭐

    这个方法更巧妙,利用异或运算的三个神仙性质:

    • 性质1:a ^ a = 0(相同的数异或,结果为0)
    • 性质2:a ^ 0 = a(任何数和0异或,结果还是自己)
    • 性质3:交换律、结合律(abc = acb)

    核心思路 :把0到n的所有数,和数组里的所有数异或一遍。成对出现的数会相互抵消(变成0),最后剩下的就是只出现一次的缺失数字

    复杂度:时间O(n),空间O(1),而且不会有求和法的溢出风险(比如n超大时,求和可能超出int范围),更稳妥!

    C代码实现

    c 复制代码
    #include <stdio.h>
    
    int missingNumber(int* nums, int numsSize) {
        int missing = numsSize; // 先把n放进去异或
        for (int i = 0; i < numsSize; i++) {
            missing ^= i ^ nums[i]; // 索引i和数组元素nums[i]异或
        }
        return missing;
    }
    
    int main() {
        int nums[] = {9,6,4,2,3,5,7,0,1};
        int sz = sizeof(nums)/sizeof(nums[0]);
        printf("缺失的数字是:%d\n", missingNumber(nums, sz)); // 输出8
        return 0;
    }

练习2:旋转数组 (LeetCode 189)

题目 :给定一个数组,把元素向右移动k个位置,k是非负数。要求空间复杂度O(1)(原地修改,不能开新数组)。

示例:输入nums = [1,2,3,4,5,6,7], k=3 → 输出[5,6,7,1,2,3,4]

解题思路:三种思路,只有一种最优!
  1. 思路1:暴力法(每次移一位)→ 超时❌

    循环k次,每次把最后一个元素移到最前面。比如k=3,就移3次。
    复杂度:时间O(n*k),空间O(1)。但k很大时(比如k=10^5),直接超时,pass!

  2. 思路2:开新数组 → 空间不达标❌

    开一个新数组,把原数组的元素放到新数组的正确位置,再拷贝回去。
    复杂度:时间O(n),但空间O(n),违反了O(1)的要求,pass!

  3. 思路3:数组翻转法 → 最优解⭐⭐⭐

    这个方法简直是天才思路!核心就是三次翻转,全程不用额外空间:

    • 步骤1:整体翻转整个数组
    • 步骤2:翻转前k个元素
    • 步骤3:翻转后n-k个元素

    举个栗子🌰:nums = [1,2,3,4,5,6,7], k=3

    1. 整体翻转 → [7,6,5,4,3,2,1]
    2. 翻转前3个 → [5,6,7,4,3,2,1]
    3. 翻转后4个 → [5,6,7,1,2,3,4](搞定!)

    小细节:如果k>n,比如k=10,n=7,其实右移10次和右移3次效果一样(10%7=3),所以先算k = k%n,避免无效操作!

    复杂度:时间O(n)(三次翻转都是O(n)),空间O(1),完美符合要求!

    C代码实现

    c 复制代码
    #include <stdio.h>
    
    // 辅助函数:翻转数组[left, right]区间的元素
    void reverse(int* nums, int left, int right) {
        while (left < right) {
            // 交换两个元素
            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
            left++;
            right--;
        }
    }
    
    void rotate(int* nums, int numsSize, int k) {
        k %= numsSize; // 处理k>numsSize的情况
        if (k == 0) return; // 不用旋转
        reverse(nums, 0, numsSize-1); // 1.整体翻转
        reverse(nums, 0, k-1); // 2.翻转前k个
        reverse(nums, k, numsSize-1); // 3.翻转后n-k个
    }
    
    // 打印数组的辅助函数
    void printArray(int* nums, int size) {
        for (int i = 0; i < size; i++) {
            printf("%d ", nums[i]);
        }
        printf("\n");
    }
    
    int main() {
        int nums[] = {1,2,3,4,5,6,7};
        int sz = sizeof(nums)/sizeof(nums[0]);
        int k = 3;
        printf("原数组:");
        printArray(nums, sz);
        rotate(nums, sz, k);
        printf("旋转后:");
        printArray(nums, sz); // 输出5 6 7 1 2 3 4
        return 0;
    }

4. 写在最后:复杂度优化的小哲学

今天咱们聊了这么多,其实核心就一个:写代码时,要养成分析复杂度的习惯

  • 不要满足于代码能跑,多问自己一句:这个算法的时间和空间复杂度是多少?有没有更优的写法?
  • 记住以空间换时间的思路,但也要看场景------比如嵌入式设备内存小,就得反过来以时间换空间。
  • 复杂度只是一个参考,实际写代码时,还要考虑代码的可读性、可维护性。毕竟,能让同事看懂的代码,才是好代码!

最后,算法学习没有捷径,多刷题、多总结,你也能成为复杂度优化的高手!

如果这篇文章对你有帮助,别忘了点赞收藏哦!有啥疑问或者想聊的算法题,评论区见~Happy Coding!😊

相关推荐
智驱力人工智能几秒前
在安全与尊严之间 特殊人员离岗检测系统的技术实现与伦理实践 高风险人员脱岗预警 人员离岗实时合规检测 监狱囚犯脱岗行为AI分析方案
人工智能·深度学习·opencv·算法·目标检测·cnn·边缘计算
培林将军3 分钟前
C语言指针
c语言·开发语言·算法
adam_life6 分钟前
P3375 【模板】KMP
算法
jllllyuz9 分钟前
基于差分进化算法优化神经网络的完整实现与解析
人工智能·神经网络·算法
yongui4783410 分钟前
基于卡尔曼滤波的电池荷电状态(SOC)估计的MATLAB实现
开发语言·算法·matlab
有一个好名字11 分钟前
力扣-盛最多水的容器
算法·leetcode·职场和发展
D_FW20 分钟前
数据结构第二章:线性表
数据结构·算法
技术狂人16830 分钟前
(六)大模型算法与优化 15 题!量化 / 剪枝 / 幻觉缓解,面试说清性能提升逻辑(深度篇)
人工智能·深度学习·算法·面试·职场和发展
tobias.b1 小时前
408真题解析-2009-8-数据结构-B树-定义及性质
数据结构·b树·计算机考研·408考研·408真题
CoovallyAIHub1 小时前
为你的 2026 年计算机视觉应用选择合适的边缘 AI 硬件
深度学习·算法·计算机视觉