递归 尾递归_如何建立递归的直觉

递归 尾递归

by Dawson Eliasen

道森·埃里亚森(Dawson Eliasen)

如何建立递归的直觉 (How to build up an intuition for recursion)

以及如何使用它来解决问题 (And how to use it to solve problems)

Recursion is one of the most intimidating topics that students face in programming. It’s hard to understand because the human brain is not capable of performing recursion — but computers are. This is exactly why recursion is such a powerful tool for programmers, but it also means that learning how to use it is exceedingly difficult. I want to help you build an intuition for recursion so you can use it to solve problems.

递归是学生在编程中面临的最令人生畏的主题之一。 很难理解,因为人脑不具有执行递归的能力,而计算机却可以。 这正是为什么递归是程序员如此强大的工具的原因,但是这也意味着学习如何使用递归极其困难。 我想帮助您建立一个递归的直觉,以便您可以使用它来解决问题。

I am a teaching assistant for the introductory computer science course at my university. I’ve explained recursion in exactly the same way a dozen times this week. My explanation seems to help most students. This article has the most general explanation at the top, and the most specific explanation at the bottom. This way, you can start at the beginning and stop as soon as you feel you understand recursion well enough. I’ve provided some examples in Java, and they are simple enough that anyone with some programming experience can interpret them.

我是我的大学计算机科学入门课程的助教。 我本周用十多次完全相同的方式解释了递归。 我的解释似乎对大多数学生有帮助。 本文在顶部具有最一般的说明,在底部具有最具体的说明。 这样,您可以从头开始并在对递归足够了解后立即停止。 我提供了一些Java示例,这些示例非常简单,任何具有一定编程经验的人都可以解释它们。

什么是递归? (What is Recursion?)

To understand recursion, let’s take a step back from programming. Let’s start by establishing a general definition for the term. Something is recursive if it is defined by its own definition to some extent. That probably doesn’t help you understand recursion very much, so let’s look at a mathematical definition. You are familiar with functions — one number goes in, another number comes out. They look like this:

要了解递归,让我们从编程上退一步。 让我们首先为该术语建立一个通用定义。 如果某种程度上由其自己的定义来定义,则它是递归的 。 这可能不会帮助您非常了解递归,因此让我们看一下数学定义。 您熟悉功能-一个数字输入,另一个数字输出。 他们看起来像这样:

f(x) = 2x

f(x)= 2x

Let’s change this idea slightly and instead think about a sequence. A sequence takes an integer number, and an integer number comes out.

让我们稍微改变一下这个想法,而是考虑一个序列。 一个序列取一个整数,然后出现一个整数。

A(n) = 2n

A(n)= 2n

Sequences can be thought of as functions with inputs and outputs that are limited to only positive integers. Generally, sequences start with 1. This means that A(0) is 1. The sequence above is the following:

可以将序列视为具有仅限于正整数的输入和输出的函数。 通常,序列以1开头。这意味着A(0)为1。上面的序列如下:

A(n) = 1, 2, 4, 6, 8, 10, … where n = 0, 1, 2, 3, 4, 5, …

A(n)= 1,2,4,6,8,10,…,其中n = 0,1,2,3,4,5,…

Now, consider the following sequence:

现在,考虑以下顺序:

A(n) = 2 x A(n-1)

A(n)= 2 x A(n-1)

This sequence is recursively defined. In other words, the value any given element depends on the value of another element. This sequence looks like this:

该序列是递归定义的。 换句话说,任何给定元素的值都取决于另一个元素的值。 此序列如下所示:

A(n) = 1, 2, 4, 8, 16, … where n = 0, 1, 2, 3, 4, …

A(n)= 1,2,4,8,16,…,其中n = 0,1,2,3,4,…

Any element is defined as 2 times the previous element.

任何元素的定义是前一个元素的2倍。

  • The n = 4 element, 16, is defined as 2 times the previous element.

    n = 4元素16被定义为前一个元素的2倍。
  • The n = 3 element, 8, is defined as 2 times the previous element.

    n = 3元素8定义为前一个元素的2倍。
  • The n = 2 element, 4, is defined as 2 times the previous element.

    n = 2个元素4定义为前一个元素的2倍。
  • The n = 1 element, 2, is defined as 2 times the previous element.

    n = 1元素2定义为前一个元素的2倍。
  • The n = 0 element, 1, is defined as…

    n = 0元素1定义为…

