我想问是什么帮助您更好地掌握了回溯的概念。
为了掌握总体思路,它帮助我观看了使用回溯的解决方案的可视化(视频或带有可视化的页面)。
为了掌握调用和递归调用的工作原理,我只是通过调试做了很多步骤并观看了一些可视化。
从您添加到代码中的 cmets,我可以看出,您已经掌握了回溯的一般概念以及为什么存在这些条件和递归调用。
那么计算机(并非特定于 Java)如何执行调用和递归调用,更重要的是,它们如何跟踪调用完成后返回的位置?
他们使用调用堆栈。来自维基百科
调用堆栈用于多个相关目的,但使用调用堆栈的主要原因是跟踪每个活动子例程在完成执行时应返回控制权的点。活动子程序是一个已被调用但尚未完成执行的子程序,此后控制权应交还给调用点。子程序的这种激活可以嵌套到任何级别(递归作为一种特殊情况),因此是堆栈结构。
调用堆栈通过在每次调用发生时将堆栈帧推入(添加)堆栈帧并在活动调用返回时弹出(删除)它们来跟踪仍在进行中的调用。
堆栈帧包含以下信息(简化):
- 调用的参数值
- 返回地址 = 调用完成后要返回的代码位置
- 被调用方法的局部变量,上下文/作用域
堆栈和堆栈帧使递归调用成为可能。
我知道您已经使用调试器在代码执行时单步执行代码,但让我们在“纸”上再做一次。
我将在您的代码中使用行号(我还删除了 cmets),以便更轻松地引用这些行。将被递归调用的方法有 5 行代码。
boolean groupSum(int start, int[] nums, int target) {
1: if (start >= nums.length) return (target == 0);
2: if (groupSum(start + 1, nums, target - nums[start])) return true;
3: if (groupSum(start + 1, nums, target)) return true;
4: System.out.println("test"); // Why does it reach that point?
5: return false;
}
我们以拨打groupSum(0, [2, 4, 8], 9) 为例。
当您的代码的某些部分调用groupSum(0, [2, 4, 8], 9) 时,它将将此调用推送到调用堆栈(创建堆栈框架)并开始执行方法体。
Stack frame 1 created: groupSum(0, [2, 4, 8], 9)
Parameters: start = 0, target = 9, nums = [2, 4, 8]
// Line 1 condition is false, so it keeps going
// Line 2 calls groupSum(0 + 1, [2, 4, 8], 9 - 2)
// which pushes a new stack frame on the call stack
Stack frame 2 created: groupSum(1, [2, 4, 8], 7)
Parameters: start = 1, target = 7, nums = [2, 4, 8]
// This creates a local scope (context) for frame 2.
// Frame 1 is isolated from changes to variables in Frame 2
// Line 1 condition is false, so it keeps going
// Line 2 calls groupSum(1 + 1, [2, 4, 8], 7 - 4)
// which pushes a new stack frame on the call stack
Stack frame 3 created: groupSum(2, [2, 4, 8], 3)
Parameters: start = 2, target = 3, nums = [2, 4, 8]
// Line 1 condition is false, so it keeps going
// Line 2 calls groupSum(2 + 1, [2, 4, 8], 3 - 8)
// which pushes a new stack frame on the call stack
Stack frame 4 created: groupSum(3, [2, 4, 8], -5)
Parameters: start = 3, target = -5, nums = [2, 4, 8]
// Line 1 condition is true. So the method returns (-5 == 0),
// which means it returns false.
这是第一次实际回溯发生的地方。它已经到达数组的末尾,但它没有找到有效的解决方案,因为 -5 不是 0,所以它返回并返回调用堆栈 - 它回溯。
return 语句从调用堆栈中弹出一个帧,并在返回地址处继续执行代码 - 在代码中调用我们现在返回的方法的点处。
在这种情况下,它弹出第 4 帧,恢复第 3 帧的局部范围(恢复所有局部变量的值),并在第 2 行(第 3 帧)继续执行。 这使得回溯“工作”,因为它在调用站点恢复本地范围的先前状态,然后从那里继续执行。
Stack frame 3 restored:
Parameters: start = 2, target = 3, nums = [2, 4, 8]
// Line 2 condition is false (because false was returned) so it keeps going
// Line 3 calls groupSum(2 + 1, [2, 4, 8], 3)
// which pushes a new stack frame on the call stack
Stack frame 4 created: // this is a completely new frame 4,
// has no relation to frame 4 from before
Parameters: start = 3, target = 3, nums = [2, 4, 8]
// Line 1 condition is true, it returns (3 == 0), so it returns false.
这里再次回溯。这会再次从调用堆栈中弹出一个帧,因此第 3 帧的范围被恢复,但这一次它继续执行第 3 行
Stack frame 3 restored:
Parameters: start = 2, target = 3, nums = [2, 4, 8]
// Line 3 condition is false (because false was returned) so it keeps going
// Line 4 prints out "test". This is when and how this line is executed.
// Line 5 returns false.
这里也是回溯,因为两条调用路径(来自第 2 行和第 3 行的调用)都没有从当前状态找到解决方案,都不得不回溯。
这会从调用堆栈中弹出一个帧,因此第 2 帧的范围被恢复,它在第 2 行继续。
Stack frame 2 restored:
Parameters: start = 1, target = 7, nums = [2, 4, 8]
// Line 2 condition is false (because false was returned), so it keeps going
// Line 3 calls groupSum(1 + 1, [2, 4, 8], 7)
// which pushes a new stack frame on the call stack
创建堆栈帧 3,整个过程重复,帧 4 的目标 7 - 8 = -1 和 7 的数字略有不同,但结果相同(返回 false,因此回溯)。
它在第 4 帧第 4 行再次打印“test”,并在第 5 行返回 false
然后第 3 帧第 4 行打印“test”并在第 5 行返回 false(回溯),第 2 帧在第 3 行再次恢复
Stack frame 2 restored:
Parameters: start = 1, target = 7, nums = [2, 4, 8]
// Line 3 condition is false (because false was returned) so it keeps going
// Line 4 prints "test"
// Line 5 returns false (backtracks)
Stack frame 1 restored:
Parameters: start = 0, target = 9, nums = [2, 4, 8]
// Line 2 condition is false, so it keeps going
// Line 3 repeats the whole thing with frames 2, 3 and 4,
// outcomes are still false (backtracks a few times),
// so after a few more "test" are printed, frame 1 is restored again,
// but this time on line 3 and since the returned value is false,
// the condition is false, so it keeps going
// Line 4 prints "test
// Line 5 returns false (backtracks)
此时从调用堆栈中弹出第 1 帧,并将执行返回到方法 groupSum(0, [2, 4, 8], 9) 的原始调用者,因此我们可以说“根帧”(第 0 帧)被恢复并且值false 被调回原来的调用者。
整个执行的结果是false,这是意料之中的,因为2, 4, 8中没有加到9的组和。
编辑:这是另一个可能会派上用场的提示(也许当您提出回溯算法时)。在 (Java) IDE 中,当调试器在断点处停止时,您还可以在调试器窗口中看到当前调用堆栈的帧以及当前范围内的变量值。您还可以选择其他框架并查看调用开始的位置以及该框架范围内的变量值。一些 IDE 甚至在代码中内联显示局部变量(如下图所示)。
这是来自 IDEA 社区版的示例