数据结构与算法分析笔记(c++)_chapter2算法分析_数学基础、模型、分析的问题、运行时间计算

第二章 算法分析

算法(algorithm)是为求解一个问题需要遵循的、被清楚地指定的简单指令的集合。
本章将讨论
·如何估计一个程序所需要的时间。
·如何将一个程序的运行时间从天或年降低到不足一秒。
·粗心地使用递归的后果。
·用于将一个数自乘得到其幂以及计算两个数的最大公因数的非常有效的算法。

2.1数学基础
在这里插入图片描述

我们比较它们的相对增长率(relative rates of growth)。
如果我们用传统的不等式来比较增长率,那么第一个定义是说T(N)的增长率小于等于(≤)f(N)的增长率。第二个定义T(N)=OMEGA(g(M)(念成“omega”)是说T(N)的增长率大于等于(≥)g(N)的增长率。第三个定T(N)=THETA(h(N))(念成“theta”)是说T(N)的增长率等于(=)h(N)的增长率。
最后一个定义T(N)=o(p(N))(念成“小o”)说的则是T(N)的增长率小于(<)p(N)的增长率。它不同于大O,因为大O包含增长率相同这种可能性。
当我们说T(N)=O(f(N))时,我们是在保证函数T(N)是在以不快于(N)的速度增长;因此f(N)
是T(N)的一个上界(upper bound)。由于这意味着f(N)=OMEGA(T(N)),于是我们说T(N)是f(N)的一个下界(lower bound)。
在这里插入图片描述

各种简化都是可能发生的。低阶项一般可以被忽略,而常数也可以丢弃掉。此时,要求的精度是很低的。
在这里插入图片描述

另外,在风格上还应注意:不要说成f(N)≤O(g(N)),因为定义已经隐含有不等式了。
对诸如所提出的冒泡排序这样的简单排序算法,当输入量增加到2倍的时候,对大量输入来说,运行时间则增加到4倍。这是因为这些算法不是线性的。在讨论排序时,我们将会看到,不好的排序算法是O(N2),或称为二次的。

2.2模型
为了在形式的框架中分析算法,我们需要一个计算模型。我们的模型基本上是一台标准的计算机,在机器中指令被顺序地执行。该模型有一个标准的简单指令系统,如加法、乘法、比较和赋值等。但不同于实际计算机的是,模型机做任意一件简单的工作都恰好花费一个时间单位。

2.3要分析的问题
要分析的最重要的资源一般说来就是运行时间。有几个因素影响着程序的运行时间。所使用的算法以及对该算法的输入。
偶尔也分析一个算法最好情形的性能。不过,通常这并不重要,因为它不代表典型的结果。平均情形性能常常反应典型的结果,而最坏情形的性能则代表对任何可能的输入在性能上的一种保证。
一般说来,若无特别说明,则所需要的量就是最坏情况的运行时间。其原因之一是它对所有的输入提供了一个界限,包括特别坏的输入,而平均情况分析不提供这样的界。
如果只是小量输入的情形,那么花费大量的努力去设计聪明的算法恐怕就太不值得了。另一方面,近来对于重写那些不再合理的基于小输入量假设而在五年以前编写的程序确实存在着巨大的市场。现在看来,这些程序太慢了,因为它们用的是些不好的算法。
数据的读入一般是个瓶颈;一旦数据读入,问题就会迅速解决。

2.4运行时间计算
如果认为两个程序花费大致相同的时间,要确定哪个程序更快的最好方法很可能就是将它们编码并运行!
一般地,如果有几种算法思想,而我们总愿意尽早去除那些不好的算法思想,因此,通常需要对算法进行分析。不仅如此,进行分析的能力还有助于洞察到如何设计高效算法。一般说来,分析还能准确地确定需要仔细编码的瓶颈。
为了简化分析,我们将采纳如下的约定:不存在特定的时间单位。因此,我们抛弃那些常数系数。我们还将抛弃低阶项,因此所要做的就是计算大O运行时间。
一般法则:
1.法则1:for循环一个for循环的运行时间至多是该for循环内语句(包括测试)的运行时间乘以迭代的次数。
2.法则2:嵌套循环从里向外分析这些循环。在一组嵌套循环内部的一条语句总的运行时间为该语句的运行时间乘以该组所有循环的大小的乘积。
3.法则3:顺序语句将各个语句的运行时间求和即可(这意味着,其中的最大值就是所得的运行时间;见2.1节的法则1)。
4.法则4:If/E1se 一个if/else语句的运行时间从不超过判断再加上S1和S2中运行时间较长者的总的运行时间。
显然在某些情形下这么估计有些过高,但决不会估计过低。
其他的法则都是显然的,但是,分析的基本策略是从内部(或最深层部分)向外展开工作。
如果有方法调用,那么这些调用要首先分析。如果有递归过程,那么存在几种选择。若递归实际上只是被薄面纱遮住的for循环,则分析通常是很简单的。

现在我们将要叙述四个算法来求解早先提出的最大子序列和的问题。
在这里插入图片描述

1.穷举式
在这里插入图片描述

运行时间为O(N^3),第14行由一个隐含于三重嵌套for循环中的O(1)语句组成。
我们可以通过撤除一个for循环来避免三次运行时间。不过这并不总是可能的
改进算法:O(N^2)
在这里插入图片描述

对这个问题有一个递归和相对复杂的O(NlogN)解法,现在对其进行论述。要是真的没出现O(N)(线性的)解法,这个算法就会是体现递归威力的极好的范例了。该方法采用一种“分治”(divide-and-conquer)策略。其想法是把问题分成两个大致相等的子问题,然后递归地对它们求解,这是“分”的部分。“治”阶段将两个子问题的解合并到一起并可能再做少量的附加工作,最后得到整个问题的解。
在我们的例子中,最大子序列和可能出现在三处地方:或者整个出现在输入数据的左半部或者整个出现在右半部,或者跨越输入数据的中部从而占据左右两半部分。前两种情况可以递归求解。第三种情况的最大和可以通过求出前半部分的最大和(包含前半部分的最后一个元素)以及后半部分的最大和(包含后半部分的第一个元素)而得到。然后将这两个和加在一起。
O(NlogN)
在这里插入图片描述

第8行至第12行处理基准情况。
第15行和第16行执行两个递归调用。我们可以看到,递归调用总是用于小于原问题的问题
第18行至第24行以及第26行至第32行计算达到中间分界处的两个最大的和数。这两个值的和为横跨左右两边的最大和。例程max3(未显示出)返回这三个可能的最大和中的最大者。
显然,算法3需要比前面两种算法更多的编程工作量。然而,程序短并不总是意味着程序好
第四种方法:比递归简单且更为有效
在这里插入图片描述

·任何负的子序列不可能是最优子序列的前缀
这个算法是许多聪明算法的典型:运行时间是明显的,但正确性则不明显。
该算法的一个附带的优点是,它只对数据进行一次扫描,一旦a[i]被读入并被处理,它就不再需要被记忆。因此,如果数组在磁盘或磁带上,它就可以被顺序读入,在主存中不必存储数组的任何部分。不仅如此,在任意时刻,算法都能对它已经读入的数据给出子序列问题的正确答案其他算法不具有这个特性)。具有这种特性的算法叫作联机算法(on-line algorithm)。仅需要常量空间并以线性时间运行的联机算法几乎是完美的算法。
2.4.4运行时间中的对数
分析算法最混乱的方面大概集中在对数上面。我们已经看到,某些分治算法将以O(NLogN)时间运行。除分治算法外,对数最常出现的规律可概括为下列一般法则:如果一个算法用常数时间(O(1))将问题的大小削减为其一部分(通常是1/2),那么该算法就是O(Log N)的。另一方面,如果使用常数时间只是把问题减少一个常数的数量(如将问题减少1),那么这种算法就是O(N)的。
具有对数特点的三个例子:
1.二分搜索(binary search).
问题:二分给定一个整数X和整数A,A1,…,A N-1,后者已经预先排序并在内存中,求下标i使得Ai=X,如果不在数据中,则返回-1

