Data Structure and algorithm 5: recursive (Recursion)
Data Structure and algorithm 5: recursive (Recursion)
Preface
In the book Joel, chief of the programmer's tribe, who talks about software, the chapter "the school only teaches java risks" mentions that,There are two traditional knowledge points in the computer science courses of the university, but many people have never understood them, that is, pointers and recursion.I am also sorry that I did not master these two knowledge points early. Some key knowledge points and some examples in this section are organized from teaching materials or networks, and the reference materials are listed at the end. If the error occurs, correct me.
Thinking list:
1) What programs have the potential for Recursive solutions?
2) How to Choose recursive or non-recursive algorithms?
3) general pattern of recursive program construction
1. Recursive Definition
First IntroductionRecursive DefinitionThis concept.
We usually define a new concept, always using defined or clearly meaningful terms, and recursive definition is a definition defined based on our own concepts.
For example, the factorial function definition:
For example, the Fibonacci series is defined as follows:
Recursive definition has two main functions: one is to generate new elements, and the other is to determine whether an element belongs to a set.
Functions with such recursive definitions are easily programmed.
2. How does recursion implement the computing process?
Let's assume that the code for converting the factorial function to a program is as follows:
//using recursionint factr(int n){ if ( n ==0) return 1; return n*factr(n-1);}
If we call fact (3), 6 is returned,I have a question: I don't seem to have done anything?
If we choose its iteration implementation:
//using iterationint facti(int n){ int result = 1; for(int i = 1 ; i <= n;i++) result *= i; return result;}
It seems that we are using the tired multiplication to calculate our factorial, and it is clear at once.
Our question is: how is the computing process executed in Recursive Implementation?
First, you need to understand the general situation of function calling in the system (this is not detailed here. For more in-depth and comprehensive information, refer to: Wiki Call stack ). In programs written in advanced languages, the links and information exchange between the called function and the called function are implemented through stacks. (For this section, refer to [1 ])
Generally, the system needs to do three things before running the called function, including:
Pass all real parameters, return address, and other information to the local Variable Allocation area of the called function to save as the called function, and transfer the control to the entry of the called function.
The system also needs to complete three tasks before the called function returns the called function:
Save the computing result of the called function. Release the data area of the called function. Transfer the control to the called function based on the returned address of the called function.
To sum upThe processing elements of a function call include:Function Control Transfer (using the function entry address and return address), Space allocation of local variables,Transmission of real parameters and form parameters, Function return result Transmission.The status of a function is determined by a 5-tuple, that is, the function (Entry address, return address, form parameter value, local variable value, return value ). The data area that stores all this information is calledActivity records(Activation record) or stack frame, which allocates space on the runtime stack. Activity records, get the dynamically allocated space when the function starts execution, and release the space when the function exits. The activity history of the main function is longer than that of other activity records.
Note that when the activity record saves the function parameters, the address can be either passed or passed. If it is a value transfer, the copy of the data element is saved, if an array is transmitted or referenced, the activity record contains the address (first address of the array) of the first element of the array or the address of the variable. At the same time, for local variables, activity records only contain their descriptors and pointers pointing to the locations where they are stored.
Simple Function calling process, such as the following code:
int main(){ int m,n; ... /*110*/ first(m,n); /*111*/ ... }int first(int s,int t){ int i; ... /*210*/ second(i); /*211*/ ...}int second(int d){ int x,y ...}
The activity record stack formed during function call is as follows:
For recursive function calls, it seems that we have completed the function without doing anything. In fact, during the process of recursive function calls, the activities of the function are continuously allocated and recycled, the computing process is ongoing.
Recursive call is not a function call on the surface, but a function instance calls another instance of the same function.(Refer to [2 ])
We will rewrite the factorial function above and mark an address number (this is a rough number, in fact the underlying machine address is not like this ):
Int main () {/* 102 */int n = factr (3);/* 103 */std: cout <"Factorial of" <
Then we call fact (3) in the main function to execute the activity record process as follows:
It can be seen that in actual recursive calls, the system continuously allocates activity records, calling from main () ---> fact (3) ---> fact (2) --> fact (1) ---> fact (0) goes deep layer by layer, and then rolls back layer by layer until it is in the main function. Because the local variables of the function are saved in the activity record, mutual interference between each call can ensure accurate calculation results when a called function is returned, and return the functions of the previous layer. The activity record stack is a good method for analyzing recursive Programs.
3. Recursive category
Recursion has many levels and many different levels of complexity.
We will learn from tail recursion and non-tail recursion, direct recursion and indirect recursion for classification.
For example, tail recursion. Tail recursion means that only one recursive call is used at the end of the function implementation. Tail recursion is characterized by that when a call is made, no other remaining statements in the function are to be executed, and there are no other direct or indirect recursive calls before this.
Examples of tail recursion:
# Include
# Include
# Include
Using namespace std; void printListi (list
: Iterator itCur, list
: Iterator end); void printListr (list
: Iterator itCur, list
: Iterator end); int main (int argc, char ** argv) {list
IList; for (int I = 0; I <10; I ++) iList. push_back (I); if (argc = 2 & string (argv [1]) = "-r") printListr (iList. begin (), iList. end (); else printListi (iList. begin (), iList. end ();} // using tail recursionvoid printListr (list
: Iterator itCur, list
: Iterator end) {if (itCur = end) {std: cout <
: Iterator itCur, list
: Iterator end) {while (itCur! = End) std: cout <* itCur ++ <""; std: cout <
Here, tail recursion or iteration is used to output the content of the linked list. We can see that tail recursion is only a deformation loop and can be easily replaced by loops.
Tail recursion is not recommended in languages with cyclic structures.
Except for tail recursion, non-tail recursion exists, for example:
# Include
# Include
Void reverse1 (); void reverse2 (); void reverse3 (); int main (int argc, char ** argv) {std: cout <"input somethind:" <
Three Versions of functions are provided to help you understand the recursive call features. Version 1 will reverse output the input. You can analyze the output of the other two versions. Of course, you can use stacks or caches to implement iterative versions of reverse output.
The tail and non-tail recursion above are both direct recursion, that is, the function itself calls itself. In another case, a function indirectly calls itself through other functions, such as f () --> g () --> f.
There is also the so-called nested recursion, that is, the function is not only defined according to itself, but also passed as a parameter of the function. For example, the Ackermann Function:
This situation is complicated.
<喎?"http: www.bkjia.com kf ware vc " target="_blank" class="keylink">
VcD4KPHA + PGJyPgo8L3A + signature/7Oyszi0tG + signature + 4 + signature/Signature + signature + tcTKtc/Wo6yyzr + signature + tcTKtc/Signature + signature/Signature/ authorization + authorization/tM7SxravtcS5/authorization + 5/authorization + 1xMXM0tG + authorization/Oqsbmyv2jrNTy1 + authorization + PGJyPgo8L3A + authorization = "brush: java; "> // using recursionvoid hanoiRecursion (int n, char src, char mid, char target) {if (n = 0) {return; // do nothing} hanoiRecursion (n-1, src, target, mid); // move the up n-1 stack to mid ++ g_stepCnt; cout <"(" <
"<
DStack [3]; if (n <= 0) return; // put n disk at stack 0 for (int I = n; I> 0; I --) dStack [0]. push (I); int lastItem =-1; // record last moved disk while (! DStack [0]. empty () |! DStack [n % 2 + 1]. empty () {// pick the smallest and not the last moved disk to move int stackNum = 0, moveItem = n + 1; for (int I = 0; I <3; I ++) {if (dStack [I]. empty () | dStack [I]. top () = lastItem) continue; if (dStack [I]. top () <moveItem) {stackNum = I; moveItem = dStack [I]. top () ;}} lastItem = moveItem; ++ g_stepCnt; // move odd-numbered disk clockwise, move even-numbered disk counter-clockwise int ta Rget = (moveItem % 2 = 0 )? (StackNum + 2) % 3 :( stackNum + 1) % 3; cout <"<
"<
The two methods move at least times.
How to calculate the number of plate moves?
One nature is that, as long as the total number of dishes is determined, the total number of times to move from where to which the destination pillar is the same.
Assume that n plates are moved h (n), the calculation is as follows:
It can be seen that when n = 64, the number is extremely large and cannot be solved within the effective time.
The iterative version of the Hanoi Tower can be implemented using bit operations, but it seems to be more skillful. If you are interested, refer to: How does this work? Weird Towers of Hanoi Solution.
4.2 Koch Snow
Koch snowflake problems, design to recursive implementation problems. For a general introduction, refer to the Wiki koch snow. Here we will mainly introduce the sections related to Recursive Implementation.
First, an OpenGL plot is given as follows:
There are three important mathematical questions about the implementation of this snowflake.
1) Drawing Rules -- calculation using the rotation angle and the coordinates of the circle
First, find the rule.
The above are 0 fragment, 1, 2, and 3 fragment graphs respectively, no matter how the fragment has an important property (I did not expect it at the beginning, later, I observed the implementation code of the teaching material ).
For the first time, the O2 point is calculated from the O1 point, and O2 is on the circumference, and the formula of the Center Coordinate is used:
prevX = prevX + (side/3)*cos(angle*PI / 180.0);prevY = prevY + (side/3)*sin(angle*PI / 180.0);
After calculation, the O3 point is calculated from the O2 point. This time, on the O2 circumference, the circumference angle is + 60, and the following angle is followed by-120 degrees, + 60, in this way, the painting ends. The angle between the second side and the first side is added with-120 degrees, and the third side and the second side are also added with-120 degrees. This angle is global, which is important.
2) The perimeter is infinite.
For each subdivision, the number of edges is four times that of the original one, and the length of the edges is 1/3 of the previous one:
When n tends to be infinite, the length does not converge and is infinite.
3) The area is limited.
The area derivation is also relatively simple. After each fragment, the area is increased on the basis of the front, and the added part is the area of the small triangle that expands outward. If the original side length is set to a, the area of the first fragment is calculated as follows:
By finding the law, we can calculate the area where n tends to be infinite:
From this we can see that Koch snowflake is infinitely large in a limited area.
When using OpenGL to draw Koch snowflake, you only need to save all the points and then render them once.
The key implementation is as follows:
//predefined variablesstd::vector< glm::vec4 > vertexVec;//hold pointsfloat prevX = 0.0f,prevY =0.0f;//the previous pointint angle = 0;float side = 3.0f;int level = 6;//prepare snow datavoid prepareData(){ float originX = 0,originY =0; vertexVec.push_back(glm::vec4(originX,originY,0,1)); for(int i=0;i< 3;i++) { drawFourLine(side,level); angle += -120; }}//draw four linesvoid drawFourLine(float side,int level){if (level == 0){ prevX = prevX + (side/3)*cos(angle*PI / 180.0); prevY = prevY + (side/3)*sin(angle*PI / 180.0); vertexVec.push_back(glm::vec4(prevX,prevY,0,1));}else{drawFourLine(side/3,level-1);angle += 60;drawFourLine(side/3,level-1); angle += -120; drawFourLine(side/3,level-1); angle += 60; drawFourLine(side/3,level-1);}}
The Code shows that we actually think of a snowflake as composed of three 4 segments, and each segment of a 4 segment can be further divided into four segments. In special cases, for example, if there is no fragment, there is only one edge, this edge can be seen as a special case with four segments.
4.2 full sorting
I have encountered a problem of full arrangement, that is, a string with no repeated values is given and its full arrangement is given, for example, AB, and ba.
In fact, the basic algorithm is described as follows:
Permutaion (input, output) If there is only one character, add the character to output; otherwise: each time a different character is taken from input as the header character head, and then the remaining leftPart is taken out for arrangement (a set of substrings is returned) add the header to the header of each result arranged in the remaining part to add the result to the output
Simply put, it is to fix a header, and then arrange the remaining substrings in full order, link each result string in full arrangement of the header and the substrings to get a complete and complete arrangement.
Repeat this process for a substring until there is only one character, it does not need to be arranged, and it can be returned directly.
Algorithm Implementation:
// permutation the input string and save it to resultvoid permutation(string input,vector
&result){ if(input.length() == 1) { result.push_back(input); return; } for(string::size_type i= 0;i < input.length();++i) { string leftPart = input; leftPart.erase(i,1);//get left part vector
strVec; permutation(leftPart,strVec);// use left part to permutate // add this char with left part result for(vector
::iterator it = strVec.begin();it != strVec.end();++it) result.push_back(input[i] + *it); }}
For example, if "abc" is input, the output result is:
Permutation of: abc ,kind: 6abcacbbacbcacabcba
There are still many procedures implemented by recursion, such as the Maze problem and the eight queens problem.
5. recursive and non-recursive Selection
I wrote a program for the Fibonacci series when I was learning python, as follows:
def fibr(n): """get the n-th Fibonacci series number,using recursion""" global callCnt callCnt += 1 # count how many time function called if n < 2: return n return fibr(n-1)+fibr(n-2)
Of course, this callCnt is added later. The program runs normally, but when I input n = 40, n = 100, the program seems to have crashed, and then I began to blame python for its low efficiency (I didn't analyze the complexity at the beginning, it is indeed wrong with Python :).
Let's take a look at the actual situation (rough time estimation ):
~ python3 fibr.py 30fib(30)=832040 called 'recursive function fibr' 2692537 times consumed 1029.3409824371338 ms**************************************************~ python3 fibr.py 40fib(40)=102334155 called 'recursive function fibr' 331160281 times consumed 125293.18809509277 ms
Now, we know that
Recursively calling the Fibonacci sequence, for example, when n = 40, the function fiber r called more than 0.3 billion times, and it took more than two minutes to complete the computation!
An iterative version is also written, which roughly compares the computing time of c ++/Python after the redundant time statistics and function call statistical statements in the program are removed:
By analyzing the above data, we can conclude that Iteration Algorithms are more efficient than recursion algorithms, C ++ is about 10 times faster than Python in interpreted language (this does not mean python has no advantages in other aspects ).
The following is a simple analysis of the Recursive Implementation and iterative implementation of the Fabonacci series.
The Recursive Implementation can be summarized as follows:
That is to say, for Fib (n), the execution function calls 2Fib (n + 1)-once During computation, and the addition Fib (n + 1)-once.
For iterative implementation:
//using iterationlong long fibi(int n){ if(n < 2) return n; else { long long last =0; long long cur = 1; for(;n > 1;n--) { long long tmp = cur; cur += last; last = tmp; } return cur; }}
For n> 1, enter the for loop and perform (n-1) times in total. Three copy operations are performed in each loop, which imply an addition operation. Therefore, a total of three (n-1) values and (n-1) addition operations are required.
The number of columns in the Fibonacci sequence increases rapidly, and the iteration algorithm does not need the function overhead for recursive calling. Addition and value assignment operations are also less than the recursive version. Therefore, it is inappropriate to use the recursive algorithm for the number of columns in the Fibonacci sequence, the iterative version should be adopted.
This example tells us that, although recursive algorithms are easy to write, they should be implemented by recursion or iteration, depending on the situation; it is best to analyze the complexity and overhead of iteration and recursive version implementation, or compare the algorithm execution efficiency on the actual machine.
6. Summary
The recursive algorithms are summarized as follows:
1) if the problem to be solved can be decomposed into a feature that has the same characteristics as the original problem, Recursive Implementation can be used.
For example, after the mobile Hanoi tower is decomposed, the above (n-1) tower is moved from one tower to another. For example, in the full arrangement problem, after taking one as the header, for the remaining elements, it also requires full arrangement. For the Maze problem, it always starts from the current position. If it is the end position, it stops. Otherwise, try to walk out of the maze in four directions, and each time you go to a new position, make the same decision.
2) there is a common mode for writing recursive Programs, that is, the program has a base or an exit, and the other part is to call itself. Please note that recursive Programs must be well selected; otherwise, they will not be able to return in the dreamspace.
Exit part. For example, if there is only one Hanoi tower on the plate, you only need to move it. If there is only one character in the full arrangement, you only need to return it. These are the exit of the program. A pattern of recursive Programs is:
Function (param) if Exit: process and return; otherwise:... function (param ).....
3) who supports recursive calls? One is the support of the language itself, the other is the support of the operating system runtime stack and possible hardware support.
Recursive functions do not avoid stack overhead during recursive calls, but they do not mean that their efficiency must be lower than that of iterative methods.
For example, iterative implementation and recursive implementation need to be compared and analyzed to determine which algorithm is used for implementation.
For more information, see [4.
Finally, I post an interesting explanation of recursion on StackOverflow:
A child couldn't sleep, so her mother told her a story about a little frog, who couldn't sleep, so the frog's mother told her a story about a little bear, who couldn't sleep, so the bear's mother told her a story about a little weasel... who fell asleep. ...and the little bear fell asleep; ...and the little frog fell asleep;...and the child fell asleep.
References:
[1] Data Structure Yan Weimin Wu weiming Tsinghua University Press
[2] Adam Drozdek, the third edition of Data Structure and algorithm c ++, compiled by Tsinghua University Press
[3] Wiki Tower of Hanoi
[4] What is recursion and when shoshould I use it?
[5] Wiki Koch snowflake
[6] a simpler iterative solution to the towers of hanoi problem