Compared with normal recursion, because the call of tail recursion is at the end of the method, the various States accumulated before the method have no significance for recursive call results, therefore, the data left in the stack in this method can be completely cleared, and the space can be used for the final recursive call. This optimization prevents recursion from accumulating on the call stack, which means that the real-time "infinite" recursion will not cause stack overflow. This is the advantage of tail recursion. Review tail recursion
Compared with normal recursion, because the call of tail recursion is at the end of the method, the various States accumulated before the method have no significance for recursive call results, therefore, the data left in the stack in this method can be completely cleared, and the space can be used for the final recursive call. This optimization prevents recursion from accumulating on the call stack, which means that the real-time "infinite" recursion will not cause stack overflow. This is the advantage of tail recursion.
Some friends may have already thought that the essence of tail recursion is to pass the "all states" required in the recursive method into the next call through the parameters of the method.
In the previous article, general recursion is like this:
// Recursive int factorial (int n) {if (n <= 2) {return 1;} else {return factorial (n-1) + factorial (n-2 );}}
To transform to tail recursion, we need to provide two accumulators:
int factorial_tail(int n,int acc1,int acc2){ if (n < 2) { return acc1; } else { return factorial_tail(n-1,acc2,acc1+acc2); }}
Therefore, the initial values of the two accumulators must be provided during the call: factorial_tail (n, 1, 1)
Concept of Continuation Passing Style
Speaking of Continuation, most programmers like me who started from C, Basic, and Pascal may not know. However, this concept is similar to common sense in the functional language community. many people always assume that you know the basic concept of continuation when discussing the problem, call-CC is directly referenced without any explanation. Therefore, if you do not know what the continuation actually refers to, you simply cannot understand what they are talking about.
The so-called continuation is actually a function call mechanism.
The call methods we are familiar with use stacks. Activation record or Stack frame is used to record all the context from the top-level function to the current function. A frame/record is the local context information of a function, including the values of all local variables and SP and PC pointer values (through static analysis, some local variable information does not need to be saved. in special cases such as the end call, no stack frame is required. However, logically, we think that all information is saved ). Before a function is called, the context information is often stored with some Pushes. when the function exits, the current record/frame is canceled to restore the record/frame of the previous caller.
For nested functions such as pascal, an extra pointer is required to save the frame address of the parent function. However, in any case, the system stores a post-import, first-out stack. Once a function exits, its frame is deleted.
Continuation is another function call method. Instead of using the stack to save the context, it stores the information in the continuation record. The difference between these continuation record and the activation record of the stack is that it does not adopt the linear method of post-in-first-out, and all records are made up of a tree (or graph ), calling another function from a function is equivalent to generating a subnode for the current node, and then moving the system Register to this subnode. The exit of a function is equal to returning from the current node to the parent node.
The deletion of these nodes is managed by the garbage collection. If this record is not referenced, it can be deleted.
What are the advantages of this call method over the stack method?
The biggest benefit is that it allows you to jump from any node to another node. Instead of following the layer-by-layer return method of the stack method. For example, in the current function, you can choose to return to the function as long as you have the node information of another function, instead of returning to your caller. You can also store your context information anywhere in a function, and then return it to your current position from any other function at an appropriate time in the future.
The Scheme language has a CallCC (call with current continuation) mechanism, that is, to obtain the current continuation and pass it to the function to be called, this function can return directly to the current continuation when appropriate.
Typical applications include exception, back-tracking algorithm, and coroutine.
When applying continuation to deal with exceptions, it is obvious that you only need to give the function that may throw exceptions a continuation record in the place where try is outside, this function can be directly returned to the place where the try statement is needed.
Exception-handling can also use continuation. C ++ and other languages generally use the policy of directly suspending the current function when exceptions occur. However, another policy is to allow resume. that is to say, after exceptions occur, it is possible that the exception handling module fixes the error and then chooses to resume execution of the code interrupted by the exception. The code that is interrupted by an exception can get the current continuation and pass it to the exception handling module. in this way, when resume is executed, it can directly jump to the exception location. The Back-tracking algorithm can also use a similar method to save the current continuation in some places, and then it can jump Back from other functions to the current statement.
The optimization of the Continuation mechanism is never a trivial problem. In fact, there are not many languages that adopt the continuation mechanism. In addition, the continuation call method depends on garbage collection, which is not intended for low-and middle-level languages such as c/c ++.
However, the idea of continuation is still useful. There is a design style called continuation-passing-style. The basic idea is: when some data needs to be returned, it is not directly treated as the return value of the function, but is accepted as a parameter called continuation. this parameter is a call-back function, it accepts the data and does what needs to be done.
For example:
x = f();print x;
Convert it to continuation-passing-style, and the following is:
f(print);
The f () function no longer returns x, but accepts a function, and then passes the x to the function.
This example may seem a bit confusing: why? One reason for a language like Haskell is that when a function may return different types of values based on different inputs, if the return value is used, an additional data structure must be designed to handle this difference. For example:
The return value of a function f (int) may be an int, two float or three complex. then, we can design our function f as follows:
f:: int -> (int->a) -> (float->float->a) -> (complex->complex->complex->a) -> a
This function accepts an integer parameter. three continuation callbacks are used to handle three different return conditions, and finally return the types returned by these three callbacks.
Another reason: for the simulated imperative monad, you can quickly return in the middle of the function (similar to the return or throw in C)
For C ++, in addition to processing different return types, another application can avoid unnecessary copying of returned values. Although c ++ currently has NRV optimization, the optimization itself is quite ambiguous, and each compiler has different implementations for NRV. The copy construction in C ++ has many side effects. as a programmer, it is not a comfortable task to know whether the side effects he has written have been executed several times.
Continuation-passing-style does not depend on any remote language features, and does not introduce any ambiguity. it may be used as a design choice. For example, for string concatenation, if you use the continuation-passing-style as follows:
Template
Void concat (const string & s1, const string & s2, F ret) {string s (s1); s. append (s2); ret (s); // here, it should be return (s), but we convert it to ret (s ).}
We can safely say that we have not introduced any unnecessary copies here, no matter what the compiler is.
Of course, the problem with continuation style is that it is not as intuitive as the direct return value, and the type system cannot guarantee that you have actually called ret (s ). Moreover, it requires a function object, and c ++ does not support lamda. defining many trivial functor will make the program very ugly.
You must weigh the advantages and disadvantages yourself.
Tail recursion and Continuation Passing Style
In my opinion, tail recursion is actually a Continuation Passing Style.
Continuation can be viewed as the current running stack. But we don't need the entire running stack. Therefore, we can package the computing results to be reused in an extra contiunation parameter and pass them on. In the most complex case, this context is just a stack data structure.
Here is an example:
One cow, 3rd years after birth, begins to generate one cow every year. According to this rule, how many cows are there in the nth year.
f(1)=1 f(2)=1 f(n)=f(n-1)+f(n-2)
Fibonacci series:, 13, 21, 34 ........
Change the problem slightly:
One cow, 4th years after birth, begins to generate one cow every year. According to this rule, how many cows are there in the nth year.
f(1)=1 f(2)=1 f(3)=1 f(n)=f(n-1)+f(n-3)
Fibonacci series: 1, 1, 1, 2, 3, 4, 6, 9, 13, 19, 28 ........
Then the problem is Generalized. the general description is as follows:
The number of cows in the nth year after birth starts to generate one in every year. According to this rule, the number of cows in the nth year is counted.
K = x-1 f (1) = 1... F (k) = 1 f (n) = f (n-1) + f (n-k)
Recursive solutions are still natural:
int fibonacci(int n, int k){ if(n <= k) return 1; int previousResult1 = fibonacci(n - 1); int previousResultK= fibonacci(n – k); int result = previousResult1 + previousResultK; return result;}
Change it to Tail Recursion. At this time, we need to track the first k results. In any case, k is a fixed number for each execution. We can use an array of k lengths to save the first k intermediate results without a longer stack structure. We can move the data in the k-length array to store the computing results currently needed. for details, refer to the move method. (Like the window concept in a network transmission protocol)
int[] alloc(int k){ int[] array = new int[k]; for(int i = 0; i < k; i++) { array[ i ] = 1; } return array;}void move(int[] array){ int k = array.length; int limit = k – 1; for(int i = 0; i < limit; i++) { array[i+1] = array[ i ]; }}int fibonacci(int n, int k){ int[] middleResults = alloc(k); return tail_recursive_fibonacci(1, middleResults[], n);}int tail_recursive_fibonacci(int currentStep, int[] middleResults, int n){ int k = middleResults.length; if(currentStep <= k) return 1; if(currentStep == n) return middleResults[0] + middleResults[k-1]; int nextStep = currentStep + 1; int currentResult = middleResults[0] + middleResults[k-1]; move(middleResults); middleResults[0] = currentResult; return tail_recursive_fibonacci(nextStep, previousResult1, previousResult2);}
Next we will change it to a loop. The key step is to use middleResults as a variable outside the loop body.
int fibonacci(int n, int k){ if(n == 1) return 1; if(n ==2) return 1; int[] middleResults = alloc(k); int last = k – 1; int result = 0; for(int i = 3; i <= n; i++) { result = middleResults[0] + middleResults[last]; move(middleResults); middleResults[0] = result; } return result;}
The preceding method is the most intuitive one, not the most economical one. For example, the currentStep parameter of the tail_recursive_fibonacci function can be omitted. in the last loop in the cyclic solution, you can directly break the result after calculating the result.
Additional reading
The topic list of this article is as follows:
- Recursive: recursive thinking
- Recursion: two conditions that must be met by recursion
- Recursion: recursive determination of string-to-text phenomena
- Recursive: recursive implementation of binary search algorithms
- Recursion: the efficiency of recursion
- Recursion: recursion and loop
- Let's talk about recursion: Is loop and iteration the same thing?
- Recursive computing and iterative computing
- On Recursion: Learn about tail recursion from Fibonacci
- Recursive: Tail recursion and CPS
- Recursion: add more knowledge about Continuation.
- Recursion: Tail recursion in PHP and its optimization
- Recursion: Tail recursion optimization from the perspective of Assembly
Address of this article: http://www.nowamagic.net/librarys/veda/detail/2331,welcome.