|
|
|
|
移动端

1.3 递归简论

《数据结构与算法分析:Java语言描述(原书第3版)》第1章引论,在这一章, 我们阐述本书的目的和目标并简要复习离散数学以及程序设计的一些概念。本节为大家介绍递归简论。

作者:冯舜玺/陈越 译来源:机械工业出版社|2016-04-13 10:39

技术沙龙 | 6月30日与多位专家探讨技术高速发展下如何应对运维新挑战!


1.3 递归简论

我们熟悉的大多数数学函数都是由一个简单公式来描述的。例如, 我们可以利用公式

C=5(F-32)/9

将华氏温度转换成摄氏温度。有了这个公式, 写一个Java方法就太简单了。除去程序中的说明和大括号外, 这一行的公式正好翻译成一行Java程序。

有时候数学函数以不太标准的形式来定义。例如, 我们可以在非负整数集上定义一个函数f, 它满足f(0)=0且f(x)=2f(x-1)+x2。从这个定义我们看到f(1)=1,f(2)=6,f(3)=21, 以及f(4)=58。当一个函数用它自己来定义时就称为是递归(recursive)的。Java允许函数是递归的。 对于数值计算使用递归通常不是个好主意。我们在解释基本概念时已经说过。 

但重要的是要记住, Java提供的仅仅是遵循递归思想的一种尝试。不是所有的数学递归函数都能被有效地(或正确地)由Java的递归模拟来实现。上面例子说的是递归函数f应该只用几行就能表示出来, 正如非递归函数一样。图1-2指出了函数f的递归实现。

第3行和第4行处理基准情况(base case), 即此时函数的值可以直接算出而不用求助递归。正如f(x)=2f(x-1)+x2若没有f(0)=0这个事实在数学上没有意义一样, Java的递归方法若无基准情况也是毫无意义的。第6行执行的是递归调用。

关于递归, 有几个重要并且可能会被混淆的概念。一个常见的问题是: 它是否就是循环推理(circular logic)?答案是: 虽然我们定义一个方法用的是这个方法本身, 但是我们并没有用方法本身定义该方法的一个特定的实例。换句话说, 通过使用f(5)来得到f(5)的值才是循环的。通过使用f(4)得到f(5)的值不是循环的, 当然, 除非f(4)的求值又要用到对f(5)的计算。两个最重要的问题恐怕就是如何做和为什么做的问题了。8如何和为什么的问题将在第3章正式解决。这里, 我们将给出一个不完全的描述。

实际上, 递归调用在处理上与其他调用没有什么不同。如果以参数4的值调用函数f, 那么程序的第6行要求计算2*f(3)+4*4。这样, 就要执行一个计算f(3)的调用, 而这又导致计算2*f(2)+3*3。因此, 又要执行另一个计算f(2)的调用, 而这意味着必须求出2*f(1)+2*2的值。为此, 通过计算2*f(0)+1*1而得到f(1)。此时, f(0)必须被赋值。由于这属于基准情况, 因此我们事先知道f(0)=0。从而f(1)的计算得以完成, 其结果为1。然后, f(2)、f(3)以及最后f(4)的值都能够计算出来。跟踪挂起的函数调用(这些调用已经开始但是正等待着递归调用来完成)以及它们的变量的记录工作都是由计算机自动完成的。然而, 重要的问题在于, 递归调用将反复进行直到基准情形出现。例如, 计算f(-1)的值将导致调用f(-2)、 f(-3)等等。由于这将不可能出现基准情形, 因此程序也就不可能算出答案。偶尔还可能发生更加微妙的错误, 我们将其展示在图1-3中。图1-3中程序的这种错误是第6行上的bad(1)定义为bad(1)。显然, 实际上bad(1)究竟是多少, 这个定义给不出任何线索。因此, 计算机将会反复调用bad(1)以期解出它的值。最后, 计算机簿记系统将占满内存空间, 程序崩溃。一般情形下, 我们会说该方法对一个特殊情形无效, 而在其他情形是正确的。但此处这么说则不正确,

因为bad(2)调用bad(1)。因此, bad(2)也不能求出值来。不仅如此, bad(3)、 bad(4)和bad(5)都要调用 bad(2), bad(2)算不出值, 9它们的值也就不能求出。事实上, 除了0之外, 这个程序对n的任何非负值都无效。对于递归程序, 不存在像“特殊情形”这样的情况。

上面的讨论导致递归的前两个基本法则:

1. 基准情形(base case)。必须总要有某些基准的情形, 它们不用递归就能求解。

2. 不断推进(making progress)。对于那些要递归求解的情形, 递归调用必须总能够朝着一个基准情形推进。

