【问题标题】:Double Recursion in one method Java一种方法Java中的双重递归
【发布时间】:2016-06-16 18:28:40
【问题描述】:

我很确定我完全理解只有一次递归的方法是如何工作的。

Ex) 计算阶乘

public int factorial(int n){  //factorial recursion
    if(n==0){
        return 1;
    }
    else{
        return n*factorial(n-1);
    }
} 

对于这些方法,我什至可以想象堆栈中发生了什么,以及在每个堆栈级别返回什么值。

但每当我遇到带有双递归的方法时,噩梦就开始了。

下面是来自编码bat的双重递归的递归问题。

Ex) 给定一个整数数组,是否可以选择一组整数,使得该组总和为给定的目标?如果是,是的。如果不是,则为假。 您使用 3 个参数;起始索引 start,一个 int Array nums,目标 int 值 target

下面是这个问题的解决方案。

public boolean groupSum(int start, int[] nums, int target) {
    if (start >= nums.length) return (target == 0);
    if (groupSum(start + 1, nums, target - nums[start])) return true;
    if (groupSum(start + 1, nums, target)) return true;
    return false;
}

我对这个解决方案的理解是这样的。假设我得到了一个数组 {2,4,8},起始索引 = 0,目标值为 10。所以 (0,{2,4,8},10) 进入该方法,函数在

处被重新调用
if (groupSum(start + 1, nums, target - nums[start])) return true;

所以它变成了(1,{2,4,8},8),它会一遍又一遍地执行,直到开始索引达到
3。当它达到 3. 最后一层(?)的堆栈进入第二个递归调用。这就是我开始忘记正在发生的事情的地方。

谁能帮我分析一下?当人们使用双重递归时,(我知道它非常低效,在实践中,几乎没有人因为它的低效而使用它。但只是为了理解它。)他们真的能想象会发生什么吗?还是他们只是使用它希望基本案例和递归能够正常工作?我认为这通常适用于所有编写合并排序、河内塔算法等的人。

非常感谢任何帮助..

【问题讨论】:

  • 你在这里发明了术语。这不是“双重递归”——你所展示的是一个普通的递归。另外,就像一位智者曾经说过的那样,to understand recursion, you have to first understand recursion
  • 好吧,我想我还没有理解递归。所以请赐教。
  • 这不是“双重”递归,因为在 groupSum() 内对 groupSum() 的第二次调用不会使堆栈深度增加一倍。
  • 很抱歉选错了词。我认为这将是“双重”,因为它使用了两次递归。

标签: java recursion


【解决方案1】:

好吧,如果您想将其可视化,通常它有点像一棵树。您首先沿着一条路径穿过树直到结束,然后退一步并选择另一条路径(如果可能)。如果没有,或者您对结果感到满意,您只需后退一步,依此类推。

我不知道这是否对你有帮助,但当我学习递归时,我认为我的方法已经奏效了。 所以我想:太好了,所以基本上我的方法已经在工作了,但是我不能用相同的参数调用它,并且必须确保通过使用不同的参数为这些确切的参数返回正确的值。

如果我们举个例子: 起初我们知道,如果我们没有数字可以看左边,那么答案取决于目标是否为 0。(第一行)

现在我们如何处理剩下的?嗯……我们需要考虑一下。 想想第一个数字。在什么情况下它是解决方案的一部分?好吧,如果您可以使用其余数字创建 target-firstnumber 的话。因为当你添加第一个数字时,你就达到了目标。 所以你试着看看这是否可能。如果是这样,它是可以解决的。 (第二行)

但如果不是,第一个数字仍然可能对解决方案并不重要。因此,您必须再次尝试构建没有该编号的目标。 (第三行)

基本上就是这些了。

当然,要这样想,你需要两件事: 1.您需要相信您的方法已经适用于其他参数 2. 你需要确保你的递归终止。这是此示例中的第一行,但您应该始终考虑是否存在任何参数组合只会创建无限递归。

【讨论】:

    【解决方案2】:

    双重递归的想法是将问题分解为两个较小的问题。一旦您解决了较小的问题,您可以加入他们的解决方案(如合并排序中所做的那样)或选择其中一个 - 这在您的示例中完成,如果解决第一个较小的问题,则只需要解决第二个较小的问题没有解决全部问题。

    您的示例尝试确定输入nums 数组的子集是否为target 和。 start 确定当前递归调用考虑数组的哪一部分(当它为0时,考虑整个数组)。

    问题分为两个,因为如果存在这样的子集,它要么包含数组的第一个元素(在这种情况下,问题被简化为查找是否存在最后 ​​n-1 个元素的子集总和为target 减去第一个元素的值的数组)或不包含它(在这种情况下,问题简化为查找是否存在数组的最后 n-1 个元素的子集,其总和是target)。

    第一个递归处理子集包含第一个元素的情况,这就是为什么它会进行递归调用,以查找目标总和减去数组剩余 n-1 个元素中的第一个元素。如果第一个递归返回 true,则表示所需的子集存在,因此永远不会调用第二个递归。

    第二个递归处理子集不包含第一个元素的情况,这就是为什么它会进行递归调用,在数组的剩余 n-1 个元素中查找目标总和(这次是第一个元素不从目标总和中减去,因为第一个元素不包括在总和中)。同样,如果第二个递归调用返回 true,则表示所需的子集存在。

    【讨论】:

    • 我认为该函数可以有 1 个改进 - 它可以在第一行检查 target == 0return true,因为我们已经达到了目标 - 目前它不必要地循环整个数组。你怎么看?
    • 感谢您的回复。但是你能解释一下为什么它分为两个有或没有第一个元素吗?为什么不考虑其他元素?
    • @LookAtTheBigPicture 有很多方法可以将问题分解成更小的部分。您可以考虑前两个元素,然后将问题分为四个部分(基于前两个元素中的两个、没有或任何一个出现在具有目标总和的子集中)。不过,这会使代码更加复杂。
    • @Eran 这听起来可能很奇怪,我什至不确定我问的是否正确,但是......就像我在问题中所说的那样。当人们确实使用双重递归时。他们能想象底层会发生什么吗?以这个问题为例,我只是隐约知道它会把问题分成有/没有第一个元素的两部分,但并不确切知道会制作什么堆栈以及每个堆栈会返回什么。
    • @LookAtTheBigPicture 您不必遵循所有递归调用来了解正在发生的事情(尽管您可能希望通过一些小的输入来跟随它以说服自己它确实有效) .了解每个递归调用如何解决一个较小的问题就足够了,了解停止条件解决了边缘情况的问题(在您的示例中为空数组),并了解如何使用较小问题的解决方案来解决原始问题。
    【解决方案3】:

    试着像这样理解它:递归可以重写为一个while循环。其中 while 的条件是递归停止条件的否定。

    如前所述,没有什么叫做双重递归。

    【讨论】: