前言:
不知道小伙伴们有木有刷算法的习惯,比如Leetcode,剑指Offer等等。如果没有的话,这里借用Pascal之父Niklaus Wirth教授说过的一句话:"程序=算法+数据结构",这句话让他获得了图灵奖。说明算法非常重要哦!希望你们能养成这种习惯。刷算法题的时候,我们肯定遇到过这种要求,例如:"这道题的要求是时间复杂度 O(N),空间复杂度 O(1)" 可能很多人对这句话不是很理解,今天我会详细介绍一下时间复杂度,空间复杂度我也会稍微提一下,我们知道算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。那我们如何去衡量算法的优劣呢,那当然是看该算法消耗的资源和时间啊!这个时候时间复杂度和空间复杂度便诞生了。
一,时间复杂度:
我们先来看一组图
这个时间复杂度到底是个什么鬼,咱们接着看。。。
一天之后,小灰和大黄各自提交了代码,它们实现的功能都差不多。大黄的代码运行一次要花10毫秒,内存占用1MB。小灰的代码运行一次要花10秒,内存占用10MB。接下来。。。
综上所述,衡量一个算法好坏,就是算法运行所需时间及占用的内存。
关于代码的基本操作执行次数,我们用四个生活中的场景,来做一下比喻:
场景1:给小灰一条长10寸的面包,小灰每3天吃掉1寸,那么吃掉整个面包需要几天?
答案自然是 3 X 10 = 30天。
如果面包的长度是 N 寸呢?
此时吃掉整个面包,需要 3 X n = 3n 天。
如果用一个函数来表达这个相对时间,可以记作 T(n) = 3n。
场景2:给小灰一条长16寸的面包,小灰每5天吃掉面包剩余长度的一半,第一次吃掉8寸,第二次吃掉4寸,第三次吃掉2寸......那么小灰把面包吃得只剩下1寸,需要多少天呢?
这个问题翻译一下,就是数字16不断地除以2,除几次以后的结果等于1?这里要涉及到数学当中的对数,以2位底,16的对数,可以简写为log16。
因此,把面包吃得只剩下1寸,需要 5 X log16 = 5 X 4 = 20 天。
如果面包的长度是 N 寸呢?
需要 5 X logn = 5logn天,记作 T(n) = 5logn。
场景3:给小灰一条长10寸的面包和一个鸡腿,小灰每2天吃掉一个鸡腿。那么小灰吃掉整个鸡腿需要多少天呢?
答案自然是2天。因为只说是吃掉鸡腿,和10寸的面包没有关系 。
如果面包的长度是 N 寸呢?
无论面包有多长,吃掉鸡腿的时间仍然是2天,记作 T(n) = 2。
场景4:给小灰一条长10寸的面包,小灰吃掉第一个一寸需要1天时间,吃掉第二个一寸需要2天时间,吃掉第三个一寸需要3天时间.....每多吃一寸,所花的时间也多一天。那么小灰吃掉整个面包需要多少天呢?
答案是从1累加到10的总和,也就是55天。
如果面包的长度是 N 寸呢?
此时吃掉整个面包,需要 1+2+3+......+ n-1 + n = (1+n)*n/2 = 0.5n^2 + 0.5n。
记作 T(n) = 0.5n^2 + 0.5n。
上面场景所讲的是吃东西所花费的相对时间,这一思想同样适用于对程序基本操作执行次数的统计。刚才的四个场景,分别对应了程序中如下最常见的四种执行方式:
1.T(n) = 3n。执行次数是线性的:
void eat1(int n){
for(int i=0; i<n; i++){;
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("吃一寸面包");
}
}
2.T(n) = 5logn。执行次数是对数的:
void eat2(int n){
for(int i=1; i<n; i*=2){
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("吃一半面包");
}
}
3.T(n) = 2。执行次数是常量的:
void eat3(int n){
System.out.println("等待一天");
System.out.println("吃一个鸡腿");
}
4.T(n) = 0.5n^2 + 0.5n。执行次数是一多项式:
void eat4(int n){
for(int i=0; i<n; i++){
for(int j=0; j<i; j++){
System.out.println("等待一天");
}
System.out.println("吃一寸面包");
}
}
这个时候小伙伴肯定会问了,比如算法A的相对时间是T(n)= 100n,算法B的相对时间是T(n)= 5n^2,这两个到底谁的运行时间更长一些?这就要看n的取值了。这时候有了渐进时间复杂度(asymptotic time complexity)的概念,官方的定义如下:
若存在函数 f(n),使得当n趋近于无穷大时,T(n)/ f(n)的极限值为不等于零的常数,则称 f(n)是T(n)的同数量级函数。
记作 T(n)= O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
渐进时间复杂度用大写O来表示,所以也被称为大O表示法。
有如下几个原则来推导时间复杂度:
- 如果运行时间是常数量级,用常数1表示;
- 只保留时间函数中的最高阶项;
- 如果最高阶项存在,则省去最高阶项前面的系数。
所以我们刚才的四个场景时间复杂度就是:
- O(n)
- O(logn)
- O(1)
- O(n^2)
这四种时间复杂度究竟谁用时更长,谁节省时间呢?稍微思考一下就可以得出结论:
O(1)< O(logn)< O(n)< O(n^2)
在编程的世界中有着各种各样的算法,除了上述的四个场景,还有许多不同形式的时间复杂度,比如:
O(nlogn), O(n^3), O(m*n),O(2^n),O(n!)
今后遨游在代码的海洋里,我们会陆续遇到上述时间复杂度的算法。
我们来举过一个栗子:
算法A的相对时间规模是T(n)= 100n,时间复杂度是O(n)
算法B的相对时间规模是T(n)= 5n^2,时间复杂度是O(n^2)
算法A运行在小灰家里的老旧电脑上,算法B运行在某台超级计算机上,运行速度是老旧电脑的100倍。
那么,随着输入规模 n 的增长,两种算法谁运行更快呢?
我前面说到O(n)用时是要小于O(n^2),当n很小的时候,算法A甚至要比B用时更长,但是当n越来越大,算法B就越来越慢,这还是算法A加强了100倍的情况下,所以时间复杂度是非常能够体现算法的优劣的。
二,空间复杂度:
这不是本文重点,当是我还是稍微提一下。空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。空间复杂度比较常用的有:O(1)、O(n)我们下面来看看:
1.O(1):和时间复杂度O(1)有异曲同工之妙
int i = 1;
int j = 2;
int m = i + j;
就是说代码中i,j,m所占用的空间不会受执行次数n影响,始终是一个常量,所以空间复杂度为S(n)=O(1)
2.O(n):
int[] m = new int[n]
for(i=1; i<=m.length; ++i)
{
j = i;
j++;
}
这段代码中所占用空间基本就是m数组的空间,由于长度为n,所以空间复杂度就是S(n)=O(n)。
结语:
以上就是我本文的主体内容,通过时间复杂度和空间复杂度的优劣我们知道了算法的好坏,从而可以知道你这个程序性能如何,所以作为一个程序猿,我们每写下一段代码,都要思考,这到底是不是最好的做法,一旦你养成这种习惯,你的IT之路将会一片光明。