算法导论3rd(译)-算法入门(2.2算法分析)

来源:互联网 发布:宝宝网络电视 贝贝版 编辑:程序博客网 时间:2024/04/30 06:33

2.2 算法分析

分析一个算法意味着预测这个算法的所需资源。有时,像内存、通信带宽或计算机硬件这些资源是首要考虑的,但大多数情况下我们需要衡量计算时间。一般情况下,通过分析一个问题的几个候选算法,我们可以找出最有效的一个。这种分析可能得出不止一个可行的候选者,但在这个过程中我们长长可以放弃一些劣质的算法。

在我们能够分析算法前,我们必须有一个我们要使用的实现技术的一个模型,包括一个技术所需资源的模型和成本。在本书大部分中,我们假设一个通用的单处理器的随机存取机(RAM)计算模型作为我们的实现技术,并了解到我们的算法会被实现为计算机程序。在RAM模型中,指令是一个接一个的执行的,没有任何并发操作。

严格的说,我们需要精确的定义RAM模型的指令及其成本。但是这样做会比较单调,并且对算法设计和分析没有多大的深入。所以我们必须小心的不要滥用RAM模型。例如,如果RAM有一个进行排序的指令会怎么样?这时我们可以用一条指令进行排序。这样的RAM是不现实的,因为现实中的计算机没有这样的指令。因此,我们的方向是现实中的计算机是如何设计的。RAM模型的指令能够在现实中的计算机中普遍找到:算数(像加、减、乘、除、取余、向下取整、向上取整)、数据移动(加载、保存、复制)以及控制(条件和非条件分支、子程序的调用和返回)。每一个这样的指令要花费一个常量时间。

RAM模型中的数据的类型为整数和浮点数(为保存实数)。尽管在本书中我们不要求精确,但在有些程序中精确是至关重要的。我们同样假设数据的每个字的大小有一个极限。例如,当处理规模为n的输入时,我们通常假设整数由位来表示,c是一个的常数且。之所以有,因为这样每个字可以容纳下n,从而使我们能够索引每个输入元素。同时,我们限制c是一个常数,因此字的大小不会任意的增长。(如果字的大小可以任意增长,那么我们可以在一个字中保存大量的数据,对它进行操作都只用常量时间——很显然这是不现实的情况。)

现实中的计算机还包含一些上面未列出的指令,这些指令表现为RAM模型中的一个灰色区域。例如,指数运算是个常量时间的指令么?通常情况下,不是。当xy都是实数时,它需要几条指令来计算。然后,在限定的条件下,指数运算是个常量时间的操作。许多计算机有一个“左移”指令,它可以在常量时间里把一个整数向左移k位。在大多数计算机中,把一个整数向左移一位等于将它乘以2,因此向左移动k为等价于乘以。因此,这样的计算机可以用一个常量时间的指令通过将整数1向左移动k位来计算,并且k不大于一个计算机字的位数。我们将努力避免RAM模型中的这些灰色区域,但在k是个很小的正整数时,我们视计算为一个常量时间的操作。

在RAM模型中,我们不试图去对现代计算机中很普遍的内存体系进行建模。即,我们不对缓存或虚拟内存进行建模。几个计算模型会尝试考虑内存体系的影响,这在实时计算机上的实时程序中有时很有意义。本书中有少量的问题会涉及内存体系的影响,但本书中的绝大部分分析将不会考虑它们。包含内存体系的模型要比RAM模型要复杂的多,因此它们可能较难处理。此外,在实际计算机中,RAM模型分析常常能对性能进行极好的预测。

在RAM模型中分析一个甚至很简单的模型也很具挑战性。需要的数学工具可能包括组合、概率论、高等代数以及标识公式中最重要项的能力。因为对于每个可能的输入,一个算法的行为可能会不同,我们需要一种将这种行为简化为简单、易理解公式的方法。

尽管我们通常选择只一种机器模型来分析给定算法,我们也会在决定怎么表达我们的分析时面临很多选择。我们喜欢一种方法,这种方法能够简单的书写和处理、能表示一个算法资源占用的重要特征并且去掉那些单调细节。

插入排序的分析

INSERTION-SORT过程的时间依赖于输入:对一千个数进行排序要比只排三个数的时间要长。此外,INSERTION-SORT需要不同数量的时间去对两个相同大小的输入序列排序,这依赖于它们已排序的程度。通常,算法的时间随着输入规模的增长而增长,所以传统上将一个程序的运行时间表述为一个其输入规模的函数。这样,我们需要仔细的定义“运行时间”和“输入规模”。

输入规模的最好的概念取决于待研究的问题。对大多数问题来说,像排序或计算离散傅立叶变换,最自然的度量是输入元素的个数——例如,待排序数组的大小n。对其他很多问题,像两个整数相乘,输入规模的最好度量是将输入表示为传统的二进制形式后所需的总位数。有时,将输入规模描述为两个数而不是一个会更合适。例如,如果一个算法的输入是图,输入规模可以描述为图中顶点和边的个数。我们需指明我们所研究的每个问题的输入规模度量。

一个算法在指定输入上的运行时间是执行原语操作或步骤的数量。定义步骤的概念便于使其尽可能的独立于机器。就目前而言,我们采取以下观点。执行我们伪码中的每一行需要常数数量的时间。一行可能比其他行需要不同数量的时间,但是我们假设第i行的每次执行需要的时间是 ,其中 是常数。这种观点是符合RAM模型的,它也反映了伪码如何在大多数实际计算机实现的。

在接下来的讨论中,我们对INSERTION-SORT运行时间的表达式将从使用所有语句耗时 的复杂的公式演变为一个更简单的记号,它更简洁、更容易被处理这种简单的记号也将很容易的确定一个算法是否比另一个更高效。

