`
Joard
  • 浏览: 28100 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论
阅读更多
   第一次写blog,答应朋友Leon,写一篇粗略地介绍“递归(recursion)”的文章。
   递归,一种古老但依旧实用的东东,很多算法用递归方法表示,用递归写的程序也很容易让人理解。
   递归的优美在于使得程序更简洁,同时也更自然。尤其是在处理层次性数据的时候,十分强大。当然,递归的缺点也十分明显--性能问题。在性能这个问题点上, 有一种手段能适当改善递归的性能--尾递归(尾递归的概念将在下文解释)。关于优缺点就此打住,还是很容易理解,我就不婆婆妈妈的罗嗦了,我直接切入正 题。
   
什么是递归?

    在这里我们区分一下几个概念“递归过程”和“递归计算过程”以及相对应的“迭代计算过程”。
“递归过程”通常是指某个过程(方法)的定义直接或间接地引用了该过程(方法)本身。在本文中如非特别说明,“递归”这个词泛指一个或某种“递归过程”,而在描述递归型计算过程的时候都会用“递归计算过程”。
下面是一个array求和的递归方法
ruby 代码
  1. def sum(arr)  
  2.   if arr.empty?  
  3.     0  
  4.   else  
  5.     arr.shift + sum(arr)  
  6.   end  
  7. end  

这是一个递归,同时也是递归计算过程的递归。来看看sum函数在1..5上的求值过程
ruby 代码
  1. [1,2,3,4,5]  
  2. 1 + sum([2,3,4,5])  
  3. 1 + (2 + sum([3,4,5]))  
  4. 1 + (2 + (3 + sum([4,5])))  
  5. 1 + (2 + (3 + (4 + sum([5]))))  
  6. 1 + (2 + (3 + (4 + (5 + sum[]))))  
  7. 1 + (2 + (3 + (4 + (5 + 0))))  
  8. 1 + (2 + (3 + (4 + 5)))  
  9. 1 + (2 + (3 + 9))  
  10. 1 + (2 + 12)  
  11. 1+ 14  
  12. 15  

通过这个求值过程,很容易看出sum函数有明显的展开和收缩(收敛)过程。类似这样先展开/后收缩(收敛)的推迟求值的过程就是“递归计算过程”。
    知道了什么是“递归计算过程”后,再来瞧瞧“迭代计算过程”。代码在这里是最佳的说明,我们稍微改变一下sum函数用迭代计算过程来求值。
ruby 代码
  1. def sum_iter(arr, resu = 0)  
  2.   if arr.empty?  
  3.     resu  
  4.   else  
  5.     resu += arr.shift  
  6.     sum_iter(arr, resu)  
  7.   end  
  8. end  

ps:sum_iter的写法有时候也会被称为尾递归写法。怎么又是尾递归?稍安毋躁,我发誓接下来我一定会说到“尾递归”的!

它在1..5上的求值过程如下
ruby 代码
  1. [1,2,3,4,5]  
  2. [  2,3,4,5] + 1  
  3. [    3,4,5] + 3  
  4. [      4,5] + 6  
  5. [        5] + 10  
  6. [         ] + 15  
  7. 15  

在这个计算过程中,平没有明显的展开和收缩(收敛)过程,在整个运行轨迹中我们只需保存变量resu。这种所有程序状态可以用固定个数的状态变量,以及一套固定的状态变量的修改规则就是“迭代计算过程”。相比递归计算过程,迭代计算过程有一个优点就是可以保存程序运行中的某个状态,然后下次运行的时候可以直接传递这个中间状态给该过程以恢复上次运算。你可以传递[4,5]和6给sum_iter,它仍然能计算出15,而你传递[4,5]给sum,结果是9。

线性递归和树形递归
    也许当你看到sum和sum_iter的时候,你更喜欢用for,foreach和while这样的循环来处理。实际上有一部分递归可以很容易地转换为循环,然而另一部递归却并不那么容易的进行转换。是否能容易转换为循环,取决于它是否是迭代/递归计算过程和线 性/树形递归。毫无疑问,拥有迭代计算过程的递归可以迅速转换为循环。迭代计算过程的递归和循环处理基本上是等价的,因此某个递归能不能转换为 迭代计算过程的递归也就决定了它能不能转换为循环处理。至于为什么要把递归转换为循环处理,可以看看【然而它工作得不过好】。
    现在我们可以回到这一节的主题线性/树形递归。性/树形递归这个其实很好理解,就像sum和sum_iter这样的程序的计算步骤是随参数arr而线性增长的,这种过程就是线性递归。线性递归还可以细分为线性递归计算过程和线性迭代计算过程的递归,分别对应sum和sum_iter。关于树形递归我就不多说了,可以很容易地联想到Tree这种结构。树形递归的也是先展开后收缩的,只不过它的展开后的求值过程是一棵树。这个不难联想到实际情况,例如自己写程序求某个目录下面的所有文件数或查找某个文件。但是这里有一点必须要注意。树形递归的效率比线性递归低得多。
例如,求fibonacci数
ruby 代码
  1. def fib(n)  
  2.   if n == 0 : 1  
  3.   elsif n == 1 : 1  
  4.   else  
  5.     fib(n-2) + fib(n-1)  
  6.   end  
  7. end  
  8.   
  9. def fib_iter(n, a = 1, b = 0)  
  10.   if n == 0 : a  
  11.   else   
  12.     fib_iter(n - 1, a + b, a)  
  13.   end  
  14. end  

这是在我机器上运行求fib(10)的结果,当求fib(100)的时候,已经慢得让我无法忍受了。
ruby 代码
  1. require 'benchmark'  
  2. include Benchmark  
  3.   
  4. bmbm(12) do |x|  
  5.   x.report("fib") { 1000.times { fib(10) } }  
  6.   x.report("fib_iter") { 1000.times { fib_iter(10) } }  
  7. end  
  8.   
  9.                  user           system       total           real  
  10. fib             0.265000   0.000000   0.265000 (  0.266000)  
  11. fib_iter      0.016000   0.000000   0.016000 (  0.016000)  

    造 成这么大的差异的原因很大一部分是因为重复计算,例如求fib(10)的时候会求解fib(9) + fib(8),然后求解fib(9)的时候仍然会再次求解fib(8)。当然这里有不少方法来提高树形递归的效率,但是其相对于线性递归的效率低下也是不 争的事实。这并不表明树形递归无用,在有些场合尤其是层次性数据的时候,非常有效。应为这种特殊场合比较难用线性递归或循环来处理,你需要懂懂脑子才能 (也许就想不出)办法把树形递归转换为线性递归或者循环处理。这里又不得不提性能,虽然我极力回避这个问题,但性能终究是一个问题。它使得你的程序运行的不那么好!
分享到:
评论
3 楼 leon_a 2007-07-02  
以前是只知其然而不知其所以然,今天受教了
2 楼 dennis_zane 2007-07-02  
呵呵,我怎么看着眼熟,就是sicp书中概念的ruby版本解释嘛,总结的很好
1 楼 Joard 2007-07-01  
然而它工作得不够好!
    上面说的那些“废话”就是递归的全部了吗?不,很遗憾,递归还有更多的东西。
    通常意义上递归会有结束条件。所谓结束条件就是告诉程序以及到达本次递归的最底层了,不必继续运行的条件。递归至少要包括一个结束条件,而且要确保程序能最终能到达某一个结束条件。不然的话,你的递归程序会因栈空间不足而中止(好吧,我承认也会因内存不足而中止)。 递归的主要开销还是在于函数调用和为了维护执行顺序所保存的中间状态,而这些信息又大多保存在栈空间中。
   “不忙,等着。你刚才不说迭代计算过程是用用固定个数的状态变量吗?”
   对,即便是迭代计算过程的递归它仍然会是消耗正比于函数调用次数的内存空间,而不是常量空间。于是聪明的cs牛人们试图找到解决这一缺陷的办法。

尾递归
    我一直在考虑是否要提到“尾递归(Tail-recursive)”,最主要的原因是我自己对尾递归也不是很了解,但既然是《粗看递归》,那么不提提尾递归怎么也说不过去。因此斗胆在这里王妄讲一下尾递归。(你看我没有骗你吧!终于到尾递归了)
    为了总能在常量内存空间中执行迭代计算过程,即使这一计算是递归过程描述的,具有这一特性的实现称谓“尾递归”(摘抄自SICP)。但是从我的手里的资料来看还有另一种定义:如果一个调用的返回值被作为该递归调用的值立即返回,那么,这个递归调用就是“尾递归”。前者更重实现,而后者侧重形式。在这里让我们回到sum_iter方法,还记得我在那里说其是尾递归写法吗?结合到这里的尾递归定义你也许能明白了吧!但这里要小心,即便是该语言不支持尾递归,sum_iter也仍然可以叫做尾
递归函数(这里是侧重尾递归这种代码形式)。这里又要罗嗦到Tail-call,什么是Tall-call,下面的方法就是区别
java 代码

   1. int inc(int a) { 
   2.     return a++ 
   3. } 
   4.  
   5. int TailCall(){            //尾调用
   6.     int a = 2; 
   7.    return inc(a); 
   8. } 
   9.  
  10. int NotTailCall(){          // 不是尾调用,因为它还要在inc返回後执行加一操作
  11.    int a = 2; 
  12.    return 1 + inc(a); 
  13. } 


综上所述,这些概念还是比较容易区分的。
    关于尾递归的实现,我们还是拿sum_iter来说事,当程序的控制权传递给尾部的sum_iter的时候,前一个sum_iter的栈空间其实已经没有用了,我们可以设法在调用下一个sum_iter的时候用下一个栈结构覆盖当前sum_iter的栈结构,并使得它们的返回值保持一致。总之,尾递归能提高拥有迭代计算过程的递归的效率,但递归计算过程却享受不了了。在下次你看见某个语言里面居然没有提供循环结构的时候,请不要惊讶,“哦,原来是尾递归”。
    像Lisp,(Erlang也支持尾递归)等函数语言里面是实现了“尾递归”的(尾递归的第一种定义)。但是很多语言并没有提供这种削除拥有迭代计算过程的递归调用的所产生的无用栈结构的特性。那么java提供尾递归吗?Eric Allen提供了一中测试方法来测试你的jvm,更准确的说是jit是否提供一定程度上的尾递归。Allen的测试代码如下
java 代码

   1. public class TailRecursionTest { 
   2.   private static int loop(int i) { 
   3.     return loop(i); 
   4.   } 
   5.   public static void main(String[] args) { 
   6.     loop(0); 
   7.   } 
   8. } 


如果jit把这个递归转换成迭代,那么这个程序会一直执行下去,并占用常量空间。如果不支持的话,那么你的程序最终会得到一个 java.lang.StackOverflowError。在SUN的Hotspot JVM上没有提供这种转换,而IBM的JVM上,该程序会稳定一直执行下去。(关于这点我没有考证,应为我没有IBM的JVM)

写在最后
谢谢各位看我罗嗦了这么久,如果你认为这文章太无聊太差劲了,但你又想获得一个更简洁但更准确的关于递归的描述。请参阅SICP的 1.2.1节和1.2.2节,你能通过这个链接看到http://mitpress.mit.edu/sicp/full-text/book/book -Z-H-11.html#%_sec_1.2.1
本文很多地方都是在再“抄袭”该书的观点,请多包含。

如果你对Recursive Function的理论感兴趣可以参考《Type Systems for Programming Languages》。
最后,如果你对尾递归的实现很感兴趣,可以阅读这篇文章http://www.ibm.com/developerworks/cn/linux/l-recurs.html该文简单地描述了在汇编级别来实现尾递归的方法。

相关推荐

    VB6采用递归思想绘制树的源码

    VB6采用递归思想绘制树的源码:粗细、分叉等等

    kahypar:KaHyPar(Karlsruhe超图分区)是一个多级超图分区框架,提供了直接的基于k途和递归二等分的分区算法,可计算出高质量的解决方案

    它既支持递归二等分又支持直接k路径分区。 作为多级算法,它包括三个阶段:在粗化阶段,对超图进行粗化以获得较小的超图的层次结构。 在对第二阶段的最小超图应用初始分区算法之后,取消粗化,并且在每个级别上,均...

    Java并发教程.md

    * [可重入锁(递归锁)](#可重入锁递归锁) * [偏向锁](#偏向锁) * [轻量级锁](#轻量级锁) * [自旋锁](#自旋锁) * [自适应自旋锁](#自适应自旋锁) * [锁消除](#锁消除) * [锁粗化](#锁粗化) * [死锁](#死锁) *...

    leetcode三角形打印-algorithm:leetcode刷题记录

    (非递归解法独自完成,着重看递归解法) 2020/11/18 二叉树深度遍历 (着重看非递归解法) 2020/11/17 二叉树的最大深度(104) (着重看非递归解法) 字符串 2020/11/18 数字格式化 判断同文异构 2020/11/11 KMP 2020/10...

    一篇文教你使用python Turtle库画出“精美碎花小清新风格树”快来拿代码!

    使用Turtle画树,看了一下网上的代码,基本上核心的方法是使用递归;其次通过递归传参更笔的粗细从而改变绘制时的线段,更改树的躯干大小,在遍历到最后一个节点时,更改笔的颜色及粗细,绘制出树尖的花瓣或绿叶。 ...

    麦卡锡函数和阿克曼函数

    《麦卡锡函数和阿克曼函数——从一道前南斯拉夫数学奥林匹克试题谈起》从一道前南斯拉夫数学奥林匹克试题谈起,以粗犷的线条,简明的介绍了麦卡锡函数、阿克曼函数及递归函数。通过对小试题的讨论,展示给读者一个...

    PHPMarkdown解析器HyperDown.zip

    列表(可递归) 引用(可递归) 缩进风格的代码块 Github风格的代码块 各种行内文字加粗,斜体等效果 链接,图片 自动链接 段内折行 脚标 分隔符 ...

    迷宫生成算法和迷宫寻径算法

    本源码通过C# GDI+ 编写。...迷宫大小、单元格大小、线粗均可自定义。优化了算法,递归改为栈实现,能够生成任意大地图而不会引起原来的函数递归栈溢出问题。生成迷宫后,支持键盘按键进行手动走迷宫。

    java数独代码

    数独是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行...现使用java编程解决数独问题,核心思想是利用递归进行穷举。

    quick-dp:快速使用动态编程

    同样,在计算机科学中,可以通过将其分解为子问题然后递归地找到子问题的最优解来最佳解决的问题被称为具有最优子结构。 -来自 安装 npm install quick-dp --save 例子 const DynamicProgramming = req

    CVPR-2020-Semi-Low-Light

    一方面,所提出的网络经过精心设计,可以提取一系列从粗到细的带表示形式,其估计在递归过程中是互惠互利的。 另一方面,在DRBN(递归带学习)的第一阶段中提取的增强图像的带表示形式弥合了配对数据的恢复知识与对...

    HyperDown:一个结构清晰的,易于维护的,现代PHP Markdown解析器

    为何要写这样一个解析器Markdown已经面世许多年了,国内外许多大大小小的网站都...当前支持的语法标题列表(可递归)引用(可递归)缩进风格的代码块Github风格的代码块各种行内文字加粗,斜体等效果链接,图片自动链接

    Python基于gevent实现文件字符串查找器

    1、递归遍历目录下所有文件并通过finder函数定位指定格式字符串 2、用来查找字符串的finder函数是自己定义的,这里定义了一个ip_port_finder通过正则表达式查找ip:port格式(粗匹配:数字.数字.数字.数字:数字)的...

    Subdivision:网格细分

    作为将每个多边形面细分为更接近光滑表面的更小面的递归过程的限制,可以从粗网格计算光滑表面。 我们称之为流程细分。 在这个项目中,我们专注于三角形网格,从基本的VRML 3D模型文件(.wrl)中读取输入数据,然后...

    FastDTW:FastDTW- 具有线性时间和内存复杂度的动态时间规整 (DTW)

    FastDTW 使用多级方法从较粗的分辨率递归地投影解决方案并细化投影的解决方案。 执行: FastDTW 是用 Java 实现的。 如果 JVM 堆大小不足以使成本矩阵适合内存,则实现将自动切换到磁盘成本矩阵。 还实施了以下列...

    基于两层模式的Web 服务工作流失效恢复算法 (2011年)

    通过细粒度的执行事务模式进行事务实例级的层次式递归恢复,采用粗粒度的全局事务工作流模式进行模型级的恢复,并用来满足客户的个性需求。该算法能够在保证用户需求的基础上动态确定补偿终止点,可有效减小补偿域,...

    6107-8111-matematicadiscreta:由 FIUBA 学生为离散数学学科(计算机工程师和系统分析学位)创建的数字笔记

    6107-8111-离散数学 由 FIUBA 学生为离散数学学科(计算机工程师和系统分析学位)创建的数字笔记 ... 递归方程。 布尔代数和开关电路。 图、树和传输网络理论的要素。 加粗的为新增内容,不在备注中,请合作添加。

    leetcode中国-CoolCode::rabbit_face:GOGOGO!!!算法解题实践

    递归 上面是属于基础知识,下面的属于高等进阶知识 :hot_beverage:如何真的对你有帮助,你可以请我喝杯咖啡,这样我就可以有精神了,多谢多谢 可支持 支付宝支付, 个人提子使用版本 2刀/月,1T流量 ,最高1Gb带宽, ...

    dev-examples

    文本变粗时的偏移量(文件/演示) 滚动元素时阻止页面滚动(文件/演示) 按行数截断文本(“文件” /“ 演示” ) JavaScript 递归 阶乘(文件/ 演示) 奇偶校验(文件/ 演示) 求幂(文件/ 演示) 求和数字...

    sudoku-player:数独游戏生成&求解器

    [TOC] 任务总体目标:实现一个能够生成数独游戏并求解数独问题的控制台程序 具体要求 ...回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况: 找到一个可能存在的正确的答

Global site tag (gtag.js) - Google Analytics