我一直在试图弄清楚这种递归方法的堆栈是什么样的。
public class Apples {
public static void main (String [] args) {
q1(5);
}
public static int q1 (int x) {
if (x < 1) {
System.out.println(x);
return 1;
}
int a = 3 + q1(x / 2);
int b = 2 * q1(x - 2) + 1;
System.out.println(x + ";" + a + ";" + b);
return a + b;
}
}
但到目前为止,我只认为堆栈按照x / 2增长:
x=0 returns 1;
x=1 a=4 b=3 returns 7;
x=2 a=10 b=3 returns 13;
x=5 a=16 b=9 returns 19;
这显然既不真实也不完整。请帮我理解堆栈是如何构建的。
答案 0 :(得分:11)
<强>理论强>:
每次,此函数将首先递归q1(x/2)
路径,直到达到结束条件。然后,将处理所有待处理的q1(x-2)
调用。现在,这是我们第一次处理q1(x-2)
的每个q1(x/2)
时变得棘手的地方。因此,我们现在回到与以前相同的位置,只有一层向下,重复直到我们处理所有q1(x-2)
次调用(在最后q1(x/2)
层中)。
想到它的一种方式就像一棵树:
我只认为堆栈根据x / 2
增长
你是对的,如果你的意思是这个函数在q1(x/2)
中比在q1(x-2)
方向上更快地递归。尽管如此,你所说的意味着它以 lg(n)的方式增长( lg(n)是基础2)。
但是,我们仍然需要分析其他堆栈帧,因此我们设置以下递归关系:
T(n)= T(n / 2)+ T(n-2)+ c1
答案 1 :(得分:4)
由于为计算a
的值而进行的重复递归调用,此递归函数的堆栈最初会增长;也就是说,我们将继续调用q1(x/2)
直到x/2 < 1
,在这种情况下,我们已经达到了递归的基本情况,并且可以简单地返回1.
每次我们从最初的q1(x/2)
来电之一返回时,我们都必须按照q1(x-2)
调用来计算b
。该递归调用还将对a
进行一系列连续的递归调用(因为a
首先在函数中计算),这遵循相同的规则;在每个返回之后,我们对b
进行递归调用,并且这个过程重复进行,直到我们到达所有调用分支的基本情况。
这里是堆栈的外观。读取它的顺序是首先按照垂直箭头,返回,然后按照对角线箭头。按照斜箭头后重复此过程。如果没有箭头,请返回。
顺便说一下,返回函数的堆栈帧将被完全取消分配,并且一个新的函数调用(如果有的话)将取代它。您可以看到,在任何给定时间,活动的堆栈帧数不超过4个。当最后一个最顶层的堆栈帧完成时,它被解除分配,并且它的位置由下面和右边的堆栈获取。你从那里回来,依此类推......
希望这个图有助于清理它。
| | |
| | |
| | |
+--------+ | | |
| a = | | | |
| b = | | | |
+--------+ | | |
| x = 0 |
+--------+
returns 1
^ +--------+
| | a = |
| | b = |
| +--------+
| / | x = -1 |
| / +--------+
| / returns 1
| /
+--------+ /
| a = 4 | /
| b = 3 |/
+--------|
| x = 1 |
+--------+
returns 7
^ +--------+
| | a = |
| | b = |
| +--------+
| / | x = 0 |
| / +--------+
| / returns 1
| /
+--------+ /
| a = 10 | /
| b = 3 |/
+--------+
| x = 2 |
+--------+
returns 13
^ +--------+
| | a = |
| | b = |
| +--------+
| | x = 0 |
| +--------+
| returns 1
|
| ^ +--------+
| | | a = |
| | | b = |
| | +--------+
| | / | x = -1 |
| | / +--------+
| | / returns 1
| | /
| +--------+ /
| | a = 4 | /
| | b = 3 |/
| +--------+
| | x = 1 |
| +--------+
| returns 7
|
| ^ +--------+
| | | a = |
| | | b = |
| | +--------+
| | | x = 0 |
| | +--------+
| | returns 1
| |
| | ^ +--------+
| | | | a = |
| | | | b = |
| | | +--------+
| | | / | x = -1 |
| | | / +--------+
| | | / returns 1
| | | /
| | +--------+ /
| | | a = 4 | /
| | | b = 3 |/
| | +--------+
| | / | x = 1 |
| | / +--------+
| | / returns 7
| | /
| | /
| +--------+ /
| | a = 10 |/
| | b = 15 |
| +--------+
| / | x = 3 |
| / +--------+
| / returns 25
| /
+--------+ /
| a = 16 | /
| b = 51 |/
+--------+ | | |
| x = 5 | | | |
+--------+ | | |
returns 67 | | |
| | |
| | |
| | |
Brofessor有一个很好的理论方法,但他说的有点不准确;当他说q1(x/2)
的速度比q1(x-2)
更快时,他意味着前者与后者相比很快会达到基本情况。考虑大于5的数字。对于x
的大值,x/2
远小于x-2
。因此,x-2
情况最终会产生比x/2
情况更多的递归调用,因此x-2
调用支配堆栈的增长。
例如,q1(64)
将对q1(x/2)
(64 / 2,32 / 2,...,1/2 = 0)进行7次递归调用。但它会有更多的递归调用q1(x-2)
(64-2,62-2,60-2,...,2-2 = 0)。
在他的绘图中,如果正确的子树更大,那么更现实的是,因为该子树将需要更长时间才能达到最低点。实际上,您可以在我的图表中看到这一点。如果您将垂直和对角线箭头视为树的分支,则使用x/2
的第一个递归调用的子树只有5个节点,而使用x-2
的第一个递归调用的子树有7个节点节点。几乎总是如此。
答案 2 :(得分:2)
要知道实际的通话转换以q1(0), q1(1)...
我可以帮助您q1(2)
,然后您可以轻松尝试q1(5)
。
x = -1, q1(-1) => 1 // "q1(-1) => 1" means q1 returns 1
x = 0, q1(0) => 1 // "q1(0) => 1" means q1 returns 1
x = 1, a = 3 + q1(0) = 3 + 1 = 4
b = 2 * q1(-1) + 1 = 2*1 + 1 = 3
q1(1) => 7
x = 2, a = 3 + q1(1) = 3 + 7 = 10
b = 2 * q1(0) + 1 = 2*1 + 1 = 3
q1(2) => 13
...
因此,您可以打印q1(2)
,然后输出13
。调试将帮助您更好地理解。