文章目录
- 时间空间复杂度
- 1. 时间空间复杂度的重要性(作用)
- 2. 时间复杂度和大O表示法
- 1)算法图解
- 2)代码随想录
- 3)王道《数据结构》
- 3. 大O指的是最糟的情形和一般的情形
- 1)大O表示的是一般情况,并不是严格的上界
- 2)王道中对三个维度的时间复杂度的解释:
- 4. 常见的大O运行时间及化简规则
- 1)常见渐进复杂度排序:
- 2)分析程序复杂性,化简规则:
- 3)大O可以省略常数项系数的原因
- 4) 复杂度化简的例子
- 5. 空间复杂度
- 1)代码随想录
- 2)王道数据结构
- 6. O(logn)的解读
算法是一组完成任务的指令。任何代码片段都可视为算法。——《算法图解》
特别说明:文章是整理的笔记加自己汇集的资料及理解,参考自代码随想录、王道《数据结构》、《算法图解》(最近跟着carl哥准备刷题了,会坚持更新,争取早日养成输出的习惯)
时间空间复杂度
1. 时间空间复杂度的重要性(作用)
即使我们知道CPU运行一个程序所需的时间不只是算法执行的时间,比如
- CPU执行每条指令所需的时间实际上并不相同,例如CPU执行加法和乘法操作的耗时实际上都是不一样的。
- 现在大多计算机系统的内存管理都有缓存技术,所以频繁访问相同地址的数据和访问不相邻元素所需的时间也是不同的。
- 计算机同时运行多个程序,每个程序里还有不同的进程线程在抢占资源。
尽管影响的因素甚多,但至少我们对自己写的程序的运行时间有个大体的评估。总不能写个查询人都反应过来了,程序还在执行中吧。
算法4中提到:
- 火箭科学家需要大致知道一枚试射火箭的着陆点是在大海里还是在城市中;
- 医学研究者需要知道一次药物测试是会杀死还是会治愈实验对象;
所以任何开发计算机程序员的软件工程师都应该能够估计这个程序的运行时间是一秒钟还是一年。
事实上抛开电脑的硬件配置,评估我们写的每一段程序好与差,最重要的还是时间和空间复杂度。 引用王道《数据结构》中的一句话,算法效率的度量是通过时间复杂度和空间复杂度来描述的。
2. 时间复杂度和大O表示法
先上结论,后边依次给出三个地方对时间复杂度的理解
- 算法的速度指的并非时间,而是操作数的增速
- 当讨论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加
- 算法的运行时间用大O表示法表示
1)算法图解
最喜欢《算法图解》中给出的解释:大O表示法指出算法的速度有多快,即大O表示法让你能够比较操作数,指出了算法运行时间的增速。
即大O解决两个问题:一是估计当前问题规模n下的运行时间,二是估计出随问题规模n的增大,算法运行时间的增速的变化。
举一个书中的列子:Bob为NASA编写一个查找算法,此算法在火箭即将登陆月球前执行,帮助计算登陆地点,时间需10秒内(简单算法和二分查找中选一个)。
测试时列表中有100个元素(假设查一个元素需1毫秒,默认有序),简单查找需100毫秒,二分查找需7毫秒。而实际中了包含10亿个元素,10亿元素查询时二分查找运行时间30毫秒(log2(1000000000)约为30)。
但Bob根据100个元素得出的结论,二分查找的速度约为简单查找的15倍,10亿个元素简单查找为30*15=450毫秒,同时简单查找编写简单bug率低,Bob毅然选择简单查询。
实际上,10亿量级时最坏时间复杂度需10亿毫秒(整整11天啊!)。
Bob会简单的认为二分查找的速度约为简单查找的15倍本质原因:评价算法时只用了运行时间来衡量,这是没有意义的。
同一个问题,用同一种算法,由于问题规模的不同,首先时间是不同的,重要的是时间的增长速度也不同。所以评价算法的快慢用时间衡量没有意义,选择将算法需要执行的操作次数表示为问题规模n的函数,既可以通过输入规模n得出运行时间,也可以通过分析函数表达式本身的性质发现运行时间增速的变化。
贴出简单查询和二分法的时间复杂度函数
2)代码随想录
时间复杂度是一个函数,它定性描述该算法的运行时间。
如何估算算法的运行时间?
通常估算算法的操作单元数量来代表程序消耗的时间(默认CPU的每个单元运行消耗的时间都相同)。
假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示,随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 O(f(n))
3)王道《数据结构》
一个语句的频度是指该语句在算法中被重复执行的次数。而算法中所有语句的频度之和记作T(n),即该算法问题的规模n的函数。
时间复杂度是分析T(n)函数的数量级,算法中的基本运算(最深层循环内的语句)的频度f(n)与T(n)同数量级,所以算法的时间复杂度记为:T(n) = O(f(n))(‘O’的含义是T(n)的数量级)
3. 大O指的是最糟的情形和一般的情形
1)大O表示的是一般情况,并不是严格的上界
算法导论给出的解释:大O用来表示上界的,当用它作为算法的最坏情况运行时间的上界,就是对任意数据输入的运行时间的上界。
算法图解中也有提到:大O说的是最糟的情形。
用算法导论中给出的例子:插入排序的时间复杂度我们通常都说是O(n^2) 。
输入数据的形式对程序运算时间是有很大影响的,在数据本来有序的情况下时间复杂度是O(n),但如果数据是逆序的话,插入排序的时间复杂度就是O(n^2) ,也就对于所有输入情况来说,最坏是O(n^2) 的时间复杂度,所以称插入排序的时间复杂度为O(n^2)。
同样的同理再看一下快速排序,都知道快速排序是O(nlogn),但是当数据已经有序情况下,快速排序的时间复杂度是O(n^2) 的。
所以严格从大O的定义来讲,快速排序的时间复杂度应该是O(n^2)。但是我们依然说快速排序是O(nlogn)的时间复杂度。
这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界。
2)王道中对三个维度的时间复杂度的解释:
最坏时间复杂度: 指最坏情况下,算法的时间复杂度
平均时间复杂度 指所有可能输入实例在等概率出现的情况下,算法的期望运行时间。
最好时间复杂度 指在最好情况下,算法的时间复杂度。
4. 常见的大O运行时间及化简规则
- O(log2(n)): 也叫对数时间,算法包括二分查找。
- O(n): 也叫线性时间,算法包括简单查找。
- O(n*logn): 算法包括快速排序,速度比较快的排序。
- O(n^2): 算法包括选择排序,速度比较慢的排序。
- O(n!): 算法包括旅行商问题,一种非常慢的算法。
图片来自算法图解,方便理解增速
1)常见渐进复杂度排序:
O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(nlogn)线性对数阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(n!)n的阶乘 < O(2^n)指数阶
2)分析程序复杂性,化简规则:
1)加法规则
T(n) = T1(n) + T2(n) =O(f(n)) + O(g(n)) =O(max(f(n),g(n)))
2)乘法规则
T(n) = T1(n) * T2(n) = O(f(n)) * O(g(n)) = O(f(n) * g(n))
*注:化简例子在下一个大标题下
3)大O可以省略常数项系数的原因
附上代码随想录中的一个举例,如图是不同算法的时间复杂度在不同数据输入规模下的差异。
通常要决定使用哪种算法时,并不仅仅只考虑时间复杂度,也需要考虑数据规模。
如果数据规模很小甚至可以用O(n^2)的算法比O(n)的更合适(在有常数项的时候)。
比如上图中 O(5n^2) 和 O(100n) 在n为20之前很明显 O(5n^2)是更优的,所花费的时间也是最少的。
但O(100n) 就是O(n)的时间复杂度,O(5n^2) 就是O(n^2)的时间复杂度,为什么要默认O(n) 优于O(n^2) 呢?
这是因为大O就是数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度,这个数据量也就是常数项系数已经不起决定性作用的数据量。
例如上图中20就是那个点,n只要大于20 常数项系数已经不起决定性作用了。
所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大, 基于这样的事实,给出的算法时间复杂的的一个排行如下所示:
O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(nlogn)线性对数阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(n!)n的阶乘 < O(2^n)指数阶
但是也要注意大常数,如果这个常数非常大,例如10^7 ,10^9 ,那么常数就是不得不考虑的因素了。
4) 复杂度化简的例子
示例:O(2n^2 + 10n + 1000)
1)去掉运行时间中的加法常数项 (因为常数项并不会因为n的增大而增加计算机的操作次数)
O(2n^2 + 10n)
2)去掉常数系数(上文中已经详细讲过为什么可以去掉常数项的原因)
O(n^2 + n)
3)提取n的操作,变成O(n(n+1)) ,省略加法常数项后也就别变成了:
O(n^2)
5. 空间复杂度
1)代码随想录
定义: 空间复杂度(Space Complexity)记作S(n) 依然使用大O来表示,是对一个算法在运行过程中占用内存空间大小的量度,记做 S(n)=O(f(n))。
作用: 利用程序的空间复杂度,可以对程序运行中需要多少内存有个预先估计。
注意: 空间复杂度只是预先大体评估程序内存使用的大小,不能准确评估。毕竟很多因素会影响程序真正内存使用大小,例如编译器的内存对齐,编程语言容器的底层实现等等这些都会影响到程序内存的开销
空间复杂度O(1): 随n的变化,所需开辟的内存空间并不会随n的变化而变化。即此算法空间复杂度为一个常量,所以表示为O(1)。在王道数据结构中称此为算法原地工作(即指所需辅助空间是常量)
int j = 0;
for (int i = 0; i < n; i++) {
j++;
}
空间复杂度为O(n): 当消耗空间和输入参数n保持线性增长,这样的空间复杂度为O(n)
int[] a = new int[n] //随着n的增大,开辟的内存大小呈线性增长,即 O(n)
for (int i = 0; i < n; i++) {
a[i] = i;
}
2)王道数据结构
算法的空间复杂度S(n),定义为该算法所耗费的存储空间,它是问题规模n的函数。
渐进空间复杂度简称为空间复杂度。记作S(n) = O(g(n))。这里的g(n) 同代码随想录在空间复杂度给出的f(n)一样,都为程序语句关于n所占存储空间的函数。
6. O(logn)的解读
通常我们所说的算法的时间复杂度是logn不一定只是log 以2为底n的对数,可以是以10为底n的对数,也可以是以20为底n的对数,但我们统一说 logn,也就是忽略底数的描述。
如下图解释(图片截与代码随想录):
假如有两个算法的时间复杂度,分别是log以2为底n的对数和log以10为底n的对数,因为以2为底n的对数 = 以2为底10的对数 * 以10为底n的对数。(下方贴出了推到过程)
而以2为底10的对数是一个常数,在上文已经讲述了我们计算时间复杂度是忽略常数项系数的。
抽象一下就是在时间复杂度的计算过程中,log以i为底n的对数等于log 以j为底n的对数,所以忽略了i,直接说是logn。
这就是为什么时间复杂度logn可以忽略底数描述了。
补充解释第二个对数公式成立推导: