复杂度分析
同一个问题可以使用不同的算法解决,那么不同的算法孰优孰劣如何区分呢?因此我们需要一个表示方法来代表每个程序的效率。
衡量一个程序好坏的标准,一般是运行时间与占用内存两个指标。
不过我们在写代码的时候肯定无法去估量程序的执行时间,因为真实的执行时间受到多方面因素的影响,比如同样一段程序,放在高配服务器上跑和放在低配服务器上跑完全是两个表现效果,比如遍历一个数组的函数,执行时间完全取决于调用函数传入的数组大小。
如何在不运行程序的情况下,判断出代码的执行时间呢?显然是不可能的。
不过我们虽然无法预估代码的绝对执行时间,但是我们可以预估代码基本的执行次数。
一段代码的执行时间如果有变化,则一定是受到外部输入的数据所影响,我们将代码中所有不变的因素,表示为大O,将变化的因素作为基数n,表示为:O(n),大O的意思是忽略重要项以外的内容,我们常以这种大O表示法来判断比较各种算法的执行效率。
接下来我会介绍几种常用的复杂度表示方法。
PS:专业的解释必然全篇都是数学证明,未免太过于复杂,让学者更加迷茫,我这里写的并不是教材,而是最直白的理解。
时间复杂度
本节中例举的各种时间复杂度以好到差依次排序。
常数时间 O(1)
先看下这个函数:
private static void test(int n) { int a = 2; int b = 3; int c = a+b; System.out.println(c); }
一共4行代码,CPU要将a的值写入内存,b的值写入内存,a和b进行计算,将计算结果写入c,最后将c输出到控制台。
尽管计算机内部要做这么多事情,这段代码的时间复杂度依然是O(1),原因是这几行代码所做的操作是固定的,是不变的因素。
再看下这个函数:
private static void test(int n) { for (int i=0;i<100000;i++){ System.out.println(i); } }
循环10W次,可能你觉得功耗可能有点大,不过它的时间复杂度仍然是O(1)。
我们可以这么固定的认为:无论接收的参数怎么变化,只要代码执行次数是无变化的,则用1来表示。 凡是O(1)复杂度的代码,通常代表着它是效率最优的方案。
对数时间 O(log n)
普遍性的说法是复杂度减半,就像纸张对折。
示例代码:
private static void test(int n) { for (int j=1;j<=n;j=j*2){ System.out.println(j); } }
这段代码的执行效果并非是一次折半,它是次次折半,以2为底,不断的进行幂运算,实际上只要有幂指数关系的,不管你的底数是几,只要能够对原复杂度进行求幂逆运算我们都可以称之为O(log n)
比如:
private static void test(int n) { for (int j=1;j<=n;j=j*3){ System.out.println(j); } }
在忽略系数、常数、底数之后,最后都可以表示为O(log n),只不过我们遇到的算法几乎不会出现一些极端例外情况,对数时间的所在地常见以二分查找为代表。
线性时间 O(n)
我们将test方法稍稍修改一下:
private static void test(int n) { for (int i=0;i<n;i++){ System.out.println(i); } }
修改之后这次不是执行10W次,而是执行n次,n是由参数传入的一个未知值,在没有真实运行的时候我们无法判断这个n到底是多少?因为它可以是任意int型数字,你可以这么认为:在理想的情况下,它的复杂度是O(1),在恶劣的情况下,它的复杂度是无限大。完全取决于方法调用方。
直白的说,for循环就是循环n次,因此这段代码的时间复杂度为O(n),这种复杂度常常表现为线性查找。
线性对数时间 O(n log n)
线性对数时间也就是线性时间嵌套对数时间:
private static void t(int n){ for (int i=0;i<n;i++){ test(n); } } private static void test(int n) { for (int j=1;j<=n;j=j*2){ System.out.println(j); } }
t这个方法的时间复杂度就是O(n log n)。
平方时间 O(n^2)
平方时间就是执行程序需要的步骤数是输入参数的平方,最常见的是嵌套循环:
private static void test(int n) { for (int i=0;i<n;i++){ for (int j=n;j>0;j--){ System.out.println(j); } } }
其他时间
比O(n^2)还要慢的自然有立方级O(n^3)
比O(n^3)更慢慢的还有指数级O(2^n)
慢到运行一次程序要绕地球三百圈的有O(n!)
正常情况下我们不会接触到这些类型的算法。
空间复杂度
所谓空间,就是程序运行占用的内存空间,空间复杂度指的就是执行算法的空间成本。
这里我们抛一道题来做例子:在一个数组中找出有重复的值,如数组[3,8,13,7,15,8,6,6] 找出8和6。
解法:
private static void test(int[] arr) { for (int i=0;i<arr.length;i++){ for(int j=0;j<i;j++){ if(arr[j] == arr[i]){ System.out.println("找到了:"+arr[i]); } } } }
很显然:时间复杂度为O(n^2)。
那我们还可以使用一种更优的解法:
private static void test(int[] arr) { HashSet hashSet = new HashSet(); for (int i=0;i<arr.length;i++){ if(hashSet.contains(arr[i])){ System.out.println("找到了:"+arr[i]); } hashSet.add(arr[i]); } }
也许你会惊讶的发现,时间复杂度被优化成了O(n)。
虽然时间复杂度降低成了O(n),但是付出的代价是空间复杂度变成了O(n),因为新的解法使用了一个HashSet来存储数据,存储数据自然要占用内存空间,而占用的空间大小完全取决于传入数组大小。
我们之所以说第二种解法更优,其实是一种常规思想,因为现实中绝大部分情况,时间复杂度显然比空间复杂度更为重要,我们宁愿多分配一些存储空间作为代价,来提升程序的执行速度。
总而言之,比较两个算法优劣的指标有两个,时间复杂度与空间复杂度,优先比较时间复杂度,时间复杂度相同的情况下比较空间复杂度。
最后:感谢阅读。