一个好的策略是验证是否是居中的元素。如果是,则答案就找到了。如果X小于居中元素,那么我们可以应用同样的策略于居中元素左边已排序的子序列;同理,如果x大于居中元素,那么我们检查数据的右半部分。
在这里插入图片描述

二分搜索可以看作是我们的第一个数据结构实现方法,它提供了在O(logN)时间内的contains操作(包含查询,即X是否在序列中),但是所有其他操作(特別是 insert操作)均需要O(N)时间。在数据是稳定(即不允许插入操作和删除操作)的应用中,这可能是非常有用的。此时输入数据需要一次排序,但是此后的访问会很快。
2.欧几里得算法:计算最大公因数(gcd)
在这里插入图片描述

算法通过连续计算余数直到余数是0为止,最后的非零余数就是最大公因数。这是一个快速算法。
事实上,在一次迭代中余数并不按照一个常数因子递减。然而,我们可以证明,在两次迭代以后,余数最多是原始值的一半。

定理2.1如果M>N,则 M mod N<M/2
证明存在两种情形。如果N≤M/2,则由于余数小于N,故定理在这种情形下成立。另一种情形是N>M/2。但是此时M仅含有一个N,从而余数为M-N<M/2,定理得证。
事实上,这个常数在最坏的情况下(如M和N是两个相邻的斐波那契数时就是这种情况〕还可以稍微改进成1.44logN。
3.幂运算
在这里插入图片描述

显然,所需要的乘法次数最多是2logN,因为把问题分半最多需要两次乘法(如果N是奇数)。这里,我们又可写出一个递推公式并将其解出。简单的直觉避免了盲目的强行处理
在这里插入图片描述

2.4.5检验你的分析
一种方法是编程并比较实际观察到的运行时间是否与通过分析所描述的运行时间相匹配。如果N大一倍,则线性程序的运行时间乘以系数2,二次程序的运行时间乘以系数4,而三次程序的运行时间则乘以系数8。以对数时间运行的程序当N增加一倍时其运行时间只是多加一个常数,而以O(NlogN)运行的程序则花费比在相同环境下运行时间的两倍稍多一些的时间。
验证一个程序是否是O(f(n))的另一个常用的技巧是对N的某个范围计算比值Tn/FN(通常用2的倍数隔开
,其中tn是凭经验观察到的运行时间。如果fn是运行时间的理想近似,那么所算出的值收敛于一个正常数。如果估计过大,则算出的值收敛于0。如果fN估计过低从而O(f(n)是错的,那么算出的值发散

2.4.6分析结果的准确性
经验指出,有时分析会估计过大。如果发生这种情况,那么或者需要分析得更细(一般通过机敏的观察),或者可能是平均运行时间显著小于最坏情形的运行时间而又不可能对所得的界再加以改进。对于许多复杂的算法,最坏的界通过某个不良输入是可以达到的,但在实践中它通常是估计过大的。遗憾的是,对于大多数这种问题,平均情形的分析是极其复杂的(在许多情形下还是未解决的),而最坏情形的界尽管过分悲观但却是最好的已知分析结果。
小结
本章对如何分析程序的复杂性给出了一些提示。遗憾的是,它并不是完善的分析指南
类有趣的分析是下界分析,我们尚未接触到。在第7章我们将看到这方面的一个例子:证明任何仅通过使用比较来进行排序的算法在最坏的情形下只需要omega(NlogN)次比较。下界的证明般是最困难的,因为它们不只适用求解某个问题的一个算法而是适用求解该问题的一类算法。
gcd算法和求幂算法应用在密码学中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值