Understanding how recursive functions work

正如标题所解释的,我有一个非常基本的编程问题,我只是还不能理解。过滤掉所有的(非常聪明的)“为了理解递归,你必须首先理解递归。”各种在线帖子的回复我还没有完全理解。

了解到当面对不知道我们不知道的事情时,我们可能倾向于问错误的问题或者错误地问正确的问题。我将分享我的“想法”我的问题是希望有一个类似的观点的人可以分享一些知识,这将有助于为我打开递归灯泡!

下面是函数(语法是用 Swift 编写的) :

func sumInts(a: Int, b: Int) -> Int {
if (a > b) {
return 0
} else {
return a + sumInts(a: a + 1, b: b)
}
}

我们将使用2和5作为我们的论点:

println(sumInts(a: 2, b: 5))

答案显然是14,但我不清楚这个价值是如何实现的。

这是我的两个问题:

  1. 该函数将被递归调用,直到满足一个条件为止。这个条件是 a > b。当满足此条件时,返回0。乍一看,我希望返回值为0,这显然是不正确的。

  2. Printing out the value of 'a' on each iteration yields a value which I would expect: 2, 3, 4, 5 (at which point 5+1 > b which meets the first condition: a > b) but I still don't see how the value of 14 is achieved.

我的第一个想法是,类似于下面这样的事情正在神奇地发生:

var answer = a;
answer += a+1 until a > b;
return answer;

所以排除魔法的可能性,我什么都得不到。我很想知道到底发生了什么,而不仅仅是含蓄的。

如果有人能好心地解释一下这种函数在技术上发生了什么,为什么结果不是0,以及最终 a + sumInts(a: a + 1, b: b) = 14是如何产生的,我将永远欠你一个人情。

31929 次浏览

好好想想的困惑是源于认为它是“同一个函数”被调用了很多次。如果你认为它是“被调用的同一个函数的许多副本”,那么它可能会更加清晰:

Only one copy of the function ever returns 0, and it's not the first one (it's the last one). So the result of calling the first one is not 0.

对于第二点困惑,我认为用英语拼写递归会更容易些。读这一行:

return a + sumInts(a + 1, b: b)

as "return the value of 'a' plus (the return value of another copy of the function, which is the copy's value of 'a' plus (the return value of another copy of the function, which is the second copy's value of 'a' plus (...", with each copy of the function spawning a new copy of itself with a increased by 1, until the a > b condition is met.

当 a > b 条件为真时,你有一个(可能是任意的)长的函数副本堆栈,这些副本都在运行中,都在等待下一个副本的结果,以找出它们应该添加到‘ a’中的内容。

(edit: also, something to be aware of is that the stack of copies of the function I mention is a real thing that takes up real memory, and will crash your program if it gets too large. The compiler can optimize it out in some cases, but exhausting stack space is a significant and unfortunate limitation of recursive functions in many languages)

递归是一个很难理解的话题,我不认为我可以在这里完全做到这一点。相反,我将尝试着关注您在这里拥有的特定代码片段,并试着描述解决方案为什么工作的直觉和代码如何计算其结果的机制。

这里给出的代码解决了以下问题: 您需要知道从 a 到 b 的所有整数的和,包括。对于您的示例,您需要从2到5的数字之和,包括

2 + 3 + 4 + 5

当试图递归地解决一个问题时,第一步应该是弄清楚如何将问题分解成具有相同结构的较小的问题。假设你想把2到5的数字加起来。简化这个过程的一种方法是注意上面的总和可以重写为

2 + (3 + 4 + 5)

在这里,(3 + 4 + 5)恰好是3到5之间所有整数的总和,包括。换句话说,如果你想知道2到5之间所有整数的和,从计算3到5之间所有整数的和开始,然后加2。

So how do you compute the sum of all the integers between 3 and 5, inclusive? Well, that sum is

3 + 4 + 5

可以被认为是

3 + (4 + 5)

这里,(4 + 5)是4到5之间所有整数的总和,包括。所以,如果你想计算3到5之间所有数字的和包括4到5之间所有整数的和然后加3。

