• 【数据结构】时间复杂度_空间复杂度


    作者:旧梦拾遗186

    专栏:数据结构成长日记

     

    每日励志:

    如果有一天,你的努力配得上你的梦想,那么你的梦想也绝对不会辜负你的努力。

    前言:

    小编带大家来学习数据结构中的复杂度问题。

    目录

    1.算法效率

    1.1 如何衡量一个算法的好坏

    1.2 算法的复杂度

    1.3 复杂度在校招中的考察

    2.时间复杂度

    2.1 时间复杂度的概念 

     2.2 大O的渐进表示法

    2.3常见时间复杂度计算举例  

    3. 空间复杂度

     

    4. 常见复杂度对比 

    5. 复杂度的oj练习 


    1.算法效率

    1.1 如何衡量一个算法的好坏

    如何衡量一个算法的好坏呢?比如对于以下斐波那契数列

    1. long long Fib(int N) {
    2. if(N < 3)
    3. return 1;
    4. return Fib(N-1) + Fib(N-2);
    5. }

    斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢? 

    1.2 算法的复杂度

    算法效率分析分为两种: 第一种是时间效率,第二种是空间效率 。时间效率被称为时间复杂度,而空间效率 被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度 而空间复杂度主要衡量一个算法所需要 的额外空间 ,在计算机发展的早期,计算机的存储容量小。所以对空间复杂度很是在乎。但是经过计算机 行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法 的空间复杂度。

    1.3 复杂度在校招中的考察

    2.时间复杂度

    2.1 时间复杂度的概念 

    时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个 函数(数学的带未知数的函数表达式) ,它定量描述了该算法的运行时间。 一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知 道。但是我们需要每个算法都上机测试吗? 是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。一个算法所花费的时间与其中语句的执行次数成正比例, 算法中的基本操作的执行次数,为算法 的时间复杂度。
    为什么 一 个算法执行所耗费的时间,从理论上说,是不能算出来的?
    举个例子:

     即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

    举个例子:(不能纯粹的数循环,要看具体的算法逻辑,进行计算)

    1. // 请计算一下Func1中++count语句总共执行了多少次?
    2. void Func1(int N) {
    3. int count = 0;
    4. for (int i = 0; i < N ; ++ i) {
    5. for (int j = 0; j < N ; ++ j)
    6. {
    7. ++count;
    8. }
    9. }
    10. for (int k = 0; k < 2 * N ; ++ k)
    11. {
    12. ++count;
    13. }
    14. int M = 10;
    15. while (M--)
    16. {
    17. ++count;
    18. }
    19. printf("%d\n", count);
    20. }

    计算次数的时间复杂度函数:n*n+2*n+10;

    Func1 执行的基本操作次数 :

    N = 10 F(N) = 130
    N = 100 F(N) = 10210
    N = 1000 F(N) = 1002010
    实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而 只需要大概执行次数 那么这 里我们使用大O的渐进表示法。

     2.2 O的渐进表示法

    O 符号( Big O notation ):是用于描述函数渐进行为的数学符号。
    推导大 O 阶方法:
    1 、用常数 1 取代运行时间中的所有加法常数。
    2 、在修改后的运行次数函数中,只保留最高阶项。
    3 、如果最高阶项存在且不是 1 ,则去除与这个项目相乘的常数。得到的结果就是大 O 阶。
    使用大 O 的渐进表示法以后, Func1 的时间复杂度为:
    N = 10 F(N) = 100
    N = 100 F(N) = 10000
    N = 1000 F(N) = 1000000
    通过上面我们会发现大 O 的渐进表示法 去掉了那些对结果影响不大的项 ,简洁明了的表示出了执行次数。
    另外有些算法的时间复杂度存在最好、平均和最坏情况:
    最坏情况:任意输入规模的最大运行次数 ( 上界 )
    平均情况:任意输入规模的期望运行次数
    最好情况:任意输入规模的最小运行次数 ( 下界 )
    例如:在一个长度为 N 数组中搜索一个数据 x
    最好情况: 1 次找到
    最坏情况: N 次找到
    平均情况: N/2 次找到
    在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为 O(N)

    2.3常见时间复杂度计算举例  

    实例 1
    1. // 计算Func2的时间复杂度?
    2. void Func2(int N)
    3. {
    4. int count = 0;
    5. for (int k = 0; k < 2 * N ; ++ k)
    6. {
    7. ++count;
    8. }
    9. int M = 10;
    10. while (M--)
    11. {
    12. ++count;
    13. }
    14. printf("%d\n", count);
    15. }

    实例2:

    1. // 计算Func3的时间复杂度?
    2. void Func3(int N, int M)
    3. {
    4. int count = 0;
    5. for (int k = 0; k < M; ++ k)
    6. {
    7. ++count;
    8. }
    9. for (int k = 0; k < N ; ++ k)
    10. {
    11. ++count;
    12. }
    13. printf("%d\n", count);
    14. }

    实例3:

    1. // 计算Func4的时间复杂度?
    2. void Func4(int N)
    3. {
    4. int count = 0;
    5. for (int k = 0; k < 100; ++ k)
    6. {
    7. ++count;
    8. }
    9. printf("%d\n", count);
    10. }

    实例4:

    1. // 计算strchr的时间复杂度?
    2. const char * strchr ( const char * str, int character );

     

     

    另外有些算法的时间复杂度存在最好、平均和最坏情况:
    最坏情况:任意输入规模的最大运行次数 ( 上界 )
    平均情况:任意输入规模的期望运行次数
    最好情况:任意输入规模的最小运行次数 ( 下界 )

    4. 实例4基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为 O(N)

    实例5:

    1. // 计算BubbleSort的时间复杂度?
    2. void BubbleSort(int* a, int n) {
    3. assert(a);
    4. for (size_t end = n; end > 0; --end)
    5. {
    6. int exchange = 0;
    7. for (size_t i = 1; i < end; ++i)
    8. {
    9. if (a[i-1] > a[i])
    10. {
    11. Swap(&a[i-1], &a[i]);
    12. exchange = 1;
    13. }
    14. }
    15. if (exchange == 0)
    16. break;
    17. }
    18. }

      这是一个冒泡排序算法。我们需要注意,计算时间复杂度时不能只看循环,这种做法是肤浅的、错误的。我们应该关注算法的内核。就拿这个冒泡算法来说,我们通过图解来表达清楚冒泡排序的核心:

    可以看到,最好的情况就是执行 N 次(不是 1 次原因是即使不进行元素交换,此算法也会对数组遍历检查)。最差的情况就是计算 1、2、3……N-2、N-1 这个等差数列之和,即 (N-1)*N/2 ,那么要表示时间复杂度,就要用大 O 的渐进表示法,即去除影响不大的项,O(N^2) 。

    实例6:

    1. // 计算BinarySearch的时间复杂度?
    2. int BinarySearch(int* a, int n, int x) {
    3. assert(a);
    4. int begin = 0;
    5. int end = n-1;
    6. // [begin, end]:begin和end是左闭右闭区间,因此有=号
    7. while (begin <= end)
    8. {
    9. int mid = begin + ((end-begin)>>1);
    10. if (a[mid] < x)
    11. begin = mid+1;
    12. else if (a[mid] > x)
    13. end = mid-1;
    14. else
    15. return mid;
    16. }
    17. return -1; }

    二分查找,具体逻辑如图

     

     

     实例7:

    1. // 计算阶乘递归Fac的时间复杂度?
    2. long long Fac(size_t N) {
    3. if(0 == N)
    4. return 1;
    5. return Fac(N-1)*N; }

    实例8:

    1. // 计算斐波那契递归Fib的时间复杂度?
    2. long long Fib(size_t N) {
    3. if(N < 3)
    4. return 1;
    5. return Fib(N-1) + Fib(N-2);
    6. }

     

    斐波那契数列算法在现实生活中,没什么用,因为其时间复杂度过大。 

    实例答案及分析:
    1. 实例 1 基本操作执行了 2N+10 次,通过推导大 O 阶方法知道,时间复杂度为 O(N)
    2. 实例 2 基本操作执行了 M+N 次,有两个未知数 M N ,时间复杂度为 O(N+M)
    3. 实例 3 基本操作执行了 10 次,通过推导大 O 阶方法,时间复杂度为 O(1)
    4. 实例 4 基本操作执行最好 1 次,最坏 N 次,时间复杂度一般看最坏,时间复杂度为 O(N)
    5. 实例 5 基本操作执行最好 N 次,最坏执行了 (N*(N+1)/2 次,通过推导大 O 阶方法 + 时间复杂度一般看最
    坏,时间复杂度为 O(N^2)
    6. 实例 6 基本操作执行最好 1 次,最坏 O(logN) 次,时间复杂度为 O(logN) ps logN 在算法分析中表示是底
    数为 2 ,对数为 N 。有些地方会写成 lgN 。(建议通过折纸查找的方式讲解 logN 是怎么计算出来的)
    7. 实例 7 通过计算分析发现基本操作递归了 N 次,时间复杂度为 O(N)
    8. 实例 8 通过计算分析发现基本操作递归了 2^N 次,时间复杂度为 O(2^N) 。(建议画图递归栈帧的二叉树
    讲解)

    3. 空间复杂度

    空间复杂度也是一个数学表达式,是对一个算法在运行过程中 临时占用存储空间大小的量度
    空间复杂度不是程序占用了多少 bytes 的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
    空间复杂度计算规则基本跟实践复杂度类似,也使用 O 渐进表示法
    注意: 函数运行时所需要的栈空间 ( 存储参数、局部变量、一些寄存器信息等 ) 在编译期间已经确定好了,因 此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

     

    实例1

    1. // 计算BubbleSort的空间复杂度?
    2. void BubbleSort(int* a, int n)
    3. {
    4. assert(a);
    5. for (size_t end = n; end > 0; --end)
    6. {
    7. int exchange = 0;
    8. for (size_t i = 1; i < end; ++i)
    9. {
    10. if (a[i-1] > a[i])
    11. {
    12. Swap(&a[i-1], &a[i]);
    13. exchange = 1;
    14. }
    15. }
    16. if (exchange == 0)
    17. break;
    18. }
    19. }

     

    实例2

    1. // 计算Fibonacci的空间复杂度?
    2. // 返回斐波那契数列的前n项
    3. long long* Fibonacci(size_t n) {
    4. if(n==0)
    5. return NULL;
    6. long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
    7. fibArray[0] = 0;
    8. fibArray[1] = 1;
    9. for (int i = 2; i <= n ; ++i)
    10. {
    11. fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
    12. }
    13. return fibArray;
    14. }

     

    实例3

    1. // 计算阶乘递归Fac的空间复杂度?
    2. long long Fac(size_t N) {
    3. if(N == 0)
    4. return 1;
    5. return Fac(N-1)*N;
    6. }

     

    实例答案及分析:

    1. 实例 1 使用了常数个额外空间,所以空间复杂度为 O(1)
    2. 实例 2 动态开辟了 N 个空间,空间复杂度为 O(N)
    3. 实例 3 递归调用了 N 次,开辟了 N 个栈帧,每个栈帧使用了常数个空间。空间复杂度为 O(N)

     

    4. 常见复杂度对比 

     

     

     

    5. 复杂度的oj练习 

    解题思路:所有数相加和减去当前数和就是那个所缺的数

    1. int missingNumber(int* nums, int numsSize){
    2. int sum = 0;
    3. for(int i = 0;i
    4. {
    5. sum+=nums[i];
    6. }
    7. int ret = 0;
    8. for(int i = 0;i<=numsSize;i++)
    9. {
    10. ret+=i;
    11. }
    12. return ret-sum;
    13. }

     

    轮转数组 

     思路一:存放最右边的数,一次右移一个,需要移动K%numsSize次

    1. void rotate(int* nums, int numsSize, int k){
    2. if (k > numsSize)
    3. {
    4. k %= numsSize;
    5. }
    6. for (int i = 0; i < k; i++)
    7. {
    8. int tmp = nums[numsSize - 1];
    9. for (int j = numsSize - 1; j >= 1; j--)
    10. {
    11. nums[j] = nums[j - 1];
    12. }
    13. nums[0] = tmp;
    14. }
    15. }

    但是 可以发现运行超时:

    思路二:开辟一个新的数组存放需要移动的数,在把剩余的数存放到新的数组中,最后把新数组中的数字赋值给原来的数组

    1. void rotate(int* nums, int numsSize, int k) {
    2. if (k >= numsSize)
    3. {
    4. k %= numsSize;
    5. }
    6. int* arr = (int*)malloc(sizeof(int) * (numsSize+1));
    7. for (int i = 0; i
    8. {
    9. arr[k-i-1] = nums[numsSize - i-1];
    10. }
    11. for (int i = k; i < numsSize; i++)
    12. {
    13. arr[i] = nums[i-k];
    14. }
    15. for(int i = 0;i
    16. {
    17. nums[i] = arr[i];
    18. }
    19. }

    思路三:前n-k个逆置,后k个逆置,整体逆置

    1. void right_move(int arr[], int left, int right)
    2. {
    3. while (left < right)
    4. {
    5. int tmp = arr[left];
    6. arr[left] = arr[right];
    7. arr[right] = tmp;
    8. ++left;
    9. --right;
    10. }
    11. }
    12. void rotate(int* nums, int numsSize, int k) {
    13. if (k >= numsSize)
    14. {
    15. k %= numsSize;
    16. }
    17. right_move(nums, 0, numsSize - 1 - k);
    18. right_move(nums, numsSize - k, numsSize - 1);
    19. right_move(nums, 0, numsSize - 1);
    20. }

     

     

    结语:

    每个人的成长都是能力和想要得到的东西,不断匹配的过程,当你的才华和欲望不匹配时,你就该静下心来学习了,如果小编的总结能对你有所帮助,希望小伙伴们三连加关注哦,你的支持是小编创作的最大动力。

     

  • 相关阅读:
    Dynamic DataSource 多数据源配置【 Springboot + DataSource + MyBatis Plus + Druid】
    leetcode_2525 根据规则将箱子分类
    SSE图像算法优化系列三十二:Zhang\Guo图像细化算法的C语言以及SIMD指令优化
    云服务器如何快速安装各种开发环境(php,mysql,redis,Apache)
    力扣练习——45 二叉树的锯齿形层次遍历
    java Collection和Map接口的区别
    【程序的编译(预处理操作)+链接】
    LNMP架构概述及相关服务的搭建
    jira提交bug规范
    JJJ:python学习笔记
  • 原文地址:https://blog.csdn.net/weixin_67900732/article/details/126056681