问题一:我们如何判断一个算法的优劣?
答:在不考虑额外空间开销的情况小,对于同一个问题,直观而言,解决这个问题的算法需要的时间越少,我们就可以认为这个算法的效率越高。
就比如:如果我们需要在有序数组array中查找一个数key,返回这个key在数组中的下标,(我们假设这个key是在数组中一定存在的)。我们第一个反应是遍历整个数组。
for(int i=0;i<array.length;i++){
if(array[i]==key)
return i;
}
暴力求解,如此的简单,so easy.
但是如果我们这个数组里如果有一亿个数,那么在最坏情况下(比如这个数字在数组的末尾),我们需要遍历完整个数组才能找到这个数,那么我们需要的时间是非常大的,所以这个这个算法在数据量大的情况下显得不那么的友好。
所以我们通常不那么做,而是使用二分查找。(每次查找范围都缩小一半,知道找到这个数为止。)
public static int rank(int [] array,int key)
{//前提:数组有序
int low=0;
int height=array.length-1;
while(low<=height)
{
int mid=low+(height-low)/2 //数组的中间索引。
if(key<array[mid]) { height=mid-1;}
if(key>array[mid]) {low=mid+1;}
else return mid;
}
}
那么在最坏情况下,我们在这个数组中需要找打这个数,我们来计算一下查找的次数。第一次N/2,第二次N/2/2,第三次N/2/2/2
易得第m次N/2^m,所以找到这个数的最坏情况为(2^m=n,则m=log2(N))。
long(2N)的查找次数远远小于N。所以对于统一个问题,好的算法和坏的算法效率天壤之别。
我们既然知道,算法运行的快慢(时间的长短)是作为判断算法优劣的之一(今天只讲时间复杂度,还要考虑到空间复杂度),那么如何推广到一般性,即我们如何判断算法的执行时间呢?
问题二:如果我们需要知道一个算法执行的时间,我们应该怎么做。
解决一:直接用时间函数来统计,它根据计算机的不同,时间会不一样,但是我们如果想知道时间,每次都要实际操作才行,这样是不太现实的,有什么方法可以直观的得出一个算法的效率呢?直觉告诉我们它和问题的规模有关。
解决二,我们直接用数学模型来判断,我们根据执行语句执行的次数,来得出一个函数,这个函数就是问题的规模。这个问题的规模取决于最频繁的执行语句。显然算法运行时间是根据执行最频繁的语句的时间来决定的。
举个例子:
Int count=0;
For(int i=0;i<N;i++)
count++;
这个算法(姑且称之为算法吧),for循环这条语句执行的次数为N。问题的规模也为N。
所以这个算法执行的时间取决于N。
例子二:三数之和:
public static int sum(int [] a)
{统计数组中和为0的三元组的数量。
int count=0;
int N=a.length;
for(int i=0;i<N;i++)
for(int j=i+1;j<N;j++)
for(int k=j+1;k<N;k++)
if(a[i]+a[j]+a[k]==0)
count++;
return count;
}
在这个嵌套的三层循环中,我们可以看到,If语句是执行最频繁的那条语句,我们选取它作为这个算法执行的次数,可得它的执行的频率为N(N-1)(N-2)/6=N^3/6-N^2/2+N/3。。这条式子在这里就推导,有兴趣可以自行推导。
求近似
但是如果我们按照这样的分析,用语句执行的频率来作为算法的时间复杂度。那么当产生复杂数学表达式的时候,不利于我们分析这个算法的优劣。例如:
N(N-1)(N-2)/6=N^3/6-N^2/2+N/3。
这个表达式是不是看起来有点复杂,但是我们可以知道的是,一般在这种表达式中,随着N的增大,那么首项之后其他项的值都相对较小,(例如当N=1000时,-N^2/2+N/3≈499 667,而N^3/6≈166 666 667,).由此我们可以得出,我们可以去除非常复杂但是幂次较低的项,因为它们对于我们的结果影响较小。
当N不断增大的时候,N^3/6-N^2/2+N/3除以N3/6的值无限趋近于一。所以我们用N^3/6作为近似值
所以我们取近似值N^3/6。
再进一步我们忽略系数,而把所有的注意力集中在问题规模上,那么这个算法的时间复杂度为N^3.。所以我们以后取时间复杂度时,只取最高次幂并且忽略它的系数。
函数 | 近似 | 时间复杂度 |
N^3/6-N^2/2+N/3 | N^3/6 | N^3 |
N^2/2+N/3 | N^2/2 | N^2 |
lgN+1 | lgN | lgN |
3 | 3 | 1 |
常见的时间复杂度总结
描述 | 时间复杂度 | 典型代码 | 说明 |
常数级别 | 1 | a+b=c | 普通语句 |
对数级别 | logN | 二分查找 | 二分策略 |
线性级别 | N | For(int i=0;i<N;i++) { } | 循环 |
线性对数级别 | NLogN | 分治 | 归并排序 |
平方级别 | N2 | For(int i=0;i<N;i++){ For(int j=i;j<N;j++) { } } | 双层循环 |
立方级别 | N3 | 三个for嵌套的for循环 | 三层循环 |
显然,对于算法,它的时间复杂度越低越好。如果一个算法的时间复杂度过高,那么对于我们来说是不可接受了,因为它实在是太慢了。
通常O(1)<O(logN)<O(N)<O(NLogN)<O(N^2)<O(N^3)<O(2^N)
参考:算法第四版。