这是有规律的!如果要计算 a 和 b 之间的整数之和(包括 b) ,可以执行下列操作。首先,计算 a + 1和 b 之间的整数之和,包括。接下来,将 a 添加到总数中。你会注意到“计算 a + 1和 b 之间的整数之和,包括在内”碰巧与我们已经尝试解决的问题类似,只是参数略有不同。我们不是从 a 到 b (包含)计算,而是从 a + 1到 b (包含)计算。这是一个递归的步骤——为了解决更大的问题(“ sum from a to b,include”) ,我们将问题简化为一个更小的版本(“ sum from a + 1 to b,include”).

如果你看一下上面的代码,你会发现其中有这样一个步骤:

return a + sumInts(a + 1, b: b)

This code is simply a translation of the above logic - if you want to sum from a to b, inclusive, start by summing a + 1 to b, inclusive (that's the recursive call to sumInts), then add a.

Of course, by itself this approach won't actually work. For example, how would you compute the sum of all the integers between 5 and 5 inclusive? Well, using our current logic, you'd compute the sum of all the integers between 6 and 5, inclusive, then add 5. So how do you compute the sum of all the integers between 6 and 5, inclusive? Well, using our current logic, you'd compute the sum of all the integers between 7 and 5, inclusive, then add 6. You'll notice a problem here - this just keeps on going and going!

在递归问题解决中,需要有一些方法来停止简化问题,而是直接去解决它。通常,您会发现一个可以立即确定答案的简单情况,然后构建解决方案,以便在出现简单情况时直接解决这些问题。这通常称为 基本情况递归基数递归基数

那么这个问题的基本情况是什么呢?当你把从 a 到 b 的整数加起来的时候,如果 a 恰好比 b 大,那么答案就是0-在这个范围内没有任何数字!因此,我们将按以下方式构建解决方案:

  1. 如果 a > b,那么答案是0。
  2. 否则(a & le; b) ,得到如下答案:
    1. Compute the sum of the integers between a + 1 and b.
    2. 加上 a 得到答案。

现在,将这个伪代码与您的实际代码进行比较:

func sumInts(a: Int, b: Int) -> Int {
if (a > b) {
return 0
} else {
return a + sumInts(a + 1, b: b)
}
}

请注意,在伪代码中概述的解决方案和实际代码之间几乎完全存在一对一的映射。第一步是基本情况——如果要求得到一个空数值范围的和,那么得到的结果是0。否则,计算 a + 1和 b 之间的和,然后加上 a。

到目前为止,我只是给出了代码背后的一个高层次的想法。但你还有两个问题,非常好的问题。首先,如果函数说如果 a > b 返回0,为什么它不总是返回0?其次,14到底是从哪里来的?让我们依次看看这些。

Let's try a very, very simple case. What happens if you call sumInts(6, 5)? In this case, tracing through the code, you see that the function just returns 0. That's the right thing to do, to - there aren't any numbers in the range. Now, try something harder. What happens when you call sumInts(5, 5)? Well, here's what happens:

  1. You call sumInts(5, 5). We fall into the else branch, which return the value of `a + sumInts(6, 5).
  2. 为了让 sumInts(5, 5)确定什么是 sumInts(6, 5),我们需要暂停我们正在做什么,并打电话给 sumInts(6, 5)
  3. sumInts(6, 5) gets called. It enters the if branch and returns 0. However, this instance of sumInts was called by sumInts(5, 5), so the return value is communicated back to sumInts(5, 5), not to the top-level caller.
  4. sumInts(5, 5)现在可以计算 5 + sumInts(6, 5)返回 5,然后返回给顶级调用者。

注意这里的值5是如何形成的。我们从一通打给 sumInts的电话开始。这会发出另一个递归调用,并且该调用返回的值将信息传递回 sumInts(5, 5)。然后,对 sumInts(5, 5)的调用进行一些计算,并将一个值返回给调用者。

如果你用 sumInts(4, 5)试试,会发生这样的情况:

  • sumInts(4, 5)尝试返回 4 + sumInts(5, 5)。为此,它调用 sumInts(5, 5)
    • sumInts(5, 5) tries to return 5 + sumInts(6, 5). To do that, it calls sumInts(6, 5).
    • sumInts(6, 5)返回0到 < code > sumInts (5,5) .
    • 代码 > sumInts (5,5) now has a value forsumInts (6,5) , namely 0. It then returns5 + 0 = 5’
  • sumInts(4, 5)现在有一个 sumInts(5, 5)的值,即5,然后返回 4 + 5 = 9

换句话说,返回的值是通过一次汇总一个值来构成的,每次取一个由对 sumInts的特定递归调用返回的值,然后加上 a的当前值。当递归到底时,最深的调用返回0。但是,该值并不会立即退出递归调用链; 相反,它只是将该值返回给递归调用的上一层。通过这种方式,每个递归调用只需再添加一个数字,并将其返回到链中更高的位置,最终得到总和。作为一个练习,尝试跟踪 sumInts(2, 5),这正是您想要开始的。

希望这个能帮上忙!

要理解递归,您必须以不同的方式思考这个问题。一旦你找到了子问题的答案,你就可以结合子问题的结果来解决更大的问题。想想你和你的朋友需要数一个大桶里的弹珠数量。你们每人拿一个小一点的桶,然后分别数一下,数完之后把总数加起来。.现在,如果你们每个人都找到一个朋友,然后把桶分得更多,那么你只需要等待其他的朋友计算出他们的总数,把它带回来给你们每个人,你把它加起来。诸如此类。特殊的情况是,当你只得到1个弹珠,然后你只是返回它,说1。让其他人在你上面做加法你已经完成了。

您必须记住,每次函数递归地调用自己时,它都会创建一个带有问题子集的新上下文,一旦该部分得到解决,它就会返回,以便完成上一次迭代。

Let me show you the steps:

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

once sumInts(a: 6, b: 5) has executed, the results can be computed so going back up the chain with the results you get:

 sumInts(a: 6, b: 5) = 0
sumInts(a: 5, b: 5) = 5 + 0 = 5
sumInts(a: 4, b: 5) = 4 + 5 = 9
sumInts(a: 3, b: 5) = 3 + 9 = 12
sumInts(a: 2, b: 5) = 2 + 12 = 14.

表示递归结构的另一种方法是:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)
sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)
sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
sumInts(a: 2, b: 5) = 14

我来试试。

执行方程 a + sumInts (a + 1,b) ,我将展示最终答案是14。

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
if (a > b) {
return 0
} else {
return a + sumInts(a + 1, b)
}
}


Given: a = 2 and b = 5


1) 2 + sumInts(2+1, 5)


2) sumInts(3, 5) = 12
i) 3 + sumInts(3+1, 5)
ii) 4 + sumInts(4+1, 5)
iii) 5 + sumInts(5+1, 5)
iv) return 0
v) return 5 + 0
vi) return 4 + 5
vii) return 3 + 9


3) 2 + 12 = 14.

Let us know if you have any further questions.

下面是下面示例中的另一个递归函数示例。

一个男人刚刚大学毕业。

它是年的时间量。

The total actual number of years worked before retiring, can be calculated as follows:

public class DoIReallyWantToKnow
{
public int howLongDoIHaveToWork(int currentAge)
{
const int DESIRED_RETIREMENT_AGE = 65;
double collectedMoney = 0.00; //remember, you just graduated college
double neededMoneyToRetire = 1000000.00


t = 0;
return work(t+1);
}


public int work(int time)
{
collectedMoney = getCollectedMoney();


if(currentAge >= DESIRED_RETIREMENT_AGE
&& collectedMoney == neededMoneyToRetire
{
return time;
}


return work(time + 1);
}
}

这应该足够让任何人沮丧了,哈哈

1.该函数将被递归调用,直到满足一个条件为止。这种情况是 a > b。当满足此条件时,返回0。乍一看,我希望返回值为0,这显然是不正确的。

下面是计算机计算 sumInts(2,5)如果能够做到的想法:

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
I want to compute sumInts(3, 5)
for this, I need to compute sumInts(4, 5)
and add 3 to the result.
I want to compute sumInts(4, 5)
for this, I need to compute sumInts(5, 5)
and add 4 to the result.
I want to compute sumInts(5, 5)
for this, I need to compute sumInts(6, 5)
and add 5 to the result.
I want to compute sumInts(6, 5)
since 6 > 5, this is zero.
The computation yielded 0, therefore I shall return 5 = 5 + 0.
The computation yielded 5, therefore I shall return 9 = 4 + 5.
The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

正如你所看到的,一些对函数 sumInts的调用实际上返回0但这不是最终值,因为计算机仍然需要将5加到0,然后4加到结果,然后3,然后2,正如我们计算机思想的最后四句话所描述的。注意,在递归中,计算机不仅要计算递归调用,还要记住如何处理递归调用返回的值。计算机内存中有一个特殊的区域叫做 堆栈,这种信息被保存在这里,这个空间是有限的,而且过于递归的函数可能会耗尽堆栈: 这就是 堆栈溢出堆栈溢出,它给我们最喜欢的网站起了这个名字。

你的陈述似乎隐含了这样一个假设,即计算机在执行递归调用时会忘记自己处于什么状态,但事实并非如此,这就是为什么你的结论与你的观察结果不符。

2.在每次迭代中打印出“ a”的值会得到一个我所期望的值: 2,3,4,5(在这一点上5 + 1 > b 满足第一个条件: a > b) ,但我仍然不知道14的值是如何实现的。

这是因为返回值本身不是 a,而是 a值和递归调用返回的值之和。

我通常了解递归函数如何工作的方法是通过查看基本情况并向后操作。下面是应用于此函数的技术。

首先是基本情况:

sumInts(6, 5) = 0

然后是 call stack中略高于这个值的调用:

sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

然后调用堆栈中刚好在这个值之上的调用:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

And so on:

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

等等:

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

注意,我们已经到达了对函数的最初调用 sumInts(2, 5) == 14

这些调用的执行顺序:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

The order in which these calls return:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

请注意,我们通过按照 返回的顺序跟踪调用,得出了关于函数如何运行的结论。

到目前为止,你已经得到了一些不错的答案,但我还要加上一个采取不同策略的答案。

首先,我写了很多关于简单递归算法的文章,您可能会感兴趣;

Http://ericlippert.com/tag/recursion/

Http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

这些是最新的顶部顺序,所以从底部开始。

其次,到目前为止,所有的答案都通过考虑 功能激活描述了递归语义。每个调用都生成一个新的 激活,并且递归调用在此激活的上下文中执行。这是一种很好的思考方式,但是还有另一种等价的方式: 智能文本搜索和替换

让我把你的函数重写成一个更简洁的形式; 不要把它想成是用任何特定的语言写的。

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

希望你能理解。如果你不熟悉这个条件运算符,它的形式是 condition ? consequence : alternative,它的含义将会变得清晰。

现在我们希望计算 s(2,5),我们通过用函数体替换调用,然后用 2替换 a,用 5替换 b:

s(2, 5)
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)

现在计算条件。我们用 false替换 2 > 5

---> false ? 0 : 2 + s(2 + 1, 5)

现在将所有假条件句替换为替代条件句,将所有真条件句替换为结果条件句。我们只有假条件句,所以我们用另一个表达式代替它:

---> 2 + s(2 + 1, 5)

现在,为了避免我输入所有这些 +符号,用它的值替换常量算术。(这有点作弊,但我不想记住所有的括号!)

---> 2 + s(3, 5)

现在搜索并替换,这次使用调用的主体,3表示 a5表示 b,我们将把调用的替换放在括号中:

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

And now we just keep on doing those same textual substitution steps:

---> 2 + (false ? 0 : 3 + s(3 + 1, 5))
---> 2 + (3 + s(3 + 1, 5))
---> 2 + (3 + s(4, 5))
---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(5, 5)))
---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(6, 5))))
---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + 0)))
---> 2 + (3 + (4 + 5))
---> 2 + (3 + 9)
---> 2 + 12
---> 14

我们在这里所做的只是简单的文本替换 。事实上,我不应该用“3”代替“2 + 1”等等,直到我不得不这样做,但是从教学的角度来看,它会变得很难阅读。

函数激活只不过是用调用的主体替换函数调用,并用相应的参数替换形式参数。要巧妙地引入括号,您必须非常小心,但除此之外,它只是文本替换。

当然,大多数语言实际上并不激活 执行作为文本替换,但 逻辑上来说就是它。

那么什么是无界递归呢?文本替换不停止的递归!请注意,最终是如何到达这样一个步骤的: 没有更多的 s需要替换,然后我们就可以应用这些规则进行算术运算了。

递归。在计算机科学中,递归是在有限自动机的主题下被深入讨论的。

最简单的形式是自我引用。例如,说“ my car is a car”是一个递归语句。问题是这个语句是一个无限递归,因为它永远不会结束。“汽车”的定义是,它是一辆“汽车”,因此它可以被替代。然而,没有尽头,因为在替代的情况下,它仍然成为“我的车是一辆车”。

如果声明是“我的车是宾利”,情况可能会有所不同。我的车是蓝色的”在这种情况下,在第二种情况下替换汽车可以是“宾利”,结果是“我的宾利是蓝色的”。这些类型的替换在计算机科学中通过 Context-Free Grammars得到了数学上的解释。

实际的替换是一个生产规则。假设语句是由 S 表示的,而 car 是一个变量,它可以是一个“ bentley”,那么这个语句可以递归地重构。

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

这可以通过多种方式构建,因为每个 |都意味着有一个选择。S可以被这些选项中的任何一个所替代,而 S 总是从空开始。ε表示终止生产。就像 S可以被替换一样,其他变量也可以(只有一个 C代表“宾利”)。

因此,从 S为空开始,用第一个选择 "my"SS替换它

"my"S

S仍然可以被替换,因为它代表了一个变量。我们可以再次选择“我的”或者 ε 来结束它,但是让我们继续我们最初的声明。我们选择的空间,这意味着 S被取代的 " "S

"my "S

接下来让我们选择 C

"my "CS

C 只有一个选择

"my bentley"S

还有 S 的空间

"my bentley "S

"my bentley is"S"my bentley is "S"my bentley is blue"S"my bentley is blue"(用 S 代替 ε 结束生产)为例,我们递归地构建了我们的语句“ my bentley is blue”。

可以将递归看作是这些结果和替换。流程中的每个步骤都将替换其前一步骤,以便产生最终结果。在从2到5的递归和的确切示例中,您最终得到的是产品

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

这就变成了

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14

我认为理解递归函数的最好方法是认识到它们是用来处理递归数据结构的。但是在原来递归计算从 ab的数字之和的函数 sumInts(a: Int, b: Int)中,它似乎不是一个递归的数据结构... ... 让我们尝试稍微修改一下版本 sumInts(a: Int, n: Int),其中 n是您将要添加的数字。

现在,sumInts 在 n上是递归的,n是一个自然数。还是不是递归数据,对吧?使用 Peano 公理,自然数可以被认为是一个递归数据结构:

enum Natural = {
case Zero
case Successor(Natural)
}

So, 0 = Zero, 1 = Succesor(Zero), 2 = Succesor(Succesor(Zero)), and so on.

一旦有了递归数据结构,就有了函数的模板。对于每个非递归情况,可以直接计算值。对于递归案例 你假设,递归函数已经在工作,并使用它来计算案例,但解构参数。在 Natural 的例子中,这意味着我们将使用 n而不是 Succesor(n),或者等效地,我们将使用 n - 1而不是 n

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
if (n == 0) {
// non recursive case
} else {
// recursive case. We use sumInts(..., n - 1)
}
}

现在递归函数的编程更简单了。首先,基本情况,n=0。如果我们不想加数,应该返回什么?答案当然是0。

递归情况怎么样?如果我们想添加 n的数字以 a开始,我们已经有一个工作的 sumInts的功能,为 n-1工作?我们需要添加 a,然后用 a + 1调用 sumInts,所以我们以:

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
if (n == 0) {
return 0
} else {
return a + sumInts(a + 1, n - 1)
}
}

好的方面是,现在您不需要考虑低级递归。你只需要确认一下:

  • For the base cases of the recursive data, it calculates the answer without using recursion.
  • 对于递归数据的递归情况,它使用解构数据上的递归来计算答案。

You might be interested in Nisan and Schocken's 执行职能. The linked pdf is part of a free online course. It describes the second part of a virtual machine implementation in which the student should write a virtual-machine-language-to-machine-language compiler. The function implementation they propose is capable of recursion because it is stack-based.

为了向您介绍函数实现: 请考虑以下虚拟机代码:

enter image description here

如果 Swift 编译成这种虚拟机语言,那么下面这段 Swift 代码:

mult(a: 2, b: 3) - 4

会汇编成

push constant 2  // Line 1
push constant 3  // Line 2
call mult        // Line 3
push constant 4  // Line 4
sub              // Line 5

虚拟机语言是围绕 全球堆栈全球堆栈.push constant n将一个整数推送到这个全局堆栈上来设计的。

在执行第1行和第2行之后,堆栈看起来像这样:

256:  2  // Argument 0
257:  3  // Argument 1

256257是内存地址。

call mult将返回行号(3)推送到堆栈上,并为函数的局部变量分配空间。

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

...and it goes-to the label function mult. The code inside mult is executed. As a result of executing that code we compute the product of 2 and 3, which is stored in the function's 0th local variable.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

就在从 mult 进行 return0之前,您会注意到下面这行:

push local 0  // push result

我们将把产品推到堆栈上。

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

当我们回来的时候,发生了以下事情:

  • 将堆栈上的最后一个值弹出到第0个参数的内存地址(本例中为256)。这里正好是放它最方便的地方。
  • 丢弃堆栈中到第0个参数地址为止的所有内容。
  • 转到返回行号(本例中为3) ,然后前进。

返回之后,我们就可以执行第4行了,我们的堆栈如下所示:

256:  6  // product that we just returned

现在我们把4放到堆栈上。

256:  6
257:  4

sub是虚拟机语言的基本函数。它接受两个参数,并以通常的地址返回结果: 第0个参数的地址。

Now we have

256:  2  // 6 - 4 = 2

既然您已经知道了函数调用是如何工作的,那么理解递归是如何工作的就相对简单了。没有魔法只是一堆。

I have implemented your sumInts function in this virtual machine language:

function sumInts 0     // `0` means it has no local variables.
label IF
push argument 0
push argument 1
lte
if-goto ELSE_CASE
push constant 0
return
label ELSE_CASE
push constant 2
push argument 0
push constant 1
add
push argument 1
call sumInts       // Line 15
add                // Line 16
return             // Line 17
// End of function

现在我将它命名为:

push constant 2
push constant 5
call sumInts           // Line 21

代码执行后,我们一直到达 lte返回 false的停止点。这就是堆栈现在的样子:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

现在让我们“解除”递归。 return0和转到第15行并前进。

271:  5
272:  0

Line 16: add

271:  5

第17行: return5和第15行,前进。

267:  4
268:  5

第16行: add

267:  9

第17行: return9和第15行,前进。

263:  3
264:  9

第16行: add

263:  12

第17行: return12和第15行,前进。

259:  2
260:  12

第16行: add

259:  14

第17行: return14和第21行,前进。

256:  14

There you have it. Recursion: Glorified goto.

已经有很多好的答案了,我还是要试一试。
当调用时,函数得到一个分配的 记忆空间,它被堆叠在调用函数的 记忆空间上。在这个内存空间中,函数保存传递给它的参数、变量及其值。这个 记忆空间与函数的结束返回调用一起消失。随着堆栈思想的发展,调用方函数的 记忆空间现在变为活动的。

对于递归调用,同一个函数会将多个 记忆空间一个接一个地堆叠起来。仅此而已。关于 在计算机内存中的工作原理的简单想法应该可以帮助您理解递归在实现中是如何发生的。

有点跑题,我知道,但是... ... 试着在谷歌上查一下 递归... ... 你会通过例子看到它的意思: -)


早期版本的 Google 返回了以下文本(从内存中引用) :

递归

See 递归

On September 10th 2014, the joke about recursion has been updated:

递归

Did you mean: 递归


有关其他答复,请参见 这个答案

我在学习和理解递归时遇到的一个非常好的技巧是,花一些时间学习一种除了通过递归之外没有任何形式的循环构造的语言。这样你就能通过练习对如何使用递归有很好的感觉。

我学习了 http://www.htdp.org/,它不仅是一个 Scheme 教程,也是一个关于如何从架构和设计的角度设计程序的很好的入门。

但基本上,你需要投入一些时间。如果没有对递归的“牢固”把握,某些算法,比如回溯,对你来说总是显得“困难”甚至“神奇”。所以,坚持下去。歌曲:-D

我希望这对你有所帮助,祝你好运!

把递归看作 多个克隆人做同样的事情。

You ask to clone[1]: "sum numbers between 2 and 5"

+ clone[1]               it knows that: result is 2 + "sum numbers between 3 and 5". so it asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           it knows that: result is 3 + "sum numbers between 4 and 5". so it asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       it knows that: result is 4 + "sum numbers between 5 and 5". so it asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   it knows that: result is 5 + "sum numbers between 6 and 5". so it asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] it knows that: it can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   it gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       it gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           it gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               it gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

and voilá!!

上面的许多答案都很好。不过,解决递归问题的一个有用的技术是,首先说明我们想要做什么,然后像人一样编写代码来解决这个问题。在上面的例子中,我们想要求和一个连续的整数序列(使用上面的数字) :

2, 3, 4, 5  //adding these numbers would sum to 14

现在,请注意这些行是令人困惑的(不是错误的,而是令人困惑的)。

if (a > b) {
return 0
}

为什么测试 a>b? 为什么测试 return 0

让我们更改代码,以更接近地反映人类的行为

func sumInts(a: Int, b: Int) -> Int {
if (a == b) {
return b // When 'a equals b' I'm at the most Right integer, return it
}
else {
return a + sumInts(a: a + 1, b: b)
}
}

我们能做得更像人类吗?太棒了!通常我们从左到右加起来(2 + 3 + ...)。但是上面的递归是从右到左求和(... + 4 + 5)。更改代码以反映它(-可能有点吓人,但并不多)

func sumInts(a: Int, b: Int) -> Int {
if (a == b) {
return b // When I'm at the most Left integer, return it
}
else {
return sumInts(a: a, b: b - 1) + b
}
}

有些人可能会觉得这个函数比较令人困惑,因为我们是从“远端”开始的,但是练习可以让它感觉很自然(这是另一个很好的“思考”技巧: 在解决递归问题时尝试“两边”)。再一次,这个函数反映了一个人(大多数?)Does: 获取所有左整数的总和,并添加“下一个”右整数。

我很难理解递归,然后我发现 这个博客和我已经看到这个问题,所以我想我必须分享。你必须阅读这篇博客,我发现这篇文章非常有帮助,它解释了如何使用栈,甚至它解释了两个递归如何一步一步地使用栈。我建议您首先了解堆栈是如何工作的,这里对此有很好的解释: 堆栈之旅

then now you will understand how recursion works now take a look of this post: 逐步理解递归

enter image description here

它是一个程序:

def hello(x):
if x==1:
return "op"
else:
u=1
e=12
s=hello(x-1)
e+=1
print(s)
print(x)
u+=1
return e


hello(3)

enter image description here enter image description here

当我不再阅读别人对递归的看法,或者不再将其视为可以避免的东西,而只是编写代码时,递归就开始对我有意义了。我发现了一个解决方案的问题,并试图在不看的情况下复制该解决方案。我只有在无助地陷入困境的时候才想到解决办法。然后我又试图复制它。我在多个问题上再次这样做,直到我对如何识别递归问题并解决它有了自己的理解和感觉。当我达到这个水平时,我开始编造问题并解决它们。这对我帮助更大。有时候,事情只能通过你自己的尝试和奋斗来学习,直到你“得到它”。

让我用一个斐波那契数列的例子来告诉你,斐波那契是

T (n) = t (n-1) + n;

如果 n = 0那么1

so let see how recursion works, I just replace n in t(n) with n-1 and so on. it looks:

T (n-1) = t (n-2) + n + 1;

T (n-1) = t (n-3) + n + 1 + n;

T (n-1) = t (n-4) + n + 1 + n + 2 + n;

.

.

.

T (n) = t (n-k) + ... + (n-k-3) + (n-k-2) + (n-k-1) + n;

我们知道如果 t(0)=(n-k)等于 1那么 n-k=0所以 n=k我们用 n代替 k:

T (n) = t (n-n) + ... + (n-n + 3) + (n-n + 2) + (n-n + 1) + n;

如果我们省略 n-n,那么:

T (n) = t (0) + ... + 3 + 2 + 1 + (n-1) + n;

so 3+2+1+(n-1)+n is natural number. it calculates as Σ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2

结果为: O(1 + n²) = O(n²)

这是理解递归关系的最好方法