The n = 0 element cannot be recursively defined. There is no previous element. We call this a base case, and it is a necessary consequence of recursive definitions. They must be explicitly represented in your code. We could represent this recursive sequence in Java like so:

n = 0元素不能递归定义。 没有上一个元素。 我们将其称为基本案例 ,这是递归定义的必要结果。 它们必须在您的代码中明确表示 。 我们可以这样用Java表示此递归序列:

public int A(int n){    if (n == 0)        return 1;    return 2 * A(n - 1);}

You should familiarize yourself with the anatomy of a recursive method. Note the base case: if n is 0, the element is defined as 1. Otherwise, the element is defined as 2 times the previous element. We must recursively call the method to get the value of the previous element, and then multiply it by 2. All recursive methods will have these two components:

您应该熟悉递归方法的结构。 请注意基本情况:如果n为0,则将该元素定义为1。否则,将该元素定义为前一个元素的2倍。 我们必须递归调用该方法以获取前一个元素的值,然后将其乘以2。所有递归方法将具有以下两个组成部分:

  • Base case, which returns a well-defined value.

    基本情况,返回定义明确的值。
  • Recursive case, which returns a recursively defined value.

    递归的情况,它返回一个递归定义的值。

Let’s do another example, continuing with the mathematics context. The Fibonacci sequence is often used to illustrate recursion. Any element of the Fibonacci sequence is the sum of the two preceding elements. It goes like this:

让我们再举一个例子,继续讲数学上下文。 斐波那契数列通常用于说明递归。 斐波那契数列的任何元素都是前面两个元素的总和。 它是这样的:

F(n) = 1, 1, 2, 3, 5, 8, … where n = 0, 1, 2, 3, 4, 5, …

F(n)= 1,1,2,3,5,8,…,其中n = 0,1,2,3,4,5,…

  • The n = 5, element, 8, is defined as the sum of the n = 4 element and the n = 3 element…

    n = 5,元素8,定义为n = 4元素和n = 3元素的和……

At this point, you should hesitate. In the previous example, each element depended on only one other element, now each element depends on two other elements. This complicates things.

此时,您应该犹豫。 在前面的示例中,每个元素仅依赖于另一个元素,现在每个元素都依赖于另外两个元素。 这使事情变得复杂。

  • The n = 4 element, 5, is defined as the sum of the n = 3 element and the n = 2 element.

    n = 4元素5定义为n = 3元素和n = 2元素之和。
  • The n = 3 element, 3, is defined as the sum of the n = 2 element and the n = 1 element.

    n = 3元素3定义为n = 2元素和n = 1元素之和。
  • The n = 2 element, 2, is defined as the sum of the n = 1 element and the n = 0 element.

    n = 2元素2定义为n = 1元素和n = 0元素之和。
  • The n = 1 element, 1, is defined as the sum of the n = 0 element and…

    n = 1个元素1定义为n = 0个元素与……的和。

The n = 1 element cannot be recursively defined. Neither can the n = 0 element. These elements cannot be recursively defined because the recursive definition requires two preceding elements. The n = 0 element has no preceding elements, and the n = 1 element has only one preceding element. This means that there are two base cases. Before writing any code, I would write down something like this:

n = 1元素不能递归定义。 n = 0元素也不能。 无法递归定义这些元素,因为递归定义需要两个前面的元素。 n = 0元素没有在前元素,n = 1元素只有一个在前元素。 这意味着有两种基本情况。 在编写任何代码之前,我将写下这样的内容:

The n = 0 element is defined as 1. The n = 1 element is defined as 1.

n = 0元素定​​义为1。n = 1元素定义为1。

The n element is defined as the sum of the n-1 element and the n-2 element.

n元素定义为n-1元素和n-2元素之和。

Now we have an idea of how this task is recursively defined, and we can go ahead and write some code. Never start writing code without first having a natural understanding of the task.

现在,我们对如何递归定义此任务有了一个想法,我们可以继续编写一些代码。 决不 在没有先对任务有自然理解的情况下开始编写代码。

public int F(int n){    if (n == 0 || n == 1)        return 1;    return F(n - 1) + F(n - 2);}
调用堆栈 (The Call Stack)

As programmers, we want to have an intuition for recursion so that we may use it to do things. To do so effectively, we must understand how a computer processes recursion.

作为程序员,我们希望对递归有一个直觉,以便我们可以使用它来做事。 为了有效地做到这一点,我们必须了解计算机如何处理递归。

