计划递归解决问题的最佳方法是啥?
Posted
技术标签:
【中文标题】计划递归解决问题的最佳方法是啥?【英文标题】:What is the best way to plan a recursive solution to a problem?计划递归解决问题的最佳方法是什么? 【发布时间】:2011-11-10 23:20:15 【问题描述】:我正在学习递归。我已经使用递归解决了一些其他问题,例如创建二叉树、河内塔等。所以,我了解递归是什么,但我发现自己难以规划和实施正确的递归解决方案。
对于规划、思考或实施问题的递归解决方案,是否有任何一般性提示?
【问题讨论】:
【参考方案1】:基本上,只考虑两件事:
能否用相同(或相似)的问题来表达问题,但使用“更简单”的参数? 是否有一个明确的点可以让这个更简单的问题变得微不足道?您会发现这些属性适用于所有经典递归算法,例如二分搜索、树遍历、排序/合并、阶乘计算、最大公分母计算等等(a)。
如果满足这两个条件,则问题可能适合递归解决方案。
我说“可能”是因为即使是表现出这些属性的问题也并不总是适合递归,例如:
// Add two unsigned inetegers.
unsigned int add (unsigned int a, unsigned int b)
if (a == 0)
return b;
return add (a - 1, b + 1);
虽然这是一个有点有效的递归解决方案(尽管有点做作),但当您的初始 a
很大时,您几乎肯定会用完堆栈空间。换言之,现实世界会影响数学思想的纯洁性。
(a) 你可能想知道为什么上面的add
和 GCD 或阶乘计算之间存在差异。答案通常在于每次递归调用减少“搜索空间”(所有可能结果的列表)的速度。
例如,遍历平衡二叉树将在每次调用时消除大约一半的剩余搜索空间。 GCD 计算执行模运算,这也可以相当快地减少搜索空间。
但是,add
函数根本不会很快减少搜索空间,这就是它不适合递归的原因。
阶乘也不会很快减少搜索空间,因为它会从每次递归调用的参数中减去一个(类似于add
)。
但是人们仍然使用它,可能是因为在递归调用的数量产生影响之前,您将用完阶乘 long 的存储空间(64 位无符号整数只能保持20!)。
【讨论】:
我认为您对“更简单的参数”的描述没有抓住重点。更好的描述可能是参数应该降低计算的复杂性。这很好地导致了您的第二点......当复杂性变得“微不足道”时,答案就很清楚(通常是硬编码)并且消除了递归的需要,从而结束了递归调用。 这很好地解释了使用递归时的内存使用情况。 +1。【参考方案2】:通常,您需要完成几项任务来设计递归算法。我将以河内塔问题为例,因为它非常符合要求。
首先,确保您可以根据递归定义来查看问题本身。具体来说,您想确定如何将整个问题描述为对类似的、较小的子问题加上固定的工作量进行操作。
对于河内的塔问题,移动大小为 N 的塔与移动 N-1 的塔和单个圆盘基本相同。但是,在不知道解决方案的情况下,这并不是很明显,哪个磁盘应该是 N+1;无论是顶部还是底部。我们需要更多信息。
下一部分,实际上是上述问题的一个子集,是考虑终止条件;你必须知道什么时候停止递归。如果您在算法中错过了这一步,您最终会陷入无限循环或超出您的数据结构。
移动大小为 1 的塔与移动单个磁盘完全一样;没有理由递归。换句话说,移动一个大小为零的塔就等于什么都不做,你可以完全跳过它。
最后;您必须确定问题定义的不变量,以指导您实际工作的方式。它基本上归结为找到您的算法必须做的事情,以便较小的子问题确实看起来像较大的问题,然后仅在这些条件下递归。
河内塔的具体要求是不允许任何圆盘放在较小的圆盘上。换句话说,您不能将 塔 放置在比该塔的底部磁盘更小的磁盘上。关于这个问题的更多推理将导致我们得出这样的结论:如果我们在中间的某个点拆分塔,并将拆分下方的磁盘重新排列为任意但有效的排列,那么拆分上方的塔可以在顶部这些重新排列的磁盘中的任何一个,因为每个磁盘都必须大于拆分时的磁盘。不能对拆分上方的磁盘进行重新排列的类似情况;顶部磁盘上根本不允许任何内容。
总的来说,这意味着我们必须自下而上;在底部盘处划分塔。这也意味着退化的情况,n=1,正在移动顶部的磁盘。因此递归算法是递归移动N-1个磁盘到一边,将第n个磁盘移动到目的地,然后将N-1个磁盘移动到目的地。
如果这还不够指导,那么您可能想问一个更具体的问题
【讨论】:
【参考方案3】:递归就是在解决问题的过程中识别“自相似性”。递归的一个典型例子,计算一个正整数的阶乘很好地展示了这个过程。
由于阶乘,n!
,被定义为n * (n-1) * (n-2) ... * 1
,你应该可以看到
n! = n * (n-1)!
换句话说,n 的阶乘是“(n-1) 的阶乘的 n 倍”。
如果您能理解该语句,以及它如何表现出“自相似”行为,那么您就可以很好地解决递归问题。 编程 递归的关键是确定何时停止,而不是执行递归调用。在阶乘的情况下,当您尝试确定阶乘的数字为 1 时停止。结果被简单地定义为 1,因此您返回该值而不是返回递归函数调用的值。
因此,在考虑如何递归解决问题时,我的建议是尝试识别手头问题中的这种自相似性。如果您可以轻松识别这种相似性,那么该问题可能具有高效且优雅的递归解决方案。如果这种自相似性不明显,它可能更适合迭代方法。
【讨论】:
感谢您的建议!我理解基本条件的概念,但你解释得很好。【参考方案4】:一旦确定问题可以递归解决,最重要的事情之一就是确定递归算法的停止条件。一个简单的例子是阶乘的计算:你知道当你到达 0 或 1 时你应该停止(无论你选择什么),因此这应该是你在输入函数之前检查的第一件事,如果你不允许递归继续'不想以堆栈溢出异常结束:
public static int factorial(int n)
if (n == 1)//I'm done
return 1;
return n * factorial(n - 1); //continue with the recursion
这几乎就是我的递归秘诀:停止条件是什么?将其作为输入递归函数后的第一条语句。
【讨论】:
以上是关于计划递归解决问题的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章
使用传递给 dplyr::filter 的参数创建一个函数 解决 nse 的最佳方法是啥?
将已解决的承诺值传递到最终的“then”链的最佳方法是啥[重复]