|
|
|
|
移动端

2.1 数学基础

《数据结构与算法分析:Java语言描述(原书第3版)》第2章算法分析,本章对如何分析程序的复杂性给出一些提示。遗憾的是, 它并不是完善的分析指南。简单的程序通常给出简单的分析, 但是情况也并不总是如此。本节为大家介绍数学基础。

作者:佚名来源:机械工业出版社|2016-04-13 11:25

第2章 算法分析

算法(algorithm)是为求解一个问题需要遵循的、 被清楚指定的简单指令的集合。对于一个问题, 一旦某种算法给定并且(以某种方式)被确定是正确的, 那么重要的一步就是确定该算法将需要多少诸如时间或空间等资源量的问题。如果一个问题的求解算法竟然需要长达一年时间, 那么这种算法就很难能有什么用处。同样, 一个需要若干个GB(gigabyte)的内存的算法在当前的大多数机器上也是无法使用的。

在这一章, 我们将讨论:

如何估计一个程序所需要的时间。

如何将一个程序的运行时间从天或年降低到秒甚至更少。

粗心使用递归的后果。

将一个数自乘得到其幂, 以及计算两个数的最大公因数的非常有效的算法。

2.1 数学基础

一般说来, 估计算法资源消耗所需的分析是一个理论问题, 因此需要一套正式的系统架构。我们先从某些数学定义开始。

本书将使用下列四个定义:

定义2.1 如果存在正常数c和n0使得当N≥n0时T(N)≤cf(N), 则记为T(N)=O(f(N))。

定义2.2 如果存在正常数c和n0使得当N≥n0时T(N)≥cg(N),则记为T(N)=Ω(g(N))。

定义2.3 T(N)=Θ(h(N))当且仅当T(N)=O(h(N))和T(N)=Ω(h(N))。29

定义2.4 如果对每一正常数c都存在常数n0使得当N>n0时T(N)<cp(N), 则T(N)=o(p(N))。有时也可以说, 如果T(N)=O(p(N))且T(N)≠Θ(p(N)), 则T(N)=o(p(N))。

这些定义的目的是要在函数间建立一种相对的级别。给定两个函数, 通常存在一些点, 在这些点上一个函数的值小于另一个函数的值, 因此, 一般地宣称, 比如说f(N)<g(N), 是没有什么意义的。于是, 我们比较它们的相对增长率(relative rate of growth)。当将相对增长率应用到算法分析时, 我们将会明白为什么它是重要的度量。

虽然对于较小的N值1000N要比N2大, 但N2以更快的速度增长, 因此N2最终将是更大的函数。在这种情况下, N=1000是转折点。第一个定义是说, 最后总会存在某个点n0从它以后c·f(N)总是至少与T(N)一样大, 从而若忽略常数因子, 则f(N)至少与T(N)一样大。在我们的例子中, T(N)=1000N,f(N)=N2,n0=1000而c=1。我们也可以让n0=10而c=100。因此, 可以说1000N=O(N2)(N平方级)。这种记法称为大O标记法。人们常常不说“……级的”, 而是说“大O……”。

如果用传统的不等式来计算增长率, 那么第一个定义是说T(N)的增长率小于或等于f(N)的增长率。第二个定义T(N)=Ω(g(N))(念成“omega”)是说T(N)的增长率大于或等于g(N)的增长率。第三个定义T(N)=Θ(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)=O(f(N))时, 我们是在保证函数T(N)是在以不快于f(N)的速度增长; 因此f(N)是T(N)的一个上界(upper bound)。这意味着f(N)=Ω(T(N)), 于是我们说T(N)是f(N)的一个下界(lower bound)。

作为一个例子, N3比N2增长快, 因此我们可以说N2=O(N3)或N3=Ω(N2)。f(N)=N2和g(N)=2N2以相同的速率增长, 从而f(N)=O(g(N))和f(N)=Ω(g(N))都是正确的。当两个函数以相同的速率增长时, 是否需要使用记号Θ()表示可能依赖于具体的上下文。直观地说, 如果g(N)=2N 2,  那么g(N)=O(N4),g(N)=O(N3)和g(N)=O(N2)从技术上看都是成立的, 但最后一个是最佳选择。写法g(N)=Θ(N2)不仅表示g(N)=O(N2)而且还表示结果尽可能地好(严密)。30

