How exactly does tail recursion work?

前端 未结 8 1568
粉色の甜心
粉色の甜心 2020-12-07 07:39

I almost understand how tail recursion works and the difference between it and a normal recursion. I only don\'t understand why it doesn\'t

相关标签:
8条回答
  • 2020-12-07 07:45

    Compiler is enough intelligent to understand tail recursion.In case, while returning back from a recursive call,there is no pending operation and recursive call is the last statement, fall under the category of tail recursion. Compiler basically performs tail recursion optimization, removing stack implementation.Consider below code.

    void tail(int i) {
        if(i<=0) return;
        else {
         system.out.print(i+"");
         tail(i-1);
        }
       }
    

    After performing optimization , the above code is converted into below one.

    void tail(int i) {
        blockToJump:{
        if(i<=0) return;
        else {
         system.out.print(i+"");
         i=i-1;
         continue blockToJump;  //jump to the bolckToJump
        }
        }
       }
    

    This is how compiler does Tail Recursion Optimization.

    0 讨论(0)
  • 2020-12-07 07:46

    There are two elements that must be present in a recursive function:

    1. The recursive call
    2. A place to keep count of the return values.

    A "regular" recursive function keeps (2) in the stack frame.

    The return values in regular recursive function are composed of two types of values:

    • Other return values
    • Result of the owns function computation

    Let's look at your example:

    int factorial (int n) {
        if (n == 0) return 1;
        else return n * factorial(n - 1);
    }
    

    The frame f(5) "stores" the result of it's own computation (5) and the value of f(4), for example. If i call factorial(5), just before the stack calls begin to collapse, i have:

     [Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]
    

    Notice that each stack stores, besides the values i mentioned, the whole scope of the function. So, the memory usage for a recursive function f is O(x), where x is the number of recursive calls i have to made. So, if i needb 1kb of RAM to calculate factorial(1) or factorial(2), i need ~100k to calculate factorial(100), and so on.

    A Tail Recursive function put (2) in it's arguments.

    In a Tail Recursion, i pass the result of the partial calculations in each recursive frame to the next one using parameters. Let's see our factorial example, Tail Recursive:

    int factorial (int n) { int helper(int num, int accumulated) { if num == 0 return accumulated else return helper(num - 1, accumulated*num) } return helper(n, 1)
    }

    Let's look at it's frames in factorial(4):

    [Stack f(4, 5): Stack f(3, 20): [Stack f(2,60): [Stack f(1, 120): 120]]]]
    

    See the differences? In "regular" recursive calls the return functions recursively compose the final value. In Tail Recursion they only reference the base case (last one evaluated). We call accumulator the argument that keeps track of the older values.

    Recursion Templates

    The regular recursive function go as follows:

    type regular(n)
        base_case
        computation
        return (result of computation) combined with (regular(n towards base case))
    

    To transform it in a Tail recursion we:

    • Introduce a helper function that carries the accumulator
    • run the helper function inside the main function, with the accumulator set to the base case.

    Look:

    type tail(n):
        type helper(n, accumulator):
            if n == base case
                return accumulator
            computation
            accumulator = computation combined with accumulator
            return helper(n towards base case, accumulator)
        helper(n, base case)
    

    See the difference?

    Tail Call optimization

    Since no state is being stored on the Non-Border-Cases of the Tail Call stacks, they aren't so important. Some languages/interpreters then substitute the old stack with the new one. So, with no stack frames constraining the number of calls, the Tail Calls behave just like a for-loop in these cases.

    It's up to your compiler to optimize it, or no.

    0 讨论(0)
  • 2020-12-07 07:47

    The recursive function is a function which calls by itself

    It allows programmers to write efficient programs using a minimal amount of code.

    The downside is that they can cause infinite loops and other unexpected results if not written properly.

    I will explain both Simple Recursive function and Tail Recursive function

    In order to write a Simple recursive function

    1. The first point to consider is when should you decide on coming out of the loop which is the if loop
    2. The second is what process to do if we are our own function

    From the given example:

    public static int fact(int n){
      if(n <=1)
         return 1;
      else 
         return n * fact(n-1);
    }
    

    From the above example

    if(n <=1)
         return 1;
    

    Is the deciding factor when to exit the loop

    else 
         return n * fact(n-1);
    

    Is the actual processing to be done

    Let me the break the task one by one for easy understanding.

    Let us see what happens internally if I run fact(4)

    1. Substituting n=4
    public static int fact(4){
      if(4 <=1)
         return 1;
      else 
         return 4 * fact(4-1);
    }
    

    If loop fails so it goes to else loop so it returns 4 * fact(3)

    1. In stack memory, we have 4 * fact(3)

      Substituting n=3

    public static int fact(3){
      if(3 <=1)
         return 1;
      else 
         return 3 * fact(3-1);
    }
    

    If loop fails so it goes to else loop

    so it returns 3 * fact(2)

    Remember we called ```4 * fact(3)``

    The output for fact(3) = 3 * fact(2)

    So far the stack has 4 * fact(3) = 4 * 3 * fact(2)

    1. In stack memory, we have 4 * 3 * fact(2)

      Substituting n=2

    public static int fact(2){
      if(2 <=1)
         return 1;
      else 
         return 2 * fact(2-1);
    }
    

    If loop fails so it goes to else loop

    so it returns 2 * fact(1)

    Remember we called 4 * 3 * fact(2)

    The output for fact(2) = 2 * fact(1)

    So far the stack has 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

    1. In stack memory, we have 4 * 3 * 2 * fact(1)

      Substituting n=1

    public static int fact(1){
      if(1 <=1)
         return 1;
      else 
         return 1 * fact(1-1);
    }
    

    If loop is true

    so it returns 1

    Remember we called 4 * 3 * 2 * fact(1)

    The output for fact(1) = 1

    So far the stack has 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

    Finally, the result of fact(4) = 4 * 3 * 2 * 1 = 24

    The Tail Recursion would be

    public static int fact(x, running_total=1) {
        if (x==1) {
            return running_total;
        } else {
            return fact(x-1, running_total*x);
        }
    }
    
    
    1. Substituting n=4
    public static int fact(4, running_total=1) {
        if (x==1) {
            return running_total;
        } else {
            return fact(4-1, running_total*4);
        }
    }
    

    If loop fails so it goes to else loop so it returns fact(3, 4)

    1. In stack memory, we have fact(3, 4)

      Substituting n=3

    public static int fact(3, running_total=4) {
        if (x==1) {
            return running_total;
        } else {
            return fact(3-1, 4*3);
        }
    }
    

    If loop fails so it goes to else loop

    so it returns fact(2, 12)

    1. In stack memory, we have fact(2, 12)

      Substituting n=2

    public static int fact(2, running_total=12) {
        if (x==1) {
            return running_total;
        } else {
            return fact(2-1, 12*2);
        }
    }
    

    If loop fails so it goes to else loop

    so it returns fact(1, 24)

    1. In stack memory, we have fact(1, 24)

      Substituting n=1

    public static int fact(1, running_total=24) {
        if (x==1) {
            return running_total;
        } else {
            return fact(1-1, 24*1);
        }
    }
    

    If loop is true

    so it returns running_total

    The output for running_total = 24

    Finally, the result of fact(4,1) = 24

    0 讨论(0)
  • 2020-12-07 07:48

    The compiler is simply able to transform this

    int fac_times (int n, int acc) {
        if (n == 0) return acc;
        else return fac_times(n - 1, acc * n);
    }
    

    into something like this:

    int fac_times (int n, int acc) {
    label:
        if (n == 0) return acc;
        acc *= n--;
        goto label;
    }
    
    0 讨论(0)
  • 2020-12-07 07:48

    Here is a simple example that shows how recursive functions work:

    long f (long n)
    {
    
        if (n == 0) // have we reached the bottom of the ocean ?
            return 0;
    
        // code executed in the descendence
    
        return f(n-1) + 1; // recurrence
    
        // code executed in the ascendence
    
    }
    

    Tail recursion is a simple recursive function, where recurrence is done at the end of the function, thus no code is done in ascendence, which helps most compilers of high-level programming languages to do what is known as Tail Recursion Optimization, also has a more complex optimization known as the Tail recursion modulo

    0 讨论(0)
  • 2020-12-07 07:59

    You ask why "it doesn't require stack to remember its return address".

    I would like to turn this around. It does use the stack to remember the return address. The trick is that the function in which the tail recursion occurs has its own return address on the stack, and when it jumps to the called function, it will treat this as it's own return address.

    Concretely, without tail call optimization:

    f: ...
       CALL g
       RET
    g:
       ...
       RET
    

    In this case, when g is called, the stack will look like:

       SP ->  Return address of "g"
              Return address of "f"
    

    On the other hand, with tail call optimization:

    f: ...
       JUMP g
    g:
       ...
       RET
    

    In this case, when g is called, the stack will look like:

       SP ->  Return address of "f"
    

    Clearly, when g returns, it will return to the location where f was called from.

    EDIT: The example above use the case where one function calls another function. The mechanism is identical when the function calls itself.

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