数据结构前言(时间复杂度,空间复杂度,讲解)

数据结构前言

看前说明

本文是我通过在学习后的思考总结,可能会有纰漏,也是第二次用md编辑器编写博客。如果有错误,欢迎大家理性提出,理性讨论。

1.什么是数据结构?

  1. 数据结构是计算机存储,组织数据的方式,指互相之间存在一种或者多种特定关系的数据元素的集合。

  2. 人话:我的理解是数据结构通俗来说就是计算机这个东西,它里面保存数据,保存元素的方式,不同方式即不同结构。这个方式有很多种,一般通常情况下,我们初级阶段接触到的就是他们是同一类数据就保存在一起,这些数据之间的关系就是只有一种,他们是同类的数据。

  • 就好比如数组,它就是一个最简单的数据结构,数组是一种存储某类型数据,某类型元素的方式,存在其中的数据和元素都是同类型的。

  • 以后还会有涉及到顺序表,链表,二叉树等等,元素之间,数据之间的关系就不是那么简单的了,他们有可以相互关联,关联的方式也各不相同。

  1. 那么这么多类型的数据结构存在的意义是什么呢?
  • 其主要的目的就是解决不同类型的问题。就好比如微信QQ上的好友列表,好友的信息包括了年龄,网名,性别等等,单凭借数组是无法解决问题的。
  • 其次便是,问题可以用某一个数据结构解决,但是极其耗时耗力,效率不高,简单说就是这个数据结构不合适去解决这个问题,这个时候为了提高效率和生产效能,就要去选择别的更合适的数据结构去解决问题。
  • 数据结构并无特别大的优劣之分,主要就是合不合适,就好比杀鸡杀牛,杀鸡就用杀鸡刀,杀牛就用杀牛刀。合适问题就好,并不是说那些高大上的数据结构就一定适用于所有的问题。
  1. 什么是算法?算法就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成为输出结构。

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

1.算法的效率

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

1.简单,简洁就是好吗?

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

就好比如上头这个斐波那契数列的递归实现方式,他确实看起来很高效很简洁,但是呢,实际测试一下效率稀碎。远不如下面这个迭代的方法。

long long Fib(int n)
{
   int a=1;
   int b=1;
   int c=1;
   while(n>2)
   {
       c=a+b;
       a=b;
       b=c;
       n--;
   }
   return c;
}

所以说,代码表面上的简单简洁不能代表这个算法的好坏优劣。

2.然后就有同学说了,运行时间短的是好算法。结论:算法的好坏不能用运行时间的长短来衡量。

为啥呢?好比如,一台搭载了i9cpu的电脑去运行冒泡排序,和一台搭载了i7cpu的电脑去运行快排,运行时间可能是一样的。所以在当今时代下,是不能凭着算法运行时间的长短去衡量算法的优劣的。那接下来就涉及到了算法的复杂度。

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

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

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

2.时间复杂度

2.1时间复杂度的概念
  • 时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。

  • 一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数正比例算法中的基本操作的执行次数,为算法的时间复杂度

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

void Func1(int N)
{
   int count = 0;
   for (int i = 0; i < N ; ++ i)
   {
       for (int j = 0; j < N ; ++ j)
       {
           ++count;
       }
   }
   for (int k = 0; k < 2 * N ; ++ k)
   {
       ++count;
   }
   int M = 10;
   while (M--)
   {
       ++count;
   }
   
   printf("%d\n",count);
}

Func1执行的基本操作次数:F(N)=N2+2*N+10

然后接下来就涉及到一个比较重要的概念了,我们可以先代入几个N的值看看:

  • N=10 F(N)=130
  • N=100 F(N)=10210
  • N=1000 F(N)=1002010
  • 以此类推…