我们需要掌握的重要结论为:

法则1:

如果T1(N)=O(f(N))且T2(N)=O(g(N)),  那么

(a) T1(N)+T2(N)=O(f(N)+g(N))(直观地和非正式地可以写成max(O(f(N)),  O(g(N))))。 

(b) T1(N)*T2(N)=O(f(N)*g(N))。

法则2:

如果T(N)是一个k次多项式, 则T(N)=Θ(Nk)。

法则3:

对任意常数k,   logkN=O(N)。它告诉我们对数增长得非常缓慢。

这些信息足以按照增长率对大部分常见的函数进行分类(见图2-1)。

有几点需要注意。首先, 将常数或低阶项放进大O是非常坏的习惯。不要写成T(N)=O(2N2)或T(N)=O(N2+N)。在这两种情形下, 正确的形式是T(N)=O(N2)。这就是说, 在需要大O表示的任何分析中, 各种简化都是可能发生的。低阶项一般可以被忽略, 而常数也可以弃掉。此时, 要求的精度是很粗糙的。

第二, 我们总能够通过计算极限limN→∞f(N)/g(N)来确定两个函数f(N)和g(N)的相对增长率, 必要的时候可以使用洛必达法则 洛必达法则说的是, 若limN→∞f(N)=∞且limN→∞g(N)=∞, 则limN→∞f(N)/g(N)=limN→∞f′(N)/g′(N), 而f′(N)和g′(N)分别是f(N)和g(N)的导数。。该极限可以有四种可能的值:

极限是0: 这意味着f(N)=o(g(N))。

极限是c≠0: 这意味着f(N)=Θ(g(N))。31

极限是∞: 这意味着g(N)=o(f(N))。

极限摆动: 二者无关(在本书中将不会发生这种情形)。

使用这种方法几乎总能够算出相对增长率, 不过有些复杂化。通常, 两个函数f(N)和g(N)间的关系用简单的代数方法就能得到。例如, 如果f(N)=Nlog(N)和g(N)=N1. 5, 那么为了确定f(N)和g(N)哪个增长得更快, 实际上就是确定logN和N0. 5哪个增长更快。这与确定log 2 N和N哪个增长更快是一样的, 而后者是个简单的问题, 因为我们已经知道, N的增长要快于log的任意的幂。因此, g(N)的增长快于f(N)的增长。

另外, 在风格上还应注意: 不要写成f(N)≤O(g(N)), 因为定义已经隐含有不等式了。写成f(N)≥O(g(N))是错误的, 它没有意义。

作为所执行的典型类型分析的例子, 考虑在互联网上下载文件的问题。设有初始3s的延迟(来建立连接), 此后下载以1.5K(B)/s的速度进行。可以推出, 如果文件为N个KB, 那么下载时间由公式T(N)=N/1.5+3表示。这是一个线性函数(linear function)。注意, 下载一个1500K的文件所用时间(1003s)近似(但不是精确)地为下载750K文件所用时间(503s)的两倍。这是典型的线性函数。还要注意, 如果连接的速度快两倍, 那么两种时间都要减少, 但1500K文件的下载仍然花费大约下载750K文件的时间的两倍。这是线性时间算法的典型特点, 这就是为什么我们写T(N)=O(N)而忽略常数因子的原因。(虽然使用大Θ会更精确, 但是一般给出的是大O答案。)

还要看到, 这种行为不是对所有的算法都成立。对于1.1节描述的第一个选择算法, 运行时间由执行一次排序所花费的时间来控制。对诸如所提出的冒泡排序这样的简单排序算法, 当输入量增加到两倍的时候, 则对大量输入的运行时间增加到4倍。这是因为这些算法不是线性的, 我们将看到, 当讨论排序时, 普通的排序算法是O(N2), 或叫作二次的。

喜欢的朋友可以添加我们的微信账号:

51CTO读书频道二维码


51CTO读书频道活动讨论群:342347198

【责任编辑:book TEL:(010)68476606】

回书目   上一节   下一节
点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

ASP网络编程从入门到精通

本书是为那些对Web开发感兴趣的读者而编写的。ASP(Active Server Pages)是微软公司在Web领域的又一次突破,它打破了以往只能由专业人员来...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