Language agnostic 规划问题递归解决方案的最佳方法是什么?

Language agnostic 规划问题递归解决方案的最佳方法是什么?,language-agnostic,data-structures,recursion,Language Agnostic,Data Structures,Recursion,我正在学习递归。我用递归解决了其他一些问题,比如创建二叉树、河内塔等等。因此,我理解递归是什么,但我发现自己很难规划和实现正确的递归解决方案 对于规划、思考或实施问题的递归解决方案,有什么一般性的建议吗?基本上,只需考虑两件事: 这个问题是否可以用相同(或类似)的问题来表示,但要使用“更简单”的参数 是否有一个明确的点,使这个更容易的问题变得微不足道 您会发现这些属性适用于所有经典递归算法,如二进制搜索、树遍历、排序/合并、阶乘计算、最大公分母计算等(a) 如果这两个条件都满足,问题可能适合

我正在学习递归。我用递归解决了其他一些问题,比如创建二叉树、河内塔等等。因此,我理解递归是什么,但我发现自己很难规划和实现正确的递归解决方案


对于规划、思考或实施问题的递归解决方案,有什么一般性的建议吗?

基本上,只需考虑两件事:

  • 这个问题是否可以用相同(或类似)的问题来表示,但要使用“更简单”的参数
  • 是否有一个明确的点,使这个更容易的问题变得微不足道
您会发现这些属性适用于所有经典递归算法,如二进制搜索、树遍历、排序/合并、阶乘计算、最大公分母计算等(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
函数并没有很快地减少搜索空间,这就是为什么它不适合递归

阶乘1也不会迅速减少搜索空间,因为它会从每次递归调用的参数中减去一个(类似于
add


然而,人们仍然使用它,可能是因为在递归调用的数量产生影响之前(64位无符号整数只能容纳20个左右),阶乘的存储空间就已经用完了。

通常,为了设计递归算法,您需要处理几个任务。我将以河内塔问题为例,因为它非常符合这一要求

首先也是最重要的一点是,确保您可以从递归定义的角度看到问题本身。具体来说,您希望确定如何将整个问题描述为在一个类似的、较小的子问题上加上固定的工作量

对于河内塔问题,移动一个大小为N的塔与移动N-1和一个圆盘的塔基本相同,这是一个非常简单的例子。但是,如果不知道解决方案,就无法立即看出哪个磁盘应该是N+1;顶部或底部。我们需要更多的信息

下一部分,实际上是上述问题的子集,是考虑终止条件;您必须知道何时停止递归。如果您在算法中错过了这一步,您将陷入无限循环或数据结构溢出

移动大小为1的塔与移动单个磁盘完全相同;没有理由重复。换一种说法,移动一座大小为零的塔等于什么都不做,你可以完全跳过它

最后,;你必须确定你的问题定义的不变量,以便指导你实际工作的方式。它基本上归结为找到算法必须做的事情,使较小的子问题看起来确实像较大的问题,然后仅在这些条件下递归

河内塔的具体要求是,不允许任何圆盘停留在较小圆盘的顶部。换句话说,你不允许把塔放在比塔的底部圆盘小的圆盘上。关于这个问题的一些更多的推理将使我们得出这样的结论:如果我们在中间某个点分裂塔,并将分裂下面的圆盘重新排列成任意的,但有效的排列,那么分裂上面的塔可以在这些重新排列的盘的任何一个上面,因为每一个都必须比分割时的磁盘大。在拆分上方重新排列磁盘时,不能出现类似情况;顶部磁盘的顶部不允许任何内容

总的来说,这意味着我们必须自下而上地工作;在底部圆盘上划分塔。这也意味着退化情况,n=1,正在移动顶部磁盘。因此,递归算法是递归地将N-1个磁盘移到一边,将第N个磁盘移到目标位置,然后将N-1个磁盘移到目标位置


如果这还不足以作为指导,那么您可能想问一个更具体的问题

递归是关于在解决问题的过程中识别“自相似性”。一个典型的递归例子,计算正整数的阶乘,很好地说明了这个过程

因为阶乘,
n,定义为
n*(n-1)*(n-2).*1
,您应该能够看到

n!=n*(n-1)

换句话说,n的阶乘是“n乘以(n-1)的阶乘”

如果您能够理解该语句,以及它如何表现出“自相似”行为,那么您已经做好了处理递归的准备。编程递归的关键是确定何时停止,而不是执行递归调用。在阶乘的情况下,当您试图确定的数字t
public static int factorial(int n)
{
    if (n == 1)//I'm done
        return 1;
    return n * factorial(n - 1); //continue with the recursion
}