我们可以发现在N越来越大的时候,是不是F(N)中的2N+10的值的大小显的越来越小昂,影响几乎是微乎其微。

  • 在这里我想说一下我们求时间复杂度并不是说要精确的去计算它的执行的一个次数,我们计算时间复杂度更像是求一个量级。
  • 怎么说呢,就像是拳击比赛一样,分有不同的量级,比如90公斤一个量级,70公斤一个量级(纯举例,本人不懂拳击。),时间复杂度也是如此,我们最主要的就是找出影响最大的那个值,那么在F(N)中,影响最大的就是N2,N2起到了决定性的作用,那么这个算法的量级我们就说是N2的这么一个量级。在N2这个量级看来,2N+10啥也不是,因为他们根本不属于一个量级,没有可比性。就好比如,70公斤这个量级在90公斤这个量级根本不够看的(纯举例说明)。那么为了更好的去表达这么一个意思,就引入了大O的渐进表示法。
2.2大O的渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:

  • 1、用常数1取代运行时间中的所有加法常数。

  • 2、在修改后的运行次数函数中,只保留最高阶项

  • 3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。即我们要除去最高阶项的系数,如最高阶项是3N2,那么在除去系数后就得到时间复杂度是O(N2)。

  • 使用大O的渐进表示法以后,Func1的时间复杂度为:O(N2)。

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

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

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

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

  • 例如:在一个长度为N数组中搜索一个数据x

  • 最好情况:1次找到

  • 最坏情况:N次找到

  • 平均情况:N/2次找到

在这里就又涉及到一个重点了,欸~,时间复杂度呢是选择最坏的情况为相应的时间复杂度。就好比如这段话上面的那个例如,时间复杂度就是O(N)。

这就好比如什么呢,好比如你和你女朋友一起约着时间出去玩去看电影,本来呢今天下午5点20分下课,你就可以去找女朋友了,然后你就拍着胸脯和女朋友说,我等会6点10分就到,结果,欸,老师拖堂了,拖堂20分钟,结果女朋友那边去到了,还等多了你20分钟,这时候你女朋友早生气的走了。好,现在算上拖堂,你说你6点30分就可以到,结果路上塞车,晚高峰,又塞了30分钟,是吧,同样的结果,女朋友又被气死了。欸,那你现在算上拖堂的时间和塞车的时间,说“宝,我7点钟到。”这回就没什么问题了,而且上面说到的拖堂和塞车是概率事件,最差最差也就是7点到,也算是准时准点,不会有任何问题。但是实际情况是不是很大可能都会比这最差的情况要好昂,就好比如昂,老师没拖堂,塞车也没那么久,你是不是就可以提前到,然后提前去到霸王茶姬点好你女朋友喜欢喝的,买好吃的,等着她,欸,是不是就好很多。

所以说时间复杂度是给我们一个最差的预期,管理好我们的预期,然后比算法给我们的最差预期。所以昂,时间复杂度是很符合人性的。

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

实例1:

void Func2(int N)
{
   int count = 0;
   for (int k = 0; k < 2 * N ; ++ k)
   {
       ++count;
   }
   int M = 10;
   while (M--)
   {
       ++count;
   }
   printf("%d\n",count);
}

我们按照上面的方法整一遍哈,F(N)=2*N+10,然后就可以得到了时间复杂度是O(N)。

实例2:

void Func3(int N, int M)
{
   int count = 0;
   for (int k = 0; k < M; ++ k)
   {
       ++count;
   }
   for (int k = 0; k < N ; ++ k)
   {
       ++count;
   }
   printf("%d\n,count");
}
  • OK,这里我先说一下,O(N),这里面这个字母N可以随便是什么,随便是什么K,A,B,C…等等,无关紧要,意思就是O(N)也可以是O(K),都行。

  • 那么这道题的时间复杂度是多少呢?首先,这里面有两个未知数,N和M,而且没有交代N和M之间的大小关系,那么此时它的时间复杂度就是O(M+N)。

  • 但是如果提到了M和N的大小关系:

  • M=N , 那么是不是就相当于F(N,M)=M+N=2M=2N,那么此时时间复杂度就是O(M)或者O(N)

  • M远大于N , 那么遵循上面说到的抓大头,选决定性的项,那么时间复杂度就是O(M)

  • N远大于M , 同理,时间复杂度是O(N)

实例3:

