计划递归解决问题的最佳方法是啥?

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

这几乎就是我的递归秘诀:停止条件是什么?将其作为输入递归函数后的第一条语句。

【讨论】:

以上是关于计划递归解决问题的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在Java中递归反转字符串的最佳方法是啥?

使用传递给 dplyr::filter 的参数创建一个函数 解决 nse 的最佳方法是啥?

将已解决的承诺值传递到最终的“then”链的最佳方法是啥[重复]

设计生成所有 n 位数字组合的递归函数的最佳方法是啥?

使用 C# 和 SQL 解决 datagridview 中重复条目的最佳方法是啥?

递归思维的算法是啥? (关于具体例子)