There is a data structure that the computer uses to keep track of method calls called the call stack. Each method call creates local variables from the method parameters. The computer needs to store these variables while the method is being executed. Then, the computer ditches the values when the method returns to avoid wasting memory.

计算机使用一种数据结构来跟踪称为调用堆栈的方法调用。 每个方法调用都根据方法参数创建局部变量 。 在执行该方法时,计算机需要存储这些变量。 然后,当方法返回时计算机将清除这些值,以避免浪费内存。

The call stack (and stacks in general) function as you might imagine some sort of real-life stack would. Imagine a stack of papers on your desk — it starts as nothing, and then you add papers one by one. You don’t know anything about any of the papers in the stack except for the paper on top. The only way you can remove papers from the stack is by taking them off the top, one-by-one, in the opposite order that they were added.

调用堆栈(通常是堆栈)的功能就像您想象的那样,是某种现实生活中的堆栈。 想象一下桌上的一堆纸-它从无到有开始,然后逐个添加文件。 除了顶部的纸之外,您对纸堆中的任何纸都一无所知。 可以从纸叠中取出纸张的唯一方法是按照与添加纸张相反的顺序将它们从顶部逐一取出。

This is essentially how the call stack works, except the items in the stack are activation records instead of papers. Activation records are just little pieces of data that store the method name and parameter values.

从本质上讲,这就是调用堆栈的工作方式,只是堆栈中的项目是激活记录而不是文件。 激活记录只是存储方法名称和参数值的一小部分数据。

Without recursion, the call stack is pretty simple. Here’s an example. If you had some code that looked like this…

没有递归,调用栈非常简单。 这是一个例子。 如果您有一些看起来像这样的代码...

public static void main(String[] args)    System.out.println(myMethod(1));

…The call stack would look like this:

…调用堆栈如下所示:

*  myMethod(int a)
*  main(String[] args)

Here we see two methods under execution, main and myMethod. The important thing to notice is that main cannot be removed from the stack until myMethod is removed from the stack. In other words, main cannot complete until myMethod is called, executed, and returns a value.

在这里,我们看到正在执行的两个方法mainmyMethod 。 需要注意的重要一点是,除非将myMethod从堆栈中删除,否则无法从堆栈中删除main 。 换句话说,在调用,执行并返回值myMethod之前, main才能完成。

This is true for any case of method composition (a method within a method) — so let’s look at recursive example: the A(int n) method we wrote earlier. Your code might look like this:

对于方法组合的任何情况(方法中的方法)都是如此-因此,让我们看一下递归示例:我们之前编写的A(int n)方法。 您的代码可能如下所示:

public static void main(String[] args)    System.out.println(A(4));
public static int A(int n){    if (n == 0)        return 1;    return 2 * A(n - 1);}

When main is called, A is called. When A is called, it calls itself. So the call stack will start building up like so:

调用main将调用A 调用A ,它会自行调用。 因此,调用堆栈将像这样开始建立:

* A(4)* main(String[] args)

A(4) calls A(3).

A(4)调用A(3)

* A(3)* A(4)* main(String[] args)

Now, it’s important to note that A(4) cannot be removed from the call stack until A(3) is removed from the call stack first. This makes sense, because the value of A(4) depends on the value of A(3). The recursion carries on…

现在,重要的是要注意,除非先从调用堆栈中删除A(3)否则才能从调用堆栈中删除A(4) 。 这是有道理的,因为A(4)的值取决于A(3)的值。 递归进行中...

* A(0)* A(1)* A(2)* A(3)* A(4)* main(String[] args)

When A(0) is called, we have reached a base case. This means that the recursion is completed, and instead of making a recursive call, a value is returned. A(0) comes off the stack, and the rest of the calls are then able to come off the stack in succession until A(4) is finally able to return its value to main.

调用A(0) ,我们已经达到一个基本情况。 这意味着递归已完成,而不是进行递归调用,而是返回一个值。 A(0)离开堆栈,然后其余的调用就可以连续离开堆栈,直到A(4)最终能够将其值返回给main。

Here’s the intuition: the return value of any method call depends on the return value of another method call. Therefore, all the method calls must be stored in memory until a base case is reached. When the base case is reached, the values start becoming well-defined instead of recursively defined. For example, A(1) is recursively defined until it knows the definition of the base case, 1. Then, it is well-defined as 2 times 1.

