资源的有效使用是评价一个软件质量的一个指标。完成某个任务的算法的有效性是确定程序执行速度的主要因素。虽然可以根据算法所用的内存量来分析算法的有效性,但是分析CPU时间往往更准确。然而通过测量两个算法的执行时间来比较算法是非常困难的。为了克服这些问题,计算机科学家开发了一个独立于计算机和指定输入的理论方法来分析算法。该方法大致估计了输入大小的改变而产生的影响。通过这个方法可以看到,随着输入大小的增长,算法执行时间增长得有多快,因此,可以通过检查两个算法的增长率来比较它们。
以线性查找为例。线性查找算法顺序比较数组中的元素与键值,直到找到键值或者数组已搜索完毕。如果该键值没有在数组中,那么对于一个大小为n的数组需要n次比较。如果该键值在数组中,那么平均需要n/2次比较。该算法的执行时间与数组的大小成正比。如果将数组大小加倍,那么,比较次数也会加倍。该算法是呈线性增长的,增长率是n的数量级。计算机科学家使用大O符号表示“数量级”。使用该符号,线性查找算法的复杂度就是O(n),读为“n阶”。
例如以下代码:
public static boolean search(double [] data, double target){
int i;
for (i = 0; i < data.length; i++){
if (data[i] == target) return true;
}
return false;
}
分析包含三个部分:
1、当for循环形开始时,完成了两个操作:第一个操作是将变量i初始化为0的赋值操作,另一个是执行测试,判断i是否小于data.length的操作。
2、当执行循环体时,由于要查找的数值在数组中不存在,因此循环体将执行n次。假设每执行一次循环体代需要k个操作,其中k是3或4左右的数值。所以循环体共执行了n次,第执行一次循环体需要k个操作,共执行了kn个操作。
3、循环结束后还有一个操作。
现在得到 的总操作娄是kn+3.但不管k有多大,可以看到,这个公式是线性的。所以算法的复杂度为O(n)。
对于相同的输入大小,算法的执行时间可能会随着输入的不同而不同。导致最短执行时间的输入称为最佳情况输入;而导致最长执行时间的输入称为最差情况输入。最佳和最差情况都不具有代表性,但是最差情况分析却是非常有用的。我们要确保自己的算法永远不会比最差情况还慢。平均情况分析试图在所有可能的相同大小输入中确定平均时间。平均情况分析是比较理想的,但是完成,这是因为对于许多问题而言,要确定各种输入实例的相对概率和分布是相当困难的。由于最差情况分析比较容易完成,所以,分析通常由最差情况主导。
如果要在线性表中查找一个已存在于线性表中的元素,那么线性查找在最差情况下需要n次比较,而在平均情况下需要n/2次比较。使用大O符号,这两种情况需要的时间都为O(n)。相乘常量(1/2)可以忽略。算法分析的重点在于增长率,而相乘常量对增长率没有影响。对于n/2或100n而言,增长率都和n一样。因此,O(n)=O(n/2)=O(100n)。
考虑在包含n个元素的数组中找出最大数据的算法。如果n为2,找到最大数需要一次比较;如果n为3,找到最大数需要再次比较。一般来况,在拥有n个无此的线性表中找到最大数需要n-1次比较。算法分析主要用于庞大的输入规模。如果输入规模较小,那么估计算法效率是意义的。随着n的增大,表达式n-1中的n就主导了复杂度。大O符号允许忽略非主导部分(例如,表达式n-1中的-1),并强调重要部分(例如,表达式n-1中的n)。因此,该算法的复杂度为O(n)。
我们用大O符号估计一个算法与输入规模相关的执行时间。如果执行时间与输入规模无关,该算法就称为耗费了常量时间,用符号O(1)表示。例如,在数组中从给定下标处获取元素的方法耗费的时间即称为常量时间,这是因为该时间不会随数组规模的增大而增长。
常用的增长函数的增长率的变化
函数 | 名称 | n=25 | n=50 | f(50)/f(25) |
O(1) | 常量时间 | 1 | 1 | 1 |
O(log n) | 对数时间 | 4.64 | 5.64 | 1.21 |
O(n) | 线性时间 | 25 | 50 | 2 |
O(n*log n) | 对数-线性时间 | 116 | 282 | 2.43 |
O(ne2) | 二次时间 | 625 | 2500 | 4 |
O(ne3) | 三次时间 | 15625 | 125000 | 8 |
O(2en) | 指数时间 | 3.36*10e7 | 1.27*10e15 | 3.35*10e7 |
常见的递归函数
递归关系 | 结果 | 举例 |
T(n)=T(n/2)+O(1) | T(n)=O(log n) | 十分查找,欧几里得的GCD |
T(n)=T(n-1)+O(1) | T(n)=O(n) | 线性查找 |
T(n)=2T(n/2)+O(1) | T(n)=O(n) | |
T(n)=2T(n/2)+O(n) | T(n)=O(nlog n) | 归并排序 |
T(n)=2T(n/2)+O(nlog n) | T(n)=O(nlog2 n) | |
T(n)=T(n-1)+O(n) | T(n)=O(ne2) | 选择排序、插入排序 |
T(n)=2T(n-1)+O(1) | T(n)=O(2en) | 汉诺塔 |
T(n)=T(n-1)+T(n-2)+O(1) | T(n)=O(2en) | 递归的斐波那契数 |