引入:算法的好与坏
衡量算法的好与坏有很多量度,这之中最重要的两个量度就是时间复杂度和空间复杂度。
那么,何为时间复杂度和空间复杂度呢?让我们来看以下一个场景。
小陈和小宇来小米公司应聘,雷总给他们布置了一个项目,要求他们用代码实现出来。
过了几天他们都实现了需求,功能都差不多。
在上述场景中,小宇虽然也按照雷总的要求实现功能,但他的代码存在两个严重的问题。
1.运行时间长
运行小陈的代码只要100ms,而运行小宇的代码需要100s,使用者肯定是无法忍受的。
2.占用空间大
小陈的代码只消耗5MB内存,而小宇的代码却需要消耗500MB内存,这会给使用者造成很多麻烦。
由此可见:运行时间长短与占用空间大小与程序的好坏密切相关。
渐进时间复杂度
若要分析代码的时间复杂度,我们先来引入基本操作执行次数这个概念。
比如现在面前有一个鸡腿,你需要三口才能吃完。我们不妨设n为吃的口数,那么有T(n)=3-n
当n为三时,恰好吃完。由此:对于程序基本操作次数,有函数T(n)表示,n为输入规模。
但有了T(n)之后,若要比较代码的运行时间还是有困难的。
例如算法S的执行次数时T(n)=10n²,算法Z的执行次数是T(n)=100n,这两个的运行时间还需要看n的取值。因此为了解决这一类问题,有了渐近时间复杂度这一概念。如下:
我们通过计算得出一个算法的运行时间 T(n), 与T(n)同数量级的即幂次最高的O(F(n))即为这个算法的时间复杂度。例如:某算法的运行时间T(n) = n+10与n是同阶的(同数量级的),所以称T(n)=O(n)为该算法的时间复杂度。
直白的讲:时间复杂度就是把程序的相对执行时间T(n)简化成一个数量级,可以是n,n²等。
如何推导出时间复杂度呢?有这几个原则:
如果运行的时间是常数量级,则用常数1表示。 eg.7--1.
只保留函数中的最高阶项。eg.n²+n--n²
如果最高阶项存在,则省去最高阶项前的系数。eg.8n--n.
在以后我们会学习到各种各样的时间复杂度如:1,lgn,n,n²,n!...
若要比较哪个更节省时间,可以通过函数图像,当n取值足够大时,不难得出:
O(1) < O(lgn) < O(n) < O(n²) < O(n!),还有更多如下:
空间复杂度
什么是空间复杂度?
在运行一段程序时,我们不仅要执行各种运算指令,同时也会根据需要,存储一些中间数据,以便后续指令可以更方便地继续执行。
但是,内存的空间有限,在时间复杂度相同的情况下,程序占用的空间自然是越小越好。如何描述占用空间的大小呢,这便用到了空间复杂度。
和时间复杂度类似,空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,同样使用了大O表示法。记作S(n)=O(f(n)),其中n为问题的规模,f(n)为算法所占存储空间的函数。
空间复杂度的计算
和时间复杂度类似,空间复杂度也有几种不同的增长趋势如:
1.常量空间:当算法的存储空间大小固定,和输入的规模没有直接联系时,空间复杂度记为O(1).
void main(int n){ int var = 3; ... }
2.线性空间:当算法分配的空间是一个线性的集合(如数组),并且集合大小和输入规模n成正比时,空间大小记为O(n).
void fun2(int n){ int[] array = new int[n]; ... }
3.二维空间:当算法分配的空间是一个二维数组的组合,并且集合的长度和宽度都与输入规模n成正比时,空间复杂度记为O(n²).
void fun3(int n){ int[][] matrix = new int[n][n]; ... }
4.递归空间:递归比较特殊,它显然没有明显地声明变量与集合,但是计算机执行时,会专门分配一片内存,用来存储"方法调用栈"。
“方法调用栈”包括入栈和出栈这两个行为。
当进入一个新方法时,执行入栈,把调用的方法和参数压入栈中。
当方法返回时,执行出栈,把调用的方法和参数信息从栈中弹出。
下面有一个递归程序:
void fun4(int n){ if(n <= 1) return; fun4(n-1); ... }
假如传入初始值n=3,那么方法fun4(参数n=3)的信息先入栈。
接下来递归用相同的方法(参数n=2)的信息调用入栈。
以此类推,递归越来越深,入栈元素越多。到n=1时,满足条件,方法出栈。
最终,“方法调用栈”全部元素都会出栈。
由此,执行递归操作所需要的内存空间和递归深度成正比。纯粹递归操作的空间复杂度也是线性的,如果递归深度是n,那么空间复杂度就是O(n).
好了,本次就讲到这里,希望你有所收获,如果你喜欢的话还请支
持哦。