Understanding how recursive functions work

后端 未结 18 772
野性不改
野性不改 2020-11-22 07:08

As the title explains I have a very fundamental programming question which I have just not been able to grok yet. Filtering out all of the (extremely clever) \"In order to

相关标签:
18条回答
  • 2020-11-22 07:44

    To understand recursion you must think of the problem in a different way. Instead of a large logical sequence of steps that makes sense as a whole you instead take a large problem and break up into smaller problems and solve those, once you have an answer for the sub problems you combine the results of the sub problems to make the solution to the bigger problem. Think of you and your friends needing to count the number of marbles in a huge bucket. You do each take a smaller bucket and go count those individually and when you are done you add the totals together.. Well now if each of you find some friend and split the buckets further, then you just need to wait for these other friends to figure out their totals, bring it back to each of you, you add it up. And so on. The special case is when you only get 1 marble to count then you just return it back and say 1. let the other people above you do the adding you are done.

    You must remember every time the function calls itself recursively it creates a new context with a subset of the problem, once that part is resolved it gets returned so that the previous iteration can complete.

    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.
    

    Another way to represent the structure of the recursion:

     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 
    
    0 讨论(0)
  • 2020-11-22 07:48

    I think the best way to understand recursive functions is realizing that they are made to process recursive data structures. But in your original function sumInts(a: Int, b: Int) that calculates recursively the sum of numbers from a to b, it seems not to be a recursive data structure... Let's try a slightly modified version sumInts(a: Int, n: Int) where n is how many numbers you'll add.

    Now, sumInts is recursive over n, a natural number. Still not a recursive data, right? Well, a natural number could be considered a recursive data structre using Peano axioms:

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

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

    Once you have a a recursive data structure, you have the template for the function. For each non recursive case, you can calculate the value directly. For the recursive cases you assume that the recursive function is already working and use it to calculate the case, but deconstructing the argument. In the case of Natural, it means that instead of Succesor(n) we'll use n, or equivalently, instead of n we'll use n - 1.

    // 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)
        }
    }
    

    Now the recursive function is simpler to program. First, the base case, n=0. What should we return if we want to add no numbers? The answer is, of course 0.

    What about the recursive case? If we want to add n numbers beginning with a and we already have a working sumInts function that works for n-1? Well, we need to add a and then invoke sumInts with a + 1, so we end with:

    // 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)
        }
    }
    

    The nice thing is that now you shouldn't need to think in the low level of recursion. You just need to verify that:

    • For the base cases of the recursive data, it calculates the answer without using recursion.
    • For the recursive cases of the recursive data, it calculates the answer using recursion over the destructured data.
    0 讨论(0)
  • 2020-11-22 07:51

    The way that I usually figure out how a recursive function works is by looking at the base case and working backwards. Here's that technique applied to this function.

    First the base case:

    sumInts(6, 5) = 0
    

    Then the call just above that in the call stack:

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

    Then the call just above that in the call stack:

    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
    

    And so on:

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

    Notice that we've arrived at our original call to the function sumInts(2, 5) == 14

    The order in which these calls are executed:

    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)
    

    Note that we came to a conclusion about how the function operates by tracing the calls in the order that they return.

    0 讨论(0)
  • 2020-11-22 07:51

    There are already a lot of good answers. Still I am giving a try.
    When called, a function get a memory-space allotted, which is stacked upon the memory-space of the caller function. In this memory-space, the function keeps the parameters passed to it, the variables and their values. This memory-space vanishes along with the ending return call of the function. As the idea of stack goes, the memory-space of the caller function now becomes active.

    For recursive calls, the same function gets multiple memory-space stacked one upon another. That's all. The simple idea of how stack works in memory of a computer should get you through the idea of how recursion happens in implementation.

    0 讨论(0)
  • 2020-11-22 07:56

    Recursion is a tricky topic to understand and I don't think I can fully do it justice here. Instead, I'll try to focus on the particular piece of code you have here and try to describe both the intuition for why the solution works and the mechanics of how the code computes its result.

    The code you've given here solves the following problem: you want to know the sum of all the integers from a to b, inclusive. For your example, you want the sum of the numbers from 2 to 5, inclusive, which is

    2 + 3 + 4 + 5

    When trying to solve a problem recursively, one of the first steps should be to figure out how to break the problem down into a smaller problem with the same structure. So suppose that you wanted to sum up the numbers from 2 to 5, inclusive. One way to simplify this is to notice that the above sum can be rewritten as

    2 + (3 + 4 + 5)

    Here, (3 + 4 + 5) happens to be the sum of all the integers between 3 and 5, inclusive. In other words, if you want to know the sum of all the integers between 2 and 5, start by computing the sum of all the integers between 3 and 5, then add 2.

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

    3 + 4 + 5

    which can be thought of instead as

    3 + (4 + 5)

    Here, (4 + 5) is the sum of all the integers between 4 and 5, inclusive. So, if you wanted to compute the sum of all the numbers between 3 and 5, inclusive, you'd compute the sum of all the integers between 4 and 5, then add 3.

    There's a pattern here! If you want to compute the sum of the integers between a and b, inclusive, you can do the following. First, compute the sum of the integers between a + 1 and b, inclusive. Next, add a to that total. You'll notice that "compute the sum of the integers between a + 1 and b, inclusive" happens to be pretty much the same sort of problem we're already trying to solve, but with slightly different parameters. Rather than computing from a to b, inclusive, we're computing from a + 1 to b, inclusive. That's the recursive step - to solve the bigger problem ("sum from a to b, inclusive"), we reduce the problem to a smaller version of itself ("sum from a + 1 to b, inclusive.").

    If you take a look at the code you have above, you'll notice that there's this step in it:

    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!

    In recursive problem solving, there needs to be some way to stop simplifying the problem and instead just go solve it directly. Typically, you'd find a simple case where the answer can be determined immediately, then structure your solution to solve simple cases directly when they arise. This is typically called a base case or a recursive basis.

    So what's the base case in this particular problem? When you're summing up integers from a to b, inclusive, if a happens to be bigger than b, then the answer is 0 - there aren't any numbers in the range! Therefore, we'll structure our solution as follows:

    1. If a > b, then the answer is 0.
    2. Otherwise (a ≤ b), get the answer as follows:
      1. Compute the sum of the integers between a + 1 and b.
      2. Add a to get the answer.

    Now, compare this pseudocode to your actual code:

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

    Notice that there's almost exactly a one-to-one map between the solution outlined in pseudocode and this actual code. The first step is the base case - in the event that you ask for the sum of an empty range of numbers, you get 0. Otherwise, compute the sum between a + 1 and b, then go add a.

    So far, I've given just a high-level idea behind the code. But you had two other, very good questions. First, why doesn't this always return 0, given that the function says to return 0 if a > b? Second, where does the 14 actually come from? Let's look at these in turn.

    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. In order for sumInts(5, 5) to determine what sumInts(6, 5) is, we need to pause what we're doing and make a call to 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) now can compute 5 + sumInts(6, 5) to get back 5. It then returns it to the top-level caller.

    Notice how the value 5 was formed here. We started off with one active call to sumInts. That fired off another recursive call, and the value returned by that call communicated the information back to sumInts(5, 5). The call to sumInts(5, 5) then in turn did some computation and returned a value back to the caller.

    If you try this with sumInts(4, 5), here's what will happen:

    • sumInts(4, 5) tries to return 4 + sumInts(5, 5). To do that, it calls sumInts(5, 5).
      • sumInts(5, 5) tries to return 5 + sumInts(6, 5). To do that, it calls sumInts(6, 5).
      • sumInts(6, 5) returns 0 back to sumInts(5, 5).</li> <li>sumInts(5, 5)now has a value forsumInts(6, 5), namely 0. It then returns5 + 0 = 5`.
    • sumInts(4, 5) now has a value for sumInts(5, 5), namely 5. It then returns 4 + 5 = 9.

    In other words, the value that's returned is formed by summing up values one at a time, each time taking one value returned by a particular recursive call to sumInts and adding on the current value of a. When the recursion bottoms out, the deepest call returns 0. However, that value doesn't immediately exit the recursive call chain; instead, it just hands the value back to the recursive call one layer above it. In that way, each recursive call just adds in one more number and returns it higher up in the chain, culminating with the overall summation. As an exercise, try tracing this out for sumInts(2, 5), which is what you wanted to begin with.

    Hope this helps!

    0 讨论(0)
  • 2020-11-22 07:57

    I think the confusion is stemming from thinking of it as "the same function" being called many times. If you think of it as "many copies of the same function being called", then it may be clearer:

    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.

    For the second bit of confusion, I think it will be easier to spell out the recursion in English. Read this line:

    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.

    By the time you reach the the a > b condition being true, you have a (potentially arbitrarily) long stack of copies of the function all in the middle of being run, all waiting on the result of the next copy to find out what they should add to '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)

    0 讨论(0)
提交回复
热议问题