第一次写blog,答应朋友Leon,写一篇粗略地介绍“递归(recursion)”的文章。
递归,一种古老但依旧实用的东东,很多算法用递归方法表示,用递归写的程序也很容易让人理解。
递归的优美在于使得程序更简洁,同时也更自然。尤其是在处理层次性数据的时候,十分强大。当然,递归的缺点也十分明显--性能问题。在性能这个问题点上, 有一种手段能适当改善递归的性能--尾递归(尾递归的概念将在下文解释)。关于优缺点就此打住,还是很容易理解,我就不婆婆妈妈的罗嗦了,我直接切入正 题。
什么是递归?
在这里我们区分一下几个概念“递归过程”和“递归计算过程”以及相对应的“迭代计算过程”。
“递归过程”通常是指某个过程(方法)的定义直接或间接地引用了该过程(方法)本身。在本文中如非特别说明,“递归”这个词泛指一个或某种“递归过程”,而在描述递归型计算过程的时候都会用“递归计算过程”。
下面是一个array求和的递归方法
ruby 代码
- def sum(arr)
- if arr.empty?
- 0
- else
- arr.shift + sum(arr)
- end
- end
这是一个递归,同时也是递归计算过程的递归。来看看sum函数在1..5上的求值过程
ruby 代码
- [1,2,3,4,5]
- 1 + sum([2,3,4,5])
- 1 + (2 + sum([3,4,5]))
- 1 + (2 + (3 + sum([4,5])))
- 1 + (2 + (3 + (4 + sum([5]))))
- 1 + (2 + (3 + (4 + (5 + sum[]))))
- 1 + (2 + (3 + (4 + (5 + 0))))
- 1 + (2 + (3 + (4 + 5)))
- 1 + (2 + (3 + 9))
- 1 + (2 + 12)
- 1+ 14
- 15
通过这个求值过程,很容易看出sum函数有明显的展开和收缩(收敛)过程。类似这样先展开/后收缩(收敛)的推迟求值的过程就是“递归计算过程”。
知道了什么是“递归计算过程”后,再来瞧瞧“迭代计算过程”。代码在这里是最佳的说明,我们稍微改变一下sum函数用迭代计算过程来求值。
ruby 代码
- def sum_iter(arr, resu = 0)
- if arr.empty?
- resu
- else
- resu += arr.shift
- sum_iter(arr, resu)
- end
- end
ps:
sum_iter的写法有时候也会被称为尾递归写法。怎么又是尾递归?稍安毋躁,我发誓接下来我一定会说到“尾递归”的!
它在1..5上的求值过程如下
ruby 代码
- [1,2,3,4,5]
- [ 2,3,4,5] + 1
- [ 3,4,5] + 3
- [ 4,5] + 6
- [ 5] + 10
- [ ] + 15
- 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 代码
- def fib(n)
- if n == 0 : 1
- elsif n == 1 : 1
- else
- fib(n-2) + fib(n-1)
- end
- end
-
- def fib_iter(n, a = 1, b = 0)
- if n == 0 : a
- else
- fib_iter(n - 1, a + b, a)
- end
- end
这是在我机器上运行求fib(10)的结果,当求fib(100)的时候,已经慢得让我无法忍受了。
ruby 代码
- require 'benchmark'
- include Benchmark
-
- bmbm(12) do |x|
- x.report("fib") { 1000.times { fib(10) } }
- x.report("fib_iter") { 1000.times { fib_iter(10) } }
- end
-
- user system total real
- fib 0.265000 0.000000 0.265000 ( 0.266000)
- fib_iter 0.016000 0.000000 0.016000 ( 0.016000)
造 成这么大的差异的原因很大一部分是因为重复计算,例如求fib(10)的时候会求解fib(9) + fib(8),然后求解fib(9)的时候仍然会再次求解fib(8)。当然这里有不少方法来提高树形递归的效率,但是其相对于线性递归的效率低下也是不 争的事实。这并不表明树形递归无用,在有些场合尤其是层次性数据的时候,非常有效。应为这种特殊场合比较难用线性递归或循环来处理,你需要懂懂脑子才能 (也许就想不出)办法把树形递归转换为线性递归或者循环处理。这里又不得不提性能,虽然我极力回避这个问题,但性能终究是一个问题。它使得你的程序运行的不那么好!
分享到:
- 2007-07-01 20:59
- 浏览 3232
- 评论(3)
- 论坛回复 / 浏览 (2 / 5713)
- 查看更多
相关推荐
递归树是递归算法的一种可视化表现形式,它能帮助我们理解递归过程,尤其是对于那些涉及到分治策略的问题,如分形几何、搜索算法(如深度优先搜索DFS)和排序算法(如快速排序、归并排序)等。 递归树的绘制通常是...
递归分形树的实现是基于数学和编程技术,特别是递归算法的应用。递归是一种解决问题的方法,它通过将问题分解为更小的子问题来解决原问题。在分形树的上下文中,递归意味着每个树干会分成若干个更细的分支,这些分支...
在VB6(Visual Basic 6)编程环境中,利用递归思想来绘制树形结构是一种常见的图形编程技巧。递归是一种算法,它通过调用自身来解决问题或执行任务,每次调用都解决一个更小的问题,直到达到某个基本条件为止。在...
在给定的“易语言递归算法画树源码”中,我们可以深入探讨递归算法以及在图形图像处理中的应用。 递归算法是计算机科学中一种强大的解决问题的方法,它通过函数或过程调用自身来实现。在画树的过程中,递归通常被...
如果当前分支长度大于15,我们将调整笔的宽度(pensize),使得较粗的树枝看起来更像树干。 ```python if (branch_len - 15) t.pencolor('green') else: t.pencolor('black') new_pensize = int(branch_len /...
在解决某些问题时,如计算阶乘、遍历树形结构等,递归能提供简洁的解决方案,但也需要理解递归的基线条件和终止条件,防止无限递归。 在复习过程中,学生应该注重对算法复杂度的理解,包括时间复杂度和空间复杂度。...
深度学习,尤其是卷积神经网络(CNN)、深度置信网络(DBN)和深度递归神经网络(DRNN),为解决这一问题提供了新的思路。这些深度模型在图像识别和计算机视觉领域的成功应用,启发了它们在手写汉字识别领域的应用,...
这可能包括特征选择,通过相关性分析或递归特征消除等方法去除冗余和不相关的特征,提高模型的解释性和性能。同时,也可以进行特征可视化,帮助医生理解模型的决策过程。 最后,分类阶段是基于特征分析的结果,使用...
在.NET框架中,C#是一种常用的编程语言,用于开发各种应用程序,包括Windows桌面应用。在描述中提到的“C#实现DataGridView转换为Excel(包括图片和文本)”是指使用C#编写代码,将数据从DataGridView控件导出到...
它既支持递归二等分又支持直接k路径分区。 作为多级算法,它包括三个阶段:在粗化阶段,对超图进行粗化以获得较小的超图的层次结构。 在对第二阶段的最小超图应用初始分区算法之后,取消粗化,并且在每个级别上,均...
(非递归解法独自完成,着重看递归解法) 2020/11/18 二叉树深度遍历 (着重看非递归解法) 2020/11/17 二叉树的最大深度(104) (着重看非递归解法) 字符串 2020/11/18 数字格式化 判断同文异构 2020/11/11 KMP 2020/10...
2. **递归定义**:给出完整多重网格循环的递归定义。 3. **迭代算子与收敛性**:讨论迭代算子的构造及其对h不敏感的收敛特性。 4. **计算量与效率评估**:分析多重网格方法的计算成本及效率。 5. **扩展与变体**:...
3. **线条粗细**:为了增加视觉效果,可以调整线条的粗细,通常树干比树枝粗,而且随着距离根部越远,树枝越细。 4. **颜色处理**:颜色的选择和变化可以增加层次感和真实感。你可以根据距离根部的位置或分支的大小...
选项C `Public Function Fn(n as Integer) If n=0 Then Fn=1 Else Fn=Fn(n-1)*n End Function` 是一个正确的递归函数,因为它有一个明确的基本情况(n=0),并且每次递归调用都向基本情况靠近。 第59题,函数`Fun(x...
4. **循环与递归**:根据预定的迭代次数或递归深度,重复执行分形生成函数,不断生成新的分支,直至形成完整的分形树结构。 5. **显示与保存**:最后,使用MATLAB的`figure`和`saveas`函数展示生成的分形树图像,并...
《麦卡锡函数和阿克曼函数——从一道前南斯拉夫数学奥林匹克试题谈起》从一道前南斯拉夫数学奥林匹克试题谈起,以粗犷的线条,简明的介绍了麦卡锡函数、阿克曼函数及递归函数。通过对小试题的讨论,展示给读者一个...
递归是一种强大的编程技巧,但它也必须谨慎使用,因为不正确的递归可能会导致无限循环或栈溢出错误。 以上就是从给定文件中提取的六个PHP代码段的学习要点,它们覆盖了循环、日期处理、函数定义与调用、递归等核心...
5. 套迭代技术是核心,它通过在不同层次的网格间递归地进行限制、校正和延拓,确保各层间的协调工作,以达到快速收敛。 多重网格方法的优势在于其与网格尺寸无关的收敛性,即使在非常密集的网格上,迭代次数也不会...
根据 `branchLen` 的大小,我们选择不同的颜色('snow' 或 'lightcoral')和笔粗(pensize),然后通过 `t.forward(branchLen)` 移动海龟并绘制线条。接着,我们使用 `random` 模块生成随机角度,通过 `t.right()` ...