这是直觉:任何方法调用的返回值都取决于另一个方法调用的返回值。 因此,所有方法调用都必须存储在内存中,直到达到基本情况为止。 达到基本情况时,值开始变得定义明确,而不是递归定义。 例如,递归定义A(1)直到知道基本情况的定义1。然后,将其定义为2乘以1。

When we are trying to solve problems with recursion, it is often more effective to think about the order in which values are returned. This is the opposite of the order in which calls are made. This order is more useful because it consists of well-defined values, instead of recursively defined values.

当我们尝试解决递归问题时,考虑返回值的顺序通常更有效。 这与进行呼叫的顺序相反。 此顺序更有用,因为它包含定义明确的值,而不是递归定义的值。

For this example, it is more useful to consider that A(0) returns 1, and then A(1) returns 2 times 1, and then A(2) returns 2 times A(1), and so on. However, when we are writing our code, it can easier to frame it in the reverse order (the order that the calls are made). This is another reason that I find it helpful to write the base case and the recursive case down before writing any code.

对于此示例,考虑A(0)返回1,然后A(1)返回2乘以1,然后A(2)返回2乘以A(1) ,则更有用。 但是,在编写代码时,可以更容易以相反的顺序(调用的顺序)对其进行构图。 这是我发现在编写任何代码之前先写下基本案例和递归案例很有用的另一个原因。

辅助方法和递归与循环 (Helper Methods and Recursion vs. Loops)

We are programmers, not mathematicians, so recursion is simply a tool. In fact, recursion is a relatively simple tool. It’s very similar to loops in that both loops and recursion induce repetition in the program.

我们是程序员,而不是数学家,因此递归只是一种工具。 实际上,递归是一个相对简单的工具。 它与循环非常相似,因为循环和递归都会在程序中引起重复。

You may have heard that any repetitive task can be done using either a while loop or a for loop. Some tasks lend themselves better to while loops and other tasks lend themselves better to for loops.

您可能听说过,可以使用while循环或for循环完成任何重复性任务。 一些任务更适合while循环,而其他任务更适合for循环。

The same is true with this new tool, recursion. Any repetitive task can be accomplished with either a loop or recursion, but some tasks lend themselves better to loops and others lend themselves better to recursion.

递归这个新工具也是如此。 任何重复性任务都可以通过循环或递归来完成,但是某些任务更适合于循环,而另一些则更适合于递归。

When we use loops, it is sometimes necessary to make use of a local variable to “keep track” of a calculation. Here’s an example.

使用循环时,有时必须使用局部变量来“跟踪”计算。 这是一个例子。

public double sum (double[] a){    double sum = 0.0;    for (int i = 0; i < a.length; i++)        sum += a[i];    return sum;
}

This method takes an array of doubles as a parameter and returns the sum of that array. It uses a local variable, sum, to keep track of the working sum. When the loop is completed, sum will hold the actual sum of all values in the array, and that value is returned. This method actually has two other local variables that are less obvious. There is the double array a, whose scope is the method, and the iterator i (keeps track of the index), whose scope is the for loop.

此方法将双精度数组作为参数,并返回该数组的和。 它使用局部变量sum来跟踪工作总和。 循环完成后, sum将保存数组中所有值的实际和,然后返回该值。 该方法实际上还有另外两个不太明显的局部变量。 有一个double数组a ,其范围是方法,还有迭代器i (保留索引的轨迹),其范围是for循环。

What if we wanted to accomplish this same task using recursion?

如果我们想使用递归来完成相同的任务怎么办?

public double recursiveSum(double[] a)    # recursively calculate sum

This task is repetitive, so it is possible to do it using recursion, though it is probably more elegantly accomplished using a loop. We just need to create a few local variables to keep track of the working sum and the index, right?

此任务是重复性的,因此可以使用递归来完成它,尽管使用循环可能更完美。 我们只需要创建一些局部变量来跟踪工作总和和索引,对吧?

Alas, this is impossible. Local variables only exist in the context of a single method call, and recursion makes use of repeated method calls to accomplish a repetitive task. This means that local variables are pretty much useless when we are using recursion. If you are writing a recursive method and you feel as though you need a local variable, you probably need a helper method.

las,这是不可能的。 局部变量仅在单个方法调用的上下文中存在,并且递归利用重复的方法调用来完成重复的任务。 这意味着在使用递归时,局部变量几乎没有用。 如果您正在编写一个递归方法,并且感觉好像需要局部变量,则可能需要一个辅助方法。