void Func4(int N)
{
   int count = 0;
   for (int k = 0; k < 100; ++ k)
   {
       ++count;
   }
   printf("%d\n", count);
}
  • 这里我们可以看到,k<100,而不是k<N这种,这个时候是不是时间复杂度是O(100)呢?不是哈。这里时间复杂度是O(1)。这里的1不是说代表运行1次,而是代表常数,有点类似于语文文言文里面的3次不是3次,而是代表很多次这样。所以再遇到类似的常数的时候,时间复杂度统一是O(1)。

  • 欸,这时候就可能有同学疑惑了,万一这个常数是1000,100000,1000000这些数呢,难到也是O(1)吗,没错,还是O(1)。为什么呢?分两方面:

  • 一方面,我们的计算机硬件技术太牛逼了,我们的cpu再不济应对这些小卡拉米也是易如反掌的事,不信你自己试试看,把例3里面的100换成1000000看看运行出来的速度是不是一样,这是技术带给我们的自信哈。换另一个例子说,就好比如,我王多鱼有一万亿,花那么1亿,2亿对我王多鱼有什么影响吗?没有影响。我花一块钱和我花一亿有什么很大区别吗,没有区别,因为我足够有钱,根本不在乎的。换言之就是我们的CPU足够快,很牛逼,一秒钟运算一亿次(纯举例,本人不懂cpu),代码里面常数再大对CPU来说也是无关痛痒的。

  • 其次,整型的范围最大也就是2 147 483 647,你不可能无限的递增下去。

  • 所以昂,O(1)。

实例4:

const char * strchr ( const char * str, int character );

这个函数作用是用来在str字符数组中查找一个字符,character是下标。

那它的时间复杂度是多少呢?这时候就要联系到上面说到的情况了。

  1. 可能一下子就找到了,第一个字符就是要找的,这是最好的情况。
  2. 可能在数组中间找到了,这算是平均的情况。
  3. 可能最后一个字符才是要找的,这是最坏的情况。

那么这时候的时间复杂度就是O(n)。选择最坏的情况,找n次,最后的时候找到了。

实例5:

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;
   }
}
  • 那么来到了代码稍微有点复杂的时候,我们就不能单纯的通过看代码去计算它的时间复杂度了。我们主要通过对思路的演算,进而求到相应的时间复杂度。

  • 就好比如这个例5是一个冒泡排序(以从小到大排序为例),冒泡排序思路是:从左到右,相邻元素进行比较,如果左边的数大于右边,就比如a[0]>a[1],那么a[0]就和a[1]交换。每比较一轮,就会找到序列中最大的一个数,这个数就会从序列的最右边冒出来。然后以此类推,最后实现无序到有序(从小到大)的转变。

  • 那么这时候这个例子的时间复杂度怎么算呢,首先我们是要找最坏的情况,那么最坏的情况是不是就得全部遍历一次,刚好是倒序的(从大到小),那么第一轮遍历就有n-1次(找到最大的),第二轮是n-2次(找到次大的),,,最后一轮就是0。然后就把每一轮的次数相加就得到F(n)=(n-1)*n/2,这其实就是一个等差数列,然后用等差数列求和就行。然后根据一开始提到的规则,最后的时间复杂度就是O(n2)。

  • 这里再补充一下,冒泡排序的最好情况的时间复杂度是多少?最好的情况的时间复杂度是O(n),不是O(1)。因为你总是要先有第一轮遍历过后,才知道这个序列是不是有序的。所以这里注意一下。

实例6:

int BinarySearch(int* a, int n, int x)
{
   assert(a);
   int begin = 0;
   int end = n-1;
   // [begin, end]:begin和end是左闭右闭区间,因此有=号
   while (begin <= end)
   {
       int mid = begin + ((end-begin)>>1);
       if (a[mid] < x)
           begin = mid+1;
       else if (a[mid] > x)
           end = mid-1;
       else
           return mid;
   }
   return -1;
}

