[学懂数据结构]谈谈复杂度(篇二)

99 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

前言

        我们知道,学好数据结构和算法的一个前提就是搞懂复杂度的概念和分析。

        本文就来分享一波作者对复杂度的学习心得与见解。本篇属于第二篇,主要介绍空间复杂度和复杂度的衍生思考的一些内容。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

空间复杂度

        时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。

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

        算法的空间复杂度通过计算算法所需的辅助空间的大小而实现,计算的是变量的个数。空间复杂度计算规则基本跟时间复杂度的类似,也使用大O渐进表示法 ,记作:S(n)=O(f(n)),n为问题输入规模,f(n)为n的函数。

        一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元,只需要分析该算法在实现时所需的辅助单元即可。

        函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定

示例1

 // 计算BubbleSort的空间复杂度?
 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;
     }
 }

        是不是O(n)呢?a不是有n个元素吗?注意啦,a数组不是算法设计时根据需要额外开辟的辅助空间!它是必不可少的必须空间,因为我们设计的这个冒泡排序算法就是要对这个数组进行操作的,而不是冒泡排序算法需要再开辟数组。这样一看,我们额外开辟的空间最多也只是常数个,所以空间复杂度为O(1)。

示例2

 // 计算Fibonacci的空间复杂度?
 // 返回斐波那契数列的前n项
 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项,在堆上申请了一个fibArray数组,所以空间复杂度为O(n)。

示例3

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

        这个递归前面讲过时间复杂度,实际上在递的过程中创建了n+1个栈帧,每次函数调用开辟一个,只有一个函数参数N被创建(栈的细节不关心),一共要创建n+1个N,所以空间复杂度为O(n)。

示例4

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

img

        我们来看看,首先,调用f(n)会调用f(n-1)和f(n-2),是不是先调用的f(n-1)?调用完f(n-1)后才会去调用f(n-2),调用f(n-1)会创建栈帧是吧,那调用完f(n-1)后该栈帧销毁,空间返还给系统,紧接着又要调用f(n-2)呢,不还得创建栈帧么,那用的是哪块空间啊?用的就是先前销毁的f(n-1)用过的那块呗,这也就是说f(n-1)和f(n-2)的函数栈帧创建用的是同一块空间。同理,如图所示:

image-20220727155839199

        所以来来回回用的就只有n个函数栈帧,每个函数栈帧中创建的都是常数个变量,所以空间复杂度为O(n)。

        若算法执行时所需辅助空间对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为(1)。


衍生思考

        有人说,我们项目之前都会进行性能测试,再做代码的时间复杂度、空间复杂度分析,是不是多此一举呢?而且,每段代码都分析一下时间复杂度、空间复杂度,是不是很浪费时间呢?你怎么看待这个问题呢?

答:

        我不认为是多此一举,渐进时间、空间复杂度分析为我们提供了一个很好的理论分析的方向,并且它是宿主平台无关的,能够让我们对我们的程序或算法有一个大致的认识,让我们知道,比如在最坏的情况下程序的执行效率如何,同时也为我们交流提供了一个不错的桥梁。我们可以说,算法1的时间复杂度是O(n),算法2的时间复杂度是O(logn),这样我们立刻就对不同的算法有了一个“效率”上的感性认识。

        当然,渐进式时间,空间复杂度分析只是一个理论模型,只能提供给粗略的估计分析,我们不能直接断定就觉得O(logn)的算法一定优于O(n), 针对不同的宿主环境,不同的数据集,不同的数据量的大小,在实际应用上面可能真正的性能会不同,个人觉得,针对不同的实际情况,进而进行一定的性能基准测试是很有必要的,比如在统一一批手机上(同样的硬件,系统等等)进行横向基准测试,进而选择适合特定应用场景下的最有算法。

        综上所述,渐进式时间,空间复杂度分析与性能基准测试并不冲突,而是相辅相成的,但是一个低阶的时间复杂度程序有极大的可能性会优于一个高阶的时间复杂度程序,所以在实际编程中,时刻关心理论时间,空间度模型是有助于产出效率高的程序的,同时,因为渐进式时间,空间复杂度分析只是提供一个粗略的分析模型,因此也不会浪费太多时间,重点在于在编程时,要具有这种复杂度分析的思维。


以上就是本文的全部内容了,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif