浅淡数据结构时间复杂度和空间复杂度

前言

初学数据结构一般最早接触到的概念就是时间复杂度和空间复杂度。这两个标准可以用来衡量一个算法的好坏。学会分析某个算法或程序的时间复杂度和空间复杂度是必需掌握的技能。本文将围绕这两个概念进行相关介绍。


1.时间复杂度和空间复杂度的相关介绍

1.为什么要引入时间复杂度和空间复杂度的概念

如何衡量一个算法的好坏呢?是代码越少越好吗?算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。引入这两个概念后可以按照这两个标准分析出某个算法优劣程度。同时还能根据这种标准来优化改进算法。这相当于一种评判标准。这种标准分析起来比较简单也比较公正。不用把代码放在机器上实际评测,同时哪怕同一段代码放在不同效率的机器上跑,执行速度肯定会有所差异。因此就引入了时间复杂度和空间复杂度的概念。

2.什么是时间复杂度和空间复杂度

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

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

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。 推导大O阶方法:1、用常数1取代运行时间中的所有加法常数。2、在修改后的运行次数函数中,只保留最高阶项。3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

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

2.具体示例分析

1.大O法只保留高阶项

分析如下代码求出时间复杂度

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

简单分析可得这个程序执行次数是N * N+2*N+10,这就找到基本语句与问题规模N之间的数学表达式。根据大O法的规则,只保留最高项同时舍去系数,O(N^2),其实我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。随着N的增大,除了N ^2外,其余项对整个结果的影响并不是很大。这段代码的空间复杂度也很好算,除了变量所占据空间外,并没有占据额外的空间,所以空间复杂度就是O(1)。

2.一般情况关注的是算法的最坏运行情况

分析如下代码求出时间空间复杂度

int BinarySearch(int* a, int n, int x)
{
 assert(a);
 int begin = 0;
 int end = n-1;
 while (begin < end)
 {
 int mid = begin + ((end-begin)>>1);
 if (a[mid] < x)
 begin = mid+1;
 else if (a[mid] > x)
 end = mid;
 else
 return mid;
 }
 return -1;
}

不难看出上述代码实际上是二分查找。二分查找是不断缩小范围进行查找,二分查找的查找情况会分为最好和最坏的情况。在目标元素存在的前提下,可能一次就找到了。这是最好的情况。当目标元素的位置处于边界位置时或者不存在时,需要多次查找,这是最坏情况下的查找的。那么这个算法的时间复杂度到底怎么算呢?当一个算法分为最好运行情况或者最坏运行情况时,实际中一般情况关注的是算法的最坏运行情况。那么这个算法的时间复杂度应该是多少呢?
在这里插入图片描述
假设有序数组长度为N,每次查找都会缩小一半的区间,也就是相当于数组长度缩短了一半查找,直到范围区间缩小至1。这相当于整个数组都查找完毕。如果查找次数为x 那么就有1 * 2 * 2 * 2…=N,也就是2^x=N,那么x=log2N,log以2为底N的对数,但是在算法分析中一般都是写成O(logN) ,和数学上表达是不一样的。这段代码中的空间上的消耗都是常数大小,所以空间复杂度是O(1)。

3.分析递归函数的时间空间复杂度

分析以下代码求出时间空间复杂度

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

不难看出以上的代码是求斐波那且数的递归写法。那么如何分析这段代码的空间复杂度呢?递归时间复杂度是等于递归的次数乘上每次递归调用执行次数。
在这里插入图片描述

递归次数的有了,接着分析每次递归执行次数。因为函数体内只用判断一次是否小于3,相当于执行了一次,也就是常数次。由此可得时间复杂度就是O(2^n)。那么空间复杂度如何分析呢?记住一句话空间是可以重复利用的但是时间是不能重复利用的。虽然递归次数很多但是要计算f(n)势必会计算f(n-1),计算f(n-1)势必会计算f(n-2)依此类推。函数每次调用都会创建见函数栈帧。为f(1)到f(n)创建的函数栈帧是会被重复利用的,直到每次函数调用结束,为之创建的函数栈帧空间也随之销毁。因为这段代码中的递归是有很多次重复调用的,也就是重复多次调用参数相同的函数,很多次递归计算实际上是在同一空间内计算的,递归的时间复杂度实际上看的是递归的深度。从f(n)到f(1)所消耗的空间也就是n,因为每次消耗常数大小的空间,为f(n)到f(1)创建n次空间。所以时间复杂度O(n)。


在分析一段代码求时间空间复杂度

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

这段代码是用来求n的阶层的递归调用。有了先前的递归分析经验,这样的代码分析起来也是很简单的。先前说了递归调用时间复杂度可以看作是递归次数乘以每次递归函数内部执行次数。因为计算f(n)会调用f(n-1),计算f(n-1)会调用f(n-2),计算f(n-2)会调用f(n-3),依此类推直到调用f(1),从f(1)到f(n)调用了n次,每次调用只判断n是否为1,所以相当于每次调用函数内部执行1次,也就是常数次。所以时间复杂度为O(n)。因为每次调用函数是创建的空间大小常数大小,调用n次函数,也就是创建了n次空间,所以实际上空间复杂度是O(n)。

3.总结

  • 1.对于分析时间复杂度的时候不能只是简单的看循环层数,循环一层就是O(n)循环两层就是O(n^2)这种判断方式太武断了。要结合算法的思想一步分析,最后得出结果。二分查找就是一层循环,但是时间复杂度就不是O(n)。
  • 2.当某种算法分最坏和最好执行状况时,以最坏的的状况来分析这个算法的时间复杂度。
  • 3.学会分析时间空间复杂度,是学习数据结构的重要一步。关于时间复杂度和空间复杂度简单的介绍完毕。以上的内容如有错误,欢迎指正!
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇

python发展前景
)">
下一篇>>