不难看出,这段代码是一个二分查找。那么这个二分查找的时间复杂度是多少呢?(注意:二分查找的前提是有序)

  • 同样,在遇到这种类型的代码的时候我们就主要看的就是它的思路了,我们从思路入手。二分查找思路是不是折半查找昂,一次又一次的将范围,元素的数量减半。也就是:

  • 比较的区间个数:(注意,这是元素个数,查找区间里面的元素个数)

  • n

  • n/2

  • n/2/2

  • n/2/2/2

  • n/2/2/2…/2=1

  • 最坏的情况是不是我们最后才找到昂,就是说当我们的区间缩进到只剩下一个值的时候才找到,这时就是最坏的情况。

  • 这时候我们就要更深入的理解一下了。最坏的情况下查找了多少次?是不是我们除了多少次2就查找了多少次?第一次,n/2减半一次,第二次n/2/2减半两次…

  • 那么我们假设我们查找了x次,那么是不是2x=n,那么x=log2n就是我们查找的次数。

  • 如果大家不理解的话可以拿张纸试一下,将纸对半折,对半折到一定次数之后,再依此把它展开,展开一次,是不是就相当于我们折叠了多少次,思路就是这个思路。

  • 然后这里涉及到了另外一些规则。当查找次数为x=log2n时,时间复杂度为O(logn),把log2n简写成logn,主要是因为log2n这个东西在文本编辑器和一些代码编辑器上不好展示,所以简写成了logn。注意,是简写成logn,而不是lgn,因为有些书上有这样写道,这样会和数学里面的lgn就混淆了。

  • 同时,需要注意的是,只有log2n可以简写成logn,就是说除了以2为底的对数函数可以简写成logn,别的底数是不可以的。

  • 这里扩展一下,后面会学到一个数据结构叫B树,名字就是这么装逼。它的时间复杂度是O(logmn),也是不能简写的哈。

  • 这时候我们就可以比较一下暴力搜索O(n)和二分查找O(log2n)的差距了。

复杂度n=1000n=100wn=10亿
O(n)1000100w10亿
O(log2n)102030
  • 这里的表格中的数据只是个大概,因为210=1024嘛,就约为1000了,只是为了让大家能更直观的了解到他们之间的差距。当n=1000的时候,暴力搜索要检索1000次,而二分查找10次就搞定了,更恐怖的是n=10亿时,暴力搜索要10亿次,而二分查找只需要30次。所以说这个差别是很大的。

  • 不过因为二分查找有一个大前提就是,数组必须是有序的,然后现实情况确是,几乎没有。所以说二分查找只是理论上的巨人,实践上的矮子。而且在实际生活中,我们还需经常进行数据的增删查改,这样维护起来是十分麻烦的。

  • 不过我们后面会学到一些更为复杂的数据结构去解决这些问题,就好比如AVL树/红黑树,他们两个的时间复杂度也是O(logn),而且他们可以在无序的情况下,很好的运用,且可以很好的解决数据增删查改的这个问题。还有B树,这也是我们后面会学到的数据结构,B树就和数据库相关了。我后面也会陆陆续续的按照学习的顺序将博客写出来。

实例7:

long long Fac(size_t N)
{
   if(0 == N)
       return 1;
   
   return Fac(N-1)*N;
}

来到了递归算法的时间复杂度计算。我们可以拆解一下:(一次一次的递归调用)

  • Fac(N)
  • Fac(N-1)
  • Fac(N-2)
  • Fac(0)

那么这个递归算法的时间复杂度怎么计算呢?我自己在学习的时候一开始是有点懵的,我个人是因为收到函数括号内的N的影响,以为是N+N-1+N-2…+0,不过你好好想想,其实是和括号里面的N没有关系的,上面的拆解只是一次次的调用:

  • Fac(N) 1次调用

  • Fac(N-1) 1次调用

  • Fac(N-2) 1次调用

  • … 很多次调用

  • Fac(0) 1次调用

  • 所以将一次次的调用加起来,才是我们真正的运行次数,也就是时间复杂度,即时间复杂度是O(N),当时我是比较难去看懂的,因为主要一方面不熟悉,一方面收到括号里面N的影响。不过这个递归算法的本质,就是一次接着一次的调用这个Fac()函数,直到N=0。

  • 这里总结一下便是:递归算法的时间复杂度是多次调用的次数累加。因为时间是累加的,一去不复返的。

