为什么要先聊这个算法复杂度呢?
首先,我们先聊聊算法。“老衲”经常听别人说起算法,但还是不太明白算法是什么。
先拽一大段概念:
算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。
是不是看不懂,不想看?让我用白话文给你道道:
算法,在日常生活中也是随处可见的。比如“老衲”要去上学,怎么去上学?
- 做飞机,机票贵(引申到程序上就是耗费空间),但是时间快呀
- 坐火车,火车票便宜,但老衲肯定要好久才能到学校,时间又耗费了很多
其实,上面的两种策略就是算法。也就是为了达到“去学校”这一目的,根据自身情况(比如我钱多,我就是要最快到;比如我钱紧张,时间还是很充裕)来选择不同的方式去上学。
同理,算法也有空间复杂度(引申到现实生活就是耗费的价格)和时间复杂度两个标准来衡量它们的好坏。
大O表示法
声明一下,由于个人能力有限,对于时间复杂度和空间复杂度的表示,掌握到了程序的大致表示,如果对复杂度的计算有浓厚的兴趣,可以去看看《c++数据结构与算法》第二章,讲得挺全面!
什么是大O表示法?
come on! 接着来聊,什么是大O表示法呢?
由于程序运行所需的时间往往会受到机器性能与其他因素影响,用绝对的时间单位衡量算法的效率并不合适。在讨论算法复杂度的时候,我们一般关注它的近似值(渐进趋势),即渐进复杂度,常用大O表示法表示。
O(1) | 常数时间复杂度 |
O(log n) | 对数时间复杂度 |
O(n) | 线性时间复杂度 |
O(n^2) | 平方 |
O(n^3) | 立方 |
O(2^n) | 指数 |
O(n!) | 阶乘 |
可见,几种时间复杂度的增长速度顺序为:O(1) < O(log n) < O(n) < O(n ^ 2) < O(n ^ 3) < O(2 ^ n) < O(n!)
n是什么
n就是算法的输入规模,就是输入数字的数量。
明白了n是什么,就可以大概解释一下7个表达式的含义了。以O(n^2)为例,它表示当输入规模为n时,时间复杂度接近于n^2。
悟出门道了吗?"老衲"说没有。没关系,接下来我将结合代码来解释一下这几种常用时间复杂度的意思。( 如果对程序比较陌生,可以先点开超链接看看我对这块的理解 程序是什么?)
O(1) 常数复杂度
int n = 1000;
System.out.println("Hey - your input is: " + n);
上面这段代码的时间复杂度就是常数级别的,没有循环,一条语句。
int n = 1000;
System.out.println("Hey - your input is: " + n);
System.out.println("Hmm.. I'm doing morestuff with: " + n);
System.out.println("And more: " + n);
这段代码的时间复杂度也是O(1)。
O(log n) 对数复杂度
log n即为log₂n,这里省略了对数的底数2。“老衲”学过高数,对log n一定不会陌生了。
下面这段代码的时间复杂度即为O(log n)。
for(int i = 1; i < n; i = i * 2)
{
System.out.println("Hey - I'm busy looking at: " + i);
}
留给“老衲”的思考题:思考一下为什么是 i * 2?
O(n) 线性复杂度
顾名思义,从上面那张图中可以看出函数图像为一条直线。
下面这段代码时间复杂度即为线性阶。单重循环遍历1~n并打印出来,共循环了n次。
for(int i = 1; i <= n; i ++)
{
System.out.println("Hey - I'm busy looking at: " + i);
}
O(n^2) 平方
明白了n次单重循环时间复杂度为O(n),就不难理解下面的双重循环嵌套,共运行n*n次,即时间复杂度为O(n^2)了吧。
for (int i = 1; i <= n; i ++)
{
for (int j = 1; j <= n; j ++)
{
System.out.println("Hey - I'm busy looking at: " + i + " and " + j);
}
}
O(n^3) 立方
以此类推,聪明的“老衲”一定能猜到O(n^3)时间复杂度的程序该怎么写。
O(2^n) 指数
下面代码的时间复杂度就是O(2^n)。
for (int i = 1; i <= Math.pow(2, n); i++){
System.out.println("Hey - I'm busy looking at: " + i);
}
O(n!) 阶乘
public static int fibonacci(int n)
{ // 这个方法的功能是递归求第n个斐波那契数
if(n == 1 || n == 2)
return 1;
else
return fibonacci(n - 1) + fibonacci(n - 2);
}
public static void main(String[] args)
{
for(int i = 0; i < fibonacci(6); i ++) // 这里的for循环对时间复杂度影响不大
System.out.println("Hey - I'm busy looking at: " + i);
}
O(n!)复杂度常出现在递归的算法中。上面的代码功能为递归求解第n个斐波那契数,时间复杂度为O(n!)。
递归没有理解没关系,这里简单介绍一下,如果想要深入理解递归请点击超链接查看我的另一篇博客:(此处应有超链接)
先来看看上面代码中用到的公式:
fibonacci(n) = fibonacci(n-1) + fibonacci(n-2) // 这是斐波那契的一种写法
代入一个数,比如6,画出递归树看一下这段代码是如何运行的:
不难看出来,运行这小小的一句代码时间成本巨大,只是一个小小的6,就让计算机运行了6!次,显然是不划算的。
在后面的算法文章里面,会细致地讲讲如何通过不同的方式来降低算法复杂度!
如何计算算法的渐进复杂度?
当我们得到算法的运行次数后,可以通过以下三步求出大O表示法表示的渐进时间复杂度:
- 用常数1取代运行时间中左右加法常数
- 在修改后的运行次数函数中只保留最高阶项
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数
举几个例子:
执行次数 | 阶 | 非正式术语 |
12 | O(1) | 常数阶 |
2n + 3 | O(n) | 线性阶 |
3n^2+2n+1 | O(n^2) | 平方阶 |
O(log n) | 对数阶 | |
O(n^3) | 立方阶 | |
2^n | O(2^n) | 指数阶 |
递归 | O(n!) | 阶乘阶 |
结束语
明白了7个大O表示法,可以返回去看看折线图,它反映了当n为不同值时,不同渐进时间复杂度的增长趋势!
“老衲”, 你理解了吗?