A helper method is a recursive method that makes use of additional parameters to keep track of values. For recursiveSum, our helper method might look like this:

辅助方法是一种递归方法,它使用其他参数来跟踪值。 对于recursiveSum ,我们的辅助方法可能如下所示:

public double recursiveSum(double[] a, double sum, int index){    if (index == a.length)        return sum;    sum += a[index];    return recursiveSum(a, sum, index + 1);}

This method builds the sum by passing the working value to a new method call with the next index. When there are no more values in the array, the working sum is the actual sum.

该方法通过将工作值传递给具有下一个索引的新方法来构建总和。 当数组中没有更多值时,工作总和就是实际总和。

Now we have two methods. The “starter method,” and the helper method.

现在我们有两种方法。 “启动方法”和辅助方法。

public double recursiveSum(double[] a)    # recursively calculate sum
public double recursiveSum(double[] a, double sum, int index){    if (index == a.length)        return sum;    sum += a[index];    return recursiveSum(a, sum, index + 1);}

The term “helper method” is actually a bit of a misnomer. It turns out that the helper method does all the work, and the other method is just a starter. It simply calls the helper method with the initial values that start the recursion.

术语“辅助方法”实际上有点用词不当。 事实证明,辅助方法可以完成所有工作,而另一种方法只是启动器。 它只是使用启动递归的初始值来调用helper方法。

public double recursiveSum(double[] a)    return recursiveSum(a, 0.0, 0);
public double recursiveSum(double[] a, double sum, int index){    if (index == a.length)        return sum;    sum += a[index];    return recursiveSum(a, sum, index + 1);}

Note that the values used in the starter call to the helper method are the same values used to initialize the local variables in the loop example. We initialize the variable used to keep track of the sum to 0.0, and we initialize the variable used to keep track of the index to 0.

请注意,在辅助方法的starter调用中使用的值与在循环示例中用于初始化局部变量的值相同。 我们将用于跟踪总和的变量初始化为0.0 ,并将用于跟踪索引的变量初始化为0

Earlier, I said that local variables are useless in the context of recursion. This isn’t completely true, because the method parameters are indeed local variables. They work for recursion because new ones are created every time the method is called. When the recursion is executed, there are many method calls being stored in the call stack, and as a result there are many copies of the local variables.

之前,我说过,局部变量在递归上下文中是无用的。 这不是完全正确的,因为方法参数确实是局部变量。 它们适用于递归,因为每次调用该方法时都会创建新的。 执行递归时,在调用堆栈中存储了许多方法调用,因此,存在许多局部变量的副本。

You might ask, “If the helper method does all the work, why do we even need the starter method? Why don’t we just call the helper method with the initial values, and then you only need to write one method?”

您可能会问:“如果辅助方法可以完成所有工作,为什么我们甚至需要起动方法? 我们为什么不只用初始值调用helper方法,然后只需要编写一个方法?”

Well, remember that we were trying to replace the method that used a for loop. That method was simple. It took an array as a parameter and returned the sum of the array as a double. If we replaced this method with one that took three parameters, we would have to remember to call it with the proper starting values. If someone else wanted to use your method, it would be impossible if he or she didn’t know the starting values.

好吧,请记住,我们正在尝试替换使用for循环的方法。 该方法很简单。 它以一个数组作为参数,并将数组的总和作为双精度值返回。 如果我们用带有三个参数的方法替换了此方法,则必须记住要使用正确的起始值来调用它。 如果其他人想使用您的方法,那么如果他或她不知道起始值,那将是不可能的。

For these reasons, it makes sense to add another method that takes care of these starting values for us.

由于这些原因,有必要添加另一种方法来为我们处理这些起始值。

结语 (Wrapping up)

Recursion is a pretty challenging concept, but you made it all the way to the end of my explanation. I hope you understand the magic a little better. I now officially grant you the title of “Grand-Wizard of Recursion.” Congratulations!

递归是一个非常具有挑战性的概念,但是您一直使用它直到我的解释结束。 希望您对魔术有所了解。 我现在正式授予您“递归大向导”的称号。 恭喜你!

翻译自: https://www.freecodecamp.org/news/how-to-build-up-an-intuition-for-recursion-986032c2f6ad/

递归 尾递归

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值