我们使用每个语句的时间消耗和每个语句的执行次数开始来介绍INSERTION-SORT过程。对于每个j=2,3,…,n,其中n = A.length,我们用来表示对于值j来说第5行while循环测试的执行次数。当for或while循环正常退出时(即,由于循环头中的测试),该测试比循环体多执行一次。我们假设注释不是可执行语句,那么它们不消耗时间。

INSERTION-SORT(A)                                                       成本                                   次数

1   for j = 2 to A.length                                                                                                  n

2       key = A[j]                                                                                                               n-1

3       //将A[j]插入到已排好序 的序列A[1…j-1]中              0                                           n-1

4       i = j – 1                                                                                                                 n-1

5       while i > 0 and A[i] > key                                                                                  

6           A[i+1] = A[i]                                                                                                      

7           i = i - 1                                                                                                              

8       A[i+1] = key                                                                                                          n-1

该算法的运行时间是每条语句的运行时间的总和。一个执行步并且执行n次的语句在总的运行时间中占。为计算T(n),INSERTION-SORT在输入规模为n个数据时的运行时间,我们对成本和时间列先进行乘积再求和,得到

                   

即使对于指定规模的输入,一个算法的运行时间依赖于指定了该大小下的哪一个输入。例如,在INSERTION-SORT中,最好的情况发生在数组已经排好序时。对于每一个j=2,3,…,n,这时我们发现在第5行中当i有初始化值j-1时A[i]≤key成立。故对于j=2,3,…,n有 成立,且最好情况的运行时间为

           

我们可以描述该运行时间为an+b,其中a和b为常数,且依赖于语句的成本,因此它是n的线性函数。

如果数组与排序的顺序相反—即递减顺序—结果就是最坏的情况。我们必须将每个A[j]与整个已排序的子数组A[1…j-1]中的每个元素进行比较,此时 ,其中j=2,3,…,n。注意到

以及

(参考附录A,来回顾如何求这些和。)我们得出在最坏情况下,INSERTION-SORT的运行时间是

我们可以描述这种最坏情况运行时间为,其中a和b是常数,c同样是依赖于语句成本,这样它是一个关于n的二次函数。

通常情况下,如插入排序,对于给定的输入,算法的运行时间是固定的。虽然在后面的章节中,我们将看到一些有趣的“随机”算法,即使是固定的输入,其行为也有所不同。

最坏情况和平均情况分析

在插入排序的分析中,我们看到了最好情况,其中输入数组已排序,以及最坏情况,其中的输入数组是反向排序的。不过,对本书其余的部分,我们通常关注于只寻找最坏情况的运行时间,也即,对于任何输入规模n的最长运行时间。我们给出用这种方向的三个原因。

.一个算法的最坏情况的原型时间为我们提供了一个对任何输入的运行时间的上界。它提供了算法将不会运行更长的时间的一个保证。我们不需要对运行时间做一些推测,以希望它永远不会变的更坏。

.对于一些算法来说,最坏情况发生的非常频繁。例如,在数据库中搜索一个指定的信息,当该信息并不在数据库时,搜索算法的最坏情况将经常发生。在一些程序中,搜索不存在的信息可能很频繁。

.“平均情况”往往大致和最坏情况一样差。假设我们随机选择n个数并应用插入排序。它需要花多少时间来确定在子数组A[1…j-1]的哪个位置插入元素A[j]?平均而言,A[1…j-1]中的一半元素小于A[j],一半元素大于A[j]。因此,我们检查A[1…j-1]的一半的元素,这样大约是j/2。所得到的平均情况的运行时间的结果是输入规模的二次函数,就像最坏情况下的运行时间一样。

在某些特殊情况下,我们将对一个算法的平均情况的运行时间感兴趣。在整本书中我们将会看到概率分析技术被应用到各种算法中。平均情况分析的范围是有限的,因为对于一个特定的问题可能并不清楚什么构成了一个“平均”输入。通常情况下,我们假设给定大小的所有输入都是等可能的。在实践中,可能会违反这个假设。但是我们有时可以使用一个随机化算法,它产生随机选择,从而使用一个概率分析得出一个期望运行时间。我们在第5章和一些后续章节中更多的探索随机化算法。

增长量级

我们使用一些简单的抽象来简化我们对INSERTION-SORT过程的分析。首先,我们忽略每条语句的实际成本,使用常数来表示这些成本。然后,我们注意到这些常量给我们的细节要比我们实际需要的要多。我们把最坏情况运行时间表示为,其中a和b是常数,c同样是依赖于语句成本 。因此,我们不仅忽略了语句的实际成本,还有抽象的成本

现在我们做一个更简化的抽象:即增长率,或者增长量级,这是运行时间中真正吸引我们的东西。因此,我们只考虑公式的首项(例如),因为当n值很大时低阶的项相对来说是微不足道的。我们还忽略首项的常系数,因为对大规模输入来说,在确定计算效率上常数因子的影响不如增长率显著。对于插入排序,当我们忽略了低阶项和首项的常系数时,就只剩下了首项中的。我们记插入排序有最坏情况运行时间Θ()。本章中我们非正式的使用Θ记号,我们将在第3章中精确的定义它。

如果一个算法的最坏情况运行时间有着较低的增长量级,我们通常认为它比另一个更高效。由于常数因子和低阶项,一个有着较高增长量级运行时间的算法可能在小的输入上需要的时间比一个有着较低增长量级运行时间的算法要少。但是,对于足够大的输入,比如一个Θ()的算法要比一个Θ()的算法在最坏情况下运行的要快。
0 0
原创粉丝点击