What is tail recursion?
In many cases, writing code in recursive mode is more intuitive than iteration. The following factorial is used as an example:
Def factorial (n ):
If n = 0:
Return 1
Return factorial (n-1) * n
However, if this function is expanded, it will become the following form:
Factorial (4)
Factorial (3) * 4
Factorial (2) * 3*4
Factorial (1) * 2*3*4
Factorial (0) * 1*2*3*4
1*1*2*3*4
1*2*3*4
2*3*4
6*4
24
It can be seen that a temporary variable is generated during each recursive call, which increases the memory usage of the process. In this way, when executing code with a deep number of recursive layers, in addition to the fearless memory waste, it may also cause the famous stack overflow error.
However, if you write the above functions as follows:
Def factorial (n, acc = 1 ):
If n = 0:
Return acc
Return factorial (n-1, n * acc)
Let's start with the following:
Factorial (4, 1)
Factorial (3, 4)
Factorial (2, 12)
Factorial (1, 24)
Factorial (0, 24)
24
Intuitively, we can see that this factorial function does not generate a series of gradually increasing intermediate variables during recursive calls, but stores the state in the acc variable.
This form of recursion is called tail recursion.
The definition of tail recursion, as the name implies, means that the final returned result in a function call is a simple recursive function call (or return result) or a tail recursion.
For example, code:
Def foo ():
Return foo ()
It is tail recursion. However, in addition to recursive function calls and other computations, return results cannot be regarded as tail recursion. For example:
Def foo ():
Return foo () + 1 # return 1 + foo () is the same
Tail recursion optimization
Code in the form of tail recursion can achieve the same efficiency and space complexity as code in the form of iteration. However, in most programming languages, function calls are simulated using a stack in the memory. In this way, Stack Overflow may still occur when tail recursive functions are executed.
The stack is used to implement the function call programming language. During a function call, you can analyze in advance how many parameters are passed in this function call and how many intermediate variables are generated, there is also the size of memory occupied by parameters and variables (usually the elements of this stack that contain all parameters and intermediate variables in this function call process are called frames ). In this way, the top pointer of the stack is directed to such a large memory offset before the call, so that the memory location of function parameters and intermediate variables is allocated before the call. When the function call is complete, the top pointer of the stack refers back to immediately clear the memory occupied by this function call. In addition, the stack is used to call functions, which is consistent with the semantics of most programming languages. For example, when calling a function, the memory space allocated by the caller can still be used, and the variables are still valid.
But for recursive functions, there will be a problem: each function call will increase the stack capacity a little. If you need to perform a deep hierarchy of recursive calls, each time you perform a recursive call, even if the tail recursion of intermediate variables is not generated, the memory usage of the entire process increases as the function call stack increases.
Theoretically, however, no intermediate variable is generated to store the tail recursion of the state. The same stack frame can be reused to implement all recursive function operations. Code Optimization in this form is called tail recursion optimization.
Python and tail recursion optimization
For the code compiled into the machine code execution (whether AOT or JIT), simply put, you just need to call... change the ret command to jump ..., the same stack frame can be reused. Of course there is a lot of additional work to do. There are many opportunities for the interpreter to dynamically modify stack frames for tail recursion optimization for the code to be interpreted.
However, the implementation of CPython does not support tail recursion optimization. By default, the number of recursive calls is limited to 1000 (which can be viewed through the sys. getrecursionlimit function ).
However, this does not mean that we cannot implement tail recursion optimization in Python. In the method of implementing tail recursion optimization, if you cannot directly control the generated machine code or modify the stack frame language during runtime for some reason, there is also a solution called Through trampolining.
The general implementation method of Through trampolining is to insert a trampolining function when calling a recursive function. When this trampoline function is called to call a real recursive function, and the function body of the recursive function is modified, it is not allowed to call the recursive function again, but directly returns the parameter of the next recursive call, the next recursive call is performed by the trampoline function. In this way, a layer-by-layer recursive call will become an iterative function call once by the trampoline function.
In addition, this Through trampolining tail recursion optimization may not be provided by the programming language itself (compiler/runtime). Some flexible languages can implement this process themselves. For example, here is a piece of implementation code using CPython. The full text of this code is as follows (with a slight modification, you can run it in Python3 ):
#! /Usr/bin/env python3
# This program shows off a python decorator (
# Which implements tail call optimization. It
# Does this by throwing an exception if it is
# It's own grandparent, and catching such
# Exceptions to recall the stack.
Import sys
Class TailRecurseException (BaseException ):
Def _ init _ (self, args, kwargs ):
Self. args = args
Self. kwargs = kwargs
Def tail_call_optimized (g ):
"""
This function decorates a function with tail call
Optimization. It does this by throwing an exception
If it is it's own grandparent, and catching such
Exceptions to fake the tail call optimization.
This function fails if the decorated
Function recurses in a non-tail context.
"""
Def func (* args, ** kwargs ):
F = sys. _ getframe ()
If f. f_back and f. f_back.f_back \
And f. f_back.f_back.f_code = f. f_code:
Raise TailRecurseException (args, kwargs)
Else:
While 1:
Try:
Return g (* args, ** kwargs)
Failed T TailRecurseException as e:
Args = e. args
Kwargs = e. kwargs
Func. _ doc _ = g. _ doc __
Return func
@ Tail_call_optimized
Def factorial (n, acc = 1 ):
"Calculate a factorial"
If n = 0:
Return acc
Return factorial (n-1, n * acc)
Print (factorial (10000 ))
# Prints a big, big number,
# But doesn' t hit the recursion limit.
@ Tail_call_optimized
Def fib (I, current = 0, next = 1 ):
If I = 0:
Return current
Else:
Return fib (I-1, next, current + next)
Print (fib (10000 ))
# Also prints a big number,
# But doesn' t hit the recursion limit.
Only a tail_call_optimized modifier is exposed, and the function that meets the conditions can be optimized in tail recursion.
The implementation principle of this code is the same as the above mentioned Through trampolining. However, as a pure Python code, the decorator cannot modify the decorated function body so that the function only returns the parameters required for the next recursion and does not call the function recursively. However, its artifact lies in that an exception is thrown every time a function is called recursively by the decorator, and then the called parameters are written into the exception object, capture this exception in the trampoline function, read the parameter, and call it iteratively.
To better understand the application of tail recursion, write a program to practice. The length of a single-chain table is solved using direct recursion and tail recursion. The C language implementation procedure is as follows:
# Include <stdio. h>
# Include <stdlib. h>
Typedef struct node
{
Int data;
Struct node * next;
} Node, * linklist;
Void InitLinklist (linklist * head)
{
If (* head! = NULL)
Free (* head );
* Head = (node *) malloc (sizeof (node ));
(* Head)-> next = NULL;
}
Void InsertNode (linklist * head, int d)
{
Node * newNode = (node *) malloc (sizeof (node ));
NewNode-> data = d;
NewNode-> next = (* head)-> next;
(* Head)-> next = newNode;
}
// Directly recursively calculate the length of the linked list
Int GetLengthRecursive (linklist head)
{
If (head-> next = NULL)
Return 0;
Return (GetLengthRecursive (head-> next) + 1 );
}
// Calculate the length of the linked list using tail recursion. The variable acc is used to save the length of the current linked list and accumulate the length continuously.
Int GetLengthTailRecursive (linklist head, int * acc)
{
If (head-> next = NULL)
Return * acc;
* Acc = * acc + 1;
Return GetLengthTailRecursive (head-> next, acc );
}
Void PrintLinklist (linklist head)
{
Node * pnode = head-> next;
While (pnode)
{
Printf ("% d->", pnode-> data );
Pnode = pnode-> next;
}
Printf ("-> NULL \ n ");
}
Int main ()
{
Linklist head = NULL;
Int len = 0;
InitLinklist (& head );
InsertNode (& head, 10 );
InsertNode (& head, 21 );
InsertNode (& head, 14 );
InsertNode (& head, 19 );
InsertNode (& head, 132 );
InsertNode (& head, 192 );
PrintLinklist (head );
Printf ("The length of linklist is: % d \ n", GetLengthRecursive (head ));
GetLengthTailRecursive (head, & len );
Printf ("The length of linklist is: % d \ n", len );
System ("pause ");
}
Summary
Recursion is generally used to solve three types of problems:
(1) data is defined recursively. (Fibonacci function, factorial of n)
(2) the problem solution is implemented recursively. (Backtracking)
(3) the data structure is defined by recursion. (Tree traversal, Graph Search)
Disadvantages of recursion:
Recursive solving is less efficient than common algorithms, such as common loops. Therefore, we should avoid recursion whenever possible, unless there is no better algorithm or a specific situation, recursion is more appropriate. During the recursive call process, the system opens a stack for storing the return points and local volumes of each layer. Therefore, excessive recursion times may cause stack overflow.