Let's calculate factorial n! = 1 x 2 x 3 x ... x n , which is fact(n) represented by a function, as you can see: Fact (n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x N = fact (n-1) x n
Therefore, fact(n) can be expressed as n x fact(n-1) , only n=1 need special handling.
So, it fact(n) is written in a recursive way:
def fact(n): if n==1: return 1 return n * fact(n - 1)
Above is a recursive function.
The advantage of recursive functions is that they are simple in definition and clear in logic. In theory, all recursive functions can be written in a circular way, but the logic of the loop is not as clear as recursion.
The use of recursive functions requires careful prevention of stack overflow. In the computer, the function call is implemented through a stack (stack) of this data structure, each time into a function call, the stack will add a stack of frames, whenever the function returns, the stack will be reduced by a stack of frames. Because the size of the stack is not infinite, there are too many recursive calls that can cause the stack to overflow. You can try fact(1000) :
>>> fact(1000)Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in fact ... File "<stdin>", line 4, in factRuntimeError: maximum recursion depth exceeded
Recall the definition of computational n!: Calculate n times (n-1) in each active period, let n=n-1 and continue the process until n=1. This definition is not tail-recursive, because the return value for each active period relies on the return value of n multiplied by one of the following active periods, so the stack frame that is generated for each call will have to be saved on the stack until the return value of the next child call is determined.
The method of solving recursive call stack overflow is optimized by tail recursion , in fact, the effect of tail recursion and loop is the same, so it is possible to think of the loop as a special tail recursive function. Now let's consider defining the computational n! in the form of tail recursion . process.
Tail recursion means that when a function returns, it calls itself, and the return statement cannot contain an expression. In this way, the compiler or interpreter can optimize the tail recursion, so that the recursive itself, regardless of the number of calls, only occupy a stack frame, there is no stack overflow situation.
The above fact(n) function, because of the introduction of the return n * fact(n - 1) multiplication expression, is not a tail recursion. To change to a tail recursion, more code is needed, mainly to pass the product of each step into the recursive function:
def fact(n): return fact_iter(1, 1, n)def fact_iter(product, count, max): #count为维护递归层次的深度 if count > max: return product return fact_iter(product * count, count + 1, max)
As you can see, return fact_iter(product * count, count + 1, max) only the recursive function itself is returned, product * count and count + 1 it is evaluated before the function call, without affecting the function call.
The feature of the tail recursive function is that it is not necessary to do anything in the regression process, which is important because most modern compilers use this feature to automatically generate optimized code. When the compiler detects that a function call is tail-recursive, it overwrites the current activity record instead of creating a new one in the stack. The compiler can do this because the recursive call is the last statement to be executed during the current active period, so there is nothing else to do in the stack frame when the call returns, so there is no need to save the stack frame. By overwriting the current stack frame instead of adding one on top of it, the stack space used is greatly reduced, which makes the actual running efficiency even higher. Although the compiler can optimize the stack overflow problem caused by tail recursion, in programming, we should try to avoid the appearance of tail recursion, because all the tail recursion can be replaced by a simple goto loop.
If we calculate fact(5) , we can see the calculation process according to the function definition as follows:
===> fact(5)===> 5 * fact(4)===> 5 * (4 * fact(3))===> 5 * (4 * (3 * fact(2)))===> 5 * (4 * (3 * (2 * fact(1))))===> 5 * (4 * (3 * (2 * 1)))===> 5 * (4 * (3 * 2))===> 5 * (4 * 6)===> 5 * 24===> 120
做尾递归优化后,
fact(5)The corresponding fact_iter(1, 1, 5) call is as follows:
===> fact_iter(1, 1, 5)===> fact_iter(1, 2, 5)===> fact_iter(2, 3, 5)===> fact_iter(6, 4, 5)===> fact_iter(24, 5, 5)===> fact_iter(120, 6, 5)===> 120
It is easy to see that the normal linear recursion consumes more resources than the tail recursion, and in the implementation, each repeated process call makes the call chain grow longer. The system has to use stacks for data preservation and recovery. And there is no such problem with tail recursion. At the end of a recursive call, if optimized, the stack does not grow, so no amount of calls will cause the stack to overflow. Unfortunately, most programming languages are not optimized for tail recursion, and the Python interpreter is not optimized, so even if the above function is changed to
fact(n) tail recursion, it can cause the stack to overflow.
Code-optimized end-of-recursion