实例8:

long long Fib(size_t N)
{
   if(N < 3)
       return 1;
   
   return Fib(N-1) + Fib(N-2);
}

终于来到了最后的斐波那契数列。也是最开始提到的一个算法。

同样,我们拆解出来看看:

  • Fib(N)
  • Fib(N-1) Fib(N-2)
  • Fib(N-2) Fib(N-3) Fib(N-3) Fib(N-4)
  • Fib(3) Fib(2) …
  • Fib(2) Fib(1) … …

其实这个和上面例7那个递归算法差不多。也是算调用的次数:

  • 20次 Fib(N)
  • 21次 Fib(N-1) Fib(N-2)
  • 22次 Fib(N-2) Fib(N-3) Fib(N-3) Fib(N-4)
  • 23次 …
  • …次 …
  • …次 Fib(3) Fib(2) …
  • 2(N-1)次 Fib(2) Fib(1) … …

那么将次数相加:F(N)=20+21+22+…+2(N-1)

不难看出,这是一个等比数列,那么这时候使用错位相减法就可以很快的得到F(N)=2N-1

所以它的时间复杂度是O(2N)。

3.空间复杂度

  • 空间复杂度也是一个数学表达式,是对一个算法在运行过程中额外临时占用存储空间大小的量度

  • 空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法

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

  • 函数栈帧内容比较复杂,后续会写博客补坑。

实例1:

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;
   }
} 
  • 这个是冒泡排序。那么它的空间复杂度是多少呢?

  • 首先,我们需要明确的是空间复杂度的概念,这个很重要。空间复杂度也是一个数学表达式,是对一个算法在运行过程中额外临时占用存储空间大小的量度

  • 也就是说,我们为了解决这个切实的问题,算法在运行过程额外临时占用存储空间大小的量度

  • 拿冒泡排序来举例,首先这个函数是不是临时创建了一个数组,这个数组是不是额外开辟的空间?注意,这里这个临时开辟的数组不是额外开辟的空间,因为我们就是要解决这个数组的排序问题,问题就在数组里面,我们就是要解决这个数组,解决数组排序这个问题。所以它不是额外开辟的空间,是不算在空间复杂度里面的。

  • 那么这个冒泡排序里面什么是为了解决这个数组排序而额外开辟的临时空间呢?这里大家也要回顾一下概念:空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数

  • 所以就比如这个冒泡排序里面,exchange,end,i是为了解决这个问题而额外临时创建的变量,变量个数是3,所以这个冒泡排序的空间复杂度就是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+1)个空间。所以它的空间复杂度是O(n)。

  • 小总结一下:空间复杂度是比时间复杂度简单不少的,而且大多数算法的空间复杂度不是O(1)就是O(n)。而且注意理解好概念的字眼,就好比如开辟的函数形参空间是不算入空间复杂度的,因为是算法在运行过程额外临时占用存储空间大小的量度,注意是运行过程中,而形参是早就已经传递过来了的。

实例3:

long long Fac(size_t N)
{
   if(N == 0)
       return 1;
   
   return Fac(N-1)*N;
}

它的空间复杂度是多少呢?这个时候有点难和大家解释清楚。也是先拆解出来:

  • Fac(N) 1次调用

  • Fac(N-1) 1次调用

  • Fac(N-2) 1次调用

  • … 很多次调用

  • Fac(0) 1次调用

  • 先和大家说结果,结果是O(N)。为什么呢?主要是这么理解的,因为我们选择了这个递归算法,在这个递归算法这个方法方式下,我们要一次次的开辟新的函数栈帧,开辟一个函数栈帧就算一次,所以这个递归算法的空间复杂度是O(N)。如果我们没有选择这个递归算法呢,同一个问题,我们选择使用循环,它的空间复杂度就是O(1)了。

  • 这样说吧,就是这个问题摆在这里,你可以选择循环或者递归来解决。如果选择循环解决,那么它这个方法下的空间复杂度就是O(1)。但是,我们选择了递归算法,正是因为,我们选择了这个递归算法,它递归调用的时候需要一次次的额外的去开辟新的函数栈帧,所以我们说在递归这个方式方法下,它的空间复杂度是O(N)。

  • 同时需要注意的是,如果我们使用递归算法的时候要注意不要递归太深,如果递归的层次太深很容易造成栈溢出。

