• 【数据结构】算法的时间复杂度和空间复杂度(复习学习兼顾)


    🏠 大家好,我是 兔7 ,一位努力学习C++的博主~💬

    🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀

    🚀 如有不懂,可以随时向我提问,我会全力讲解~

    🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!

    🔥 你们的支持是我创作的动力!

    🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!

    🧸 人的心态决定姿态!

    🚀 本文章CSDN首发!

    目录

    0. 前言

    1. 算法效率

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

    1.2 算法的复杂度

    2.时间复杂度

    2.1 时间复杂度的概念

    2.2 大O的渐进表示法

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

    3.空间复杂度

    4. 常见复杂度对比

    5. 复杂度的oj练习


    0. 前言

            此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

            大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

            感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!

    1. 算法效率

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

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

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

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

    1.2 算法的复杂度

            算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度空间复杂度

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

    2.时间复杂度

    2.1 时间复杂度的概念

            时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数(数学上的函数表达式),它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。

            但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度

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

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

    Func1 执行的基本操作次数 :N^2 + 2*N + 10

    • 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的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

    另外有些算法的时间复杂度存在最好、平均和最坏情况:

    最坏情况:任意输入规模的最大运行次数(上界)

    平均情况:任意输入规模的期望运行次数

    最好情况:任意输入规模的最小运行次数(下界)

    例如:在一个长度为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*N + 10 ,所以最后就是 O(N)了。

    实例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. }

            这里我们可以看到,按照执行次数就是 N + M,但是这里因为不知道 N 和 M 哪个更大,所以不能取舍,具体情况就应该根据实际来,但是这里就是O(N+M)。

    实例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. }

            这里因为执行的次数为常数 100,所以这里就是O(1)。

    实例4:  

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

            这个题就是将字符串转化为字符,所以有多少个字符就执行几次,所以这里就是O(N)。

    实例5:  

    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. }

            这里是冒泡排序,这里的逻辑就是从第一个开始分别与其它的数开始比较,如果大于(小于)就交换,直到到最后一个数的前一个为止,所以这里就是O(N^2)。

    实例6:  

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

            这里是二分查找,这里就不能只看循环了,要看里面的逻辑,二分查找的逻辑就是找到中间的数,看这个数比不比我们要找的数大,如果大就在中间这个数的左边那个数到第一个数这段区间进行查找,以此类推。

            但是二分查找有一个缺点就是数组必须有序,所以有很大限制。

            这里的运算是这样的:设x是执行了多少次,N/2/2/2.... = 1,也就是一直二分查找,最后为1的时候找到了,然后转化一下就是 N = 2^x ,所以最后得到的是 x = logN。所以最后得到的就是O(logN)。

     实例7: 

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

            这个其实就是一个求 N!,所以递归之后进行的次数就是 N次,所以就是O(N)。

    实例8:  

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

            这个是斐波那契数列,接下来看一下展开图:

            我们知道,算 F(6) 的时候,要算 F(5) F(4) ,算 F(5) 的时候,要算 F(4) F(3) ,最后得到的结论就是第一行执行了1次,第二行执行了2次,第三行执行了4次,第四行执行了8次,我们可以算出来第N行总共执行了 2^N - 1 次,所以最后就是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. }

            这里的冒泡排序的空间复杂度不是O(N),有的人认为总共对N个数进行冒泡排列,就以为是N,其实不是,N个数是 a 这个指针传参来的,我们算只算我们在函数中创建的,所以在函数中我们只创建了 end 、 exchange、i 这三个。

            有的人又认为循环了这么多次,肯定是创建了循环次数个变量,其实不然,这里创建后出了作用域就销毁,销毁后再进来还是用同一块空间,也就是说,一个变量一直用的是同一块空间。

            所以最后的空间复杂度是O(1)。

    实例2:  

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

            这个斐波那契数列是因为在中间开辟了 n + 1 个空间,然后用的是循环的方式去运算,所以是在这 n + 1 个空间中去操作的。

            所以最后的空间复杂度是O(N)。

    实例3:  

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

            这个求阶乘递归调用了N次,开辟了N个栈帧,每个栈帧用了常数次个空间。

            所以最后的空间复杂度是O(N)。

    实例4:  

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

            这个的空间复杂度有一些难度,这里就像是二叉树的递归,总之就是递归到最底层,然后再返回的时候会先销毁这个栈帧然后再建立新栈帧,所以这个的空间复杂度其实就是这个斐波那契数列递归的深度

            通过上面那么多题的讲解,这里我们要牢记一句话:空间可以重复利用,时间是一去不复返的。

    4. 常见复杂度对比

    一般算法常见的复杂度如下:

     

            所以这也就清楚了为什么我们不是按照次数一次一次去算,而是按照量级去算。

            开始的时候相差并不多,但是因为量级的原因,相差的越来越多,所以我们只要算出这个时间复杂度是哪个量级,就能够知道这个算法的运算效率。

    5. 复杂度的oj练习

    面试题 17.04. 消失的数字

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

            这个写的思路就是从 0 开始,一直加到 numsSize ,然后减去数组里的所有数之和,最后的出来的就是缺少的数。

            当然还有其他的办法,找一个合适的排序方法,然后依次对应从 0 到 n ,看缺少了哪个,当然这里的排序方法很重要,因为这里又时间复杂度的限制,上面的思路是最简单的思路。

    189. 轮转数组

     

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

    这道题有三个思路:

            第一个就是:按照要求,先保存第一个数,然后其他数进行移动,然后再填充保存的数,依次执行K次。

            第二个就是:从第K个位置到最后拷贝到一个数组上,然后再从第一个位置到第K个位置拷贝到数组里,最后再放回给定的数组中。当然要对K做取模处理。

            第三个就是我写的这种方法,三步旋转法,就是先让 0 到 K-1 的数进行逆序,然后让 K+1 到 最后一个数进行逆序,最后再整体一逆序,最后的出来的结果就是正确结果。

            总结,第三个方法是最好的,也是最难想到的。所以我们要总结经验,当我们记住了这种思想,没准面试的时候有道相似的题就可以用的到。

            如上就是 算法的时间复杂度和空间复杂度 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!

            再次感谢大家观看,感谢大家支持!​​​​​​​

  • 相关阅读:
    Linux权限基础知识
    mysql 不一定不走索引
    Python3用Django连接Mysql以及学习过程中的一些问题
    nginx的location指令(实战示例、匹配顺序、匹配冲突)
    Zookeeper【Curator客户端Java版】从0到1——万字学习笔记
    腾讯内部最通俗易懂的项目管理PPT
    【牛客 - 剑指offer / 快速幂】JZ16 数值的整数次方 两种方案(直接运算、快速幂) Java实现
    智云通CRM:如何将客户回绝扭转乾坤?
    高级路由配置
    【Java】SPI在Java中的实现与应用
  • 原文地址:https://blog.csdn.net/weixin_69725192/article/details/125960548