在本书中我们将用递归解决一些问题。作为非数学应用的一个例子, 考虑一本大词典。词典中的词都是用其他的词定义的。当查一个单词的时候, 我们不是总能理解对该词的解释, 于是我们不得不再查找解释中的一些词。同样, 对这些词中的某些地方我们又不理解, 因此还要继续这种查找。因为词典是有限的, 所以实际上或者我们最终要查到一处, 明白了此处解释中所有的单词(从而理解这里的解释, 并按照查找的路径回查其余的解释)或者我们发现这些解释形成一个循环, 无法理解其最终含义, 或者在解释中需要我们理解的某个单词不在这本词典里。

我们理解这些单词的递归策略如下: 如果知道一个单词的含义, 那么就算我们成功; 否则, 就在词典里查找这个单词。如果我们理解对该词解释中的所有的单词, 那么又算我们成功; 否则, 通过递归查找一些我们不认识的单词来“算出”对该单词解释的含义。如果词典编纂得完美无瑕, 那么这个过程就能够终止; 如果其中一个单词没有查到或是循环定义(解释), 那么这个过程则循环不定。

打印输出整数

设有一个正整数n并希望把它打印出来。我们的例程的名字为printOut(n)。假设仅有的现成I/O例程将只处理单个数字并将其输出到终端。我们为这种例程命名为printDigit; 例如, printDigit(4)将输出4到终端。

递归将为该问题提供一个非常漂亮的解。要打印76234, 我们首先需要打印出7623, 然后再打印出4。第二步用语句printDigit(n%10)很容易完成, 但是第一步却不比原问题简单多少。它实际上是同一个问题, 因此可以用语句printOut(n/10)递归地解决它。

这告诉我们如何去解决一般的问题, 不过我们仍然需要确认程序不是循环不定的。由于我们尚未定义一个基准情况, 因此很清楚, 我们仍然还有些事情要做。如果0≤n<10, 那么基准情形就是printDigit(n)。现在, printOut(n)已对每一个从0到9的正整数定义, 而更大的正整数则用较小的正整数定义。因此, 不存在循环的问题。整个方法在图1-4中指出。

我们没有努力去高效地做这件事。我们本可以避免使用mod例程(它是非常耗时的), 因为n%10=n-n/10*10。 x是小于或等于x的最大整数。

递归和归纳

让我们多少严格一些地证明上述递归的整数打印程序是可行的。为此, 我们将使用归纳法证明。

定理1.4

递归的整数打印算法对n≥0是正确的。

证明(通过对n所含数字的个数, 用归纳法证明之):

首先, 如果n只有一位数字, 那么程序显然是正确的, 因为它只是调用一次printDigit。然后, 设printOut对所有k个或更少位数的数均能正常工作。我们知道k+1位数字的数可以通过其前k位数字后跟一位最低位数字来表示。但是前k位数字形成的数恰好是n/10, 由归纳假设它能够被正确地打印出来, 而最后的一位数字是n mod 10, 因此该程序能够正确打印出任意k+1位数字的数。于是, 根据归纳法, 所有的数都能被正确地打印出来。

这个证明看起来可能有些奇怪, 但它实际上相当于是算法的描述。证明阐述的是在设计递归程序时, 同一问题的所有较小实例均可以假设运行正确, 递归程序只需要把这些较小问题的解(它们通过递归奇迹般地得到)结合起来形成现行问题的解。其数学根据则是归纳法的证明。由此, 我们给出递归的第三个法则:

3.设计法则(design rule)。假设所有的递归调用都能运行。

这是一条重要的法则, 因为它意味着, 当设计递归程序时一般没有必要知道簿记管理的细节, 你不必试图追踪大量的递归调用。追踪具体的递归调用的序列常常是非常困难的。当然, 在许多情况下, 这正是使用递归好处的体现, 因为计算机能够算出复杂的细节。11

递归的主要问题是隐含的簿记开销。虽然这些开销几乎总是合理的(因为递归程序不仅简化了算法设计而且也有助于给出更加简洁的代码), 但是递归绝不应该作为简单for循环的代替物。我们将在3.6节更仔细地讨论递归涉及的系统开销。

当编写递归例程时, 关键是要牢记递归的四条基本法则:

1. 基准情形。必须总要有某些基准情形, 它无需递归就能解出。

2. 不断推进。对于那些需要递归求解的情形, 每一次递归调用都必须要使状况朝向一种基准情形推进。

3. 设计法则。假设所有的递归调用都能运行。

4. 合成效益法则(compound interest rule)。在求解一个问题的同一实例时, 切勿在不同的递归调用中做重复性的工作。

第四条法则(连同它的名称一起)将在后面的章节证明是合理的。使用递归计算诸如斐波那契数之类简单数学函数的值的想法一般来说不是一个好主意, 其道理正是根据第四条法则。只要在头脑中记租些法则, 递归程序设计就应该是简单明了的。

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

51CTO读书频道二维码


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

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

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

读 书 +更多

SQL Server 2005奥秘

本书是作者深入研究SQL Server 2005数据库体系结构和内部机制的经验总结。 全书不拘泥于具体的管理操作,而是通过对存储的数据和日志文件...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