实例4:

long long Fib(size_t N)
{
   if(N < 3)
       return 1;
   
   return Fib(N-1) + Fib(N-2);
}

那么这个斐波那契数列的空间复杂度又是多少呢?先说结论,它的空间复杂度是O(N)。这里就涉及到了时间和空间的一个区别,以及递归的一个理解。同样的,我们先拆解开来分析:(复制了上面的时间复杂度来用一下)

  • 20次 Fib(N)

  • 21次 Fib(N-1) Fib(N-2)

  • 22次 Fib(N-2) Fib(N-3) Fib(N-3) Fib(N-4)

  • 23次 …

  • …次 …

  • …次 Fib(3) Fib(2) …

  • 2(N-1)Fib(2) Fib(1) … …

  • 首先它的时间复杂度是O(2N),而它的空间复杂度是O(N),为什么呢?很重要的一个东西,因为时间它是一去不复返的,是不能重复利用的,过去了就过去了。但是,空间是可以重复利用的!这就是本质上的原因。

  • 首先来看,递归函数是多次多用函数,累计起来,这个递归是调用了2N-1次函数。但是Fib(N)是同时调用了Fib(N-1)和Fib(N-2)吗?这里就涉及到了递归调用的一个顺序问题。这个函数递归是不会同时调用Fib(N-1)和Fib(N-2)的,而是先调用Fib(N-1),同样的,接着继续调用Fib(N-2)(注意:这里的Fib(N-2)是Fib(N-1)里面的),然后再继续调用Fib(N-3),直到最后去到Fib(2),这里就有N-1次调用函数了,然后再返回上一级Fib(3),然后再接着调用Fib(1),然后以同样的规律继续。

  • 大家结合图片理解一波:
    在这里插入图片描述

  • 但是在这里它开始返回之后,函数栈帧销毁,它是真的销毁了吗?名义上虽然是,但是我们不能单纯的去理解函数栈帧销毁这个概念,函数栈帧销毁的本质是将这块空间的使用权交回给系统,空间它依旧在那里,只是使用权被拿回去了,拿回去给操作系统了。这块空间还是能继续使用的。

  • 这就好比如酒店开房,我们开辟的函数栈帧就好比如我们去酒店开房,我们开房之后,房间的使用权是酒店给予我们的。当我们退房之后,我们房间的使用权就交还给酒店。房间就好比如空间,酒店就是操作系统。我们退房之后,酒店是可以继续将酒店的房间租别人使用,别人来住的。

  • 那么这个时候:这个函数递归是不会同时调用Fib(N-1)和Fib(N-2)的,而是先调用Fib(N-1),同样的,接着继续调用Fib(N-2)(注意:这里的Fib(N-2)是Fib(N-1)里面的),然后再继续调用Fib(N-3),直到最后去到Fib(2),这里就有N-1次调用函数了,然后再返回上一级Fib(3),然后再接着调用Fib(1),然后以同样的规律继续。

  • 这两个高亮的函数栈帧是使用的同一块空间。

  • 为了更好的让大家理解空间复用,大家看一下下面这张图:

在这里插入图片描述

  • 这张图中就很明显的说明了,空间复用的现象。(函数虽然名字不同,里面的参数的值也不同,但是a和b的地址确是一样的!)
  • 计算斐波那契数列的空间复杂度最主要的就是要理解好空间可以复用和函数递归的规律和方式。虽然递归函数Fib(N)里面的参数的值不同,但是逻辑是相同的,所以就复用了空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值