From: http://blog.chinaunix.net/uid-26941022-id-3331402.html
New students in computer science often have difficulty understanding the concept of recursive programming. Recursive thinking is difficult because it is like circular reasoning ). It is not an intuitive process. When we direct others to do things, we seldom recursively direct them.
For people who are new to computer programming, there is a simple definition of recursion: recursion occurs when a function calls itself directly or indirectly.
Typical examples of Recursion
Computing factorial is a classic example of recursive programming. The factorial used to calculate a certain number is to use that number to multiply all the smaller ones including 1. For example,factorial(5)
Equivalent5*4*3*2*1
, Andfactorial(3)
Equivalent3*2*1
.
An interesting feature of factorial is that the factorial of a certain number is equal to the factorial of the starting number multiplied by a number smaller than the Starting number. For example,factorial(5)
And5 * factorial(4)
Same. You may write a factorial function like this:
Listing 1. The first attempt of the factorial function
int factorial(int n){ return n * factorial(n - 1);} |
However, the problem with this function is that it will run forever because it has no termination. The function is continuously called.factorial
. When the calculation reaches zero, there is no condition to stop it, so it will continue to call the factorial of zero and negative numbers. Therefore, our function requires a condition to tell it when to stop.
Since the factorial of a number smaller than 1 does not make any sense, we stop when we calculate the number 1 and return the factorial of 1 (that is, 1 ). Therefore, the true recursive functions are similar:
List 2. Actual recursive functions
As long as the initial value is greater than zero, this function can be terminated. The stop location is calledBaseline condition (base case). The baseline condition is the bottom-layer position of the recursive program. If you do not need to perform any operations at this position, you can directly return a result. All recursive Programs must have at least one baseline condition and must ensure that they will eventually reach a baseline condition; otherwise, the program will run forever until the program lacks memory or stack space.
Procedure
Every recursive program follows the same basic steps:
- Initialize the algorithm. Recursive programs usually need a seed value used at the beginning ). To complete this task, you can pass parameters to the function or provide an entry function. This function is non-recursive, but can set a seed value for Recursive calculation.
- Check whether the current value to be processed matches the baseline condition. If yes, it is processed and returned.
- Use smaller or simpler subquestions (or multiple subquestions) to redefine the answer.
- Run algorithms on subproblems.
- The expression that combines the result into the answer.
- Returned results.
Use inductive definition
Sometimes it is difficult to obtain simpler subproblems when writing recursive Programs. HoweverInductively-defined DatasetThis makes it easier to obtain sub-problems. A data set is defined according to its own data structure. This is calledInduction Definition).
For example, a linked list is defined according to itself. The node struct contained in the linked list consists of the data it holds and the pointer pointing to another node struct (or null, ending the linked list. Because the node struct contains a pointer to the node struct, it is called inductive definition.
Writing recursion using inductive data is very simple. Note that, similar to our recursive program, the definition of the linked list also includes a baseline condition-here it is a null pointer. Since the NULL pointer ends a linked list, we can also use the NULL pointer condition as the baseline condition for many recursive Programs Based on the linked list.
Linked List Example
Let's look at some examples of recursive functions based on linked lists. Suppose we have a list of numbers and add them together. Perform each step of the recursive process sequence to determine how it is applied to our summation function:
- Initialize the algorithm. The seed value of this algorithm is the first node to be processed. It is passed as a parameter to the function.
- Check the baseline conditions. The program needs to check whether the current node is a null list. If yes, zero is returned because the sum of all members in an empty list is zero.
- Use a simpler sub-question to redefine the answer. We can define the answer as adding the sum of the rest of the list to the content of the current node. To determine the sum of the rest of the list, we call this function for the next node.
- Merge results. After recursive call, we add the value of the current node to the result of recursive call.
The pseudo code and actual code of this function are as follows:
Listing 3. pseudo code of the sum_list Program
function sum_list(list l) is l null? yes - the sum of an empty list is 0 - return that data = head of list l rest_of_list = rest of list l the sum of the list is: data + sum_list(rest_of_list) |
The pseudo code of this program is almost identical to its scheme implementation.
Listing 4. C code of the sum_list Program
int sum_list(struct list_node *l){ if(l == NULL) return 0; return l.data + sum_list(l.next);} |
You may think that you know how to write this program without recursion to make it faster or better. We will discuss the speed and space of recursion later. Here, we will continue to discuss the recursion of the inductive dataset.
Suppose we have a string list and want to know whether a specific string is included in that list. The easier way to divide this problem is to go to a single node again.
The sub-question is: "whether the search string matchesThis nodeAre the strings in ?" If yes, you have an answer. If not, it is closer to the next step. What are the baseline conditions? There are two:
- If the current node has the string, it is the baseline condition ("true" is returned ").
- If the list is empty, it is also a baseline condition ("false" is returned ").
This program does not always meet the first baseline condition, because it does not always have a string being searched. However, we can assert that if the program cannot meet the first baseline condition, it can reach at least the second baseline condition when it reaches the end of the list.
Compile a program to ensure the correctness
Bug is a part of every programmer's daily life, because even the smallest loop and the simplest function call have bugs. Although most programmers can check the code and test the code bugs, they do not know how to prove that their programs will be executed as they imagined. For this reason, we will study some common sources of bugs, and then explain how to write the correct program and how to prove its correctness.
Bug Source: Status Change
Variable status changes are a major source of bugs. You may think that the program is keenly aware of how the variable changes its State. This is true sometimes in a simple loop, but not in a complex loop. Variables given in a loop can change the state in multiple ways.
For example, if you have a complexif
Statement, some branches may modify a variable, while other branches may modify other variables. In addition, the order is usually important, but it is difficult to ensure that the encoding order is correct in all cases. In general, due to these order problems, modifying a bug for a certain situation will introduce bugs for other situations.
To prevent such errors, developers need:
- Determine in advance how each variable gets its current value.
- Make sure that the variables have no double purpose. (Many programmers often use the same variable to store two related but slightly different values .)
- When the loop starts again, make sure all variables are in the state they should be in. (Setting new values for cyclic variables is neglected in rare use and test-less scenarios, which is a common programming error .)
To achieve these goals, we only need to develop a rule in programming:A variable is assigned only once and never modified!
What? (You are not credible !) This rule is unacceptable to many people. They are familiar with imperative, procedural, and object-oriented programming-variable assignment and modification are the basis of these programming technologies! Even so, for imperative language programmers, state changes are still the main cause of program design errors.
So how can I not modify variables during programming? Let's study the situations where variables are frequently modified and whether we can complete the task without modifying the variables:
- Use a variable again.
- Variable Condition modification ).
- Loop variable.
First, let's look at the first scenario to re-use a variable. A variable is usually re-used for different (but similar) purposes. For example, sometimes, in a part of a loop, an index pointing to the current position is required in the first half of the loop, and the rest of the loop needs an index before or after the index, many programmers use the same variable for both cases, but only incrementally process the variable as needed. When the current program is modified, this will undoubtedly make it difficult for programmers to understand these two purposes. To prevent this problem, the best solution is to create two independent variables and use the same method to obtain the second variable based on the first variable, just as writing the same variable.
The second case is that the conditional modification of the variable is a subset of the reuse problem, but sometimes we will keep the existing value and sometimes use a new value. Similarly, it is best to create a new variable. In most languages, we can use ternary operators.? :
To set the value of the new variable. For example, if we need to assign a new value to the new variable, the condition is that it is not greatersome_value
, We can write it like thisint
new_variable = old_variable > some_value ? old variable : new_value;
.
(We will discuss circular variables later in this article .)
When we solve the problem of changing the state of all variables, we can be sure that when we define variables for the first time, the definition of variables will be maintained throughout the entire lifecycle of the function. This makes the operation order much simpler, especially when modifying existing code. You do not have to worry about the order in which variables are modified, or what assumptions about their States at every moment.
When the state of a variable cannot be changed, a full definition of its origin is given at the moment and place of its declaration. You no longer need to search all the code to find out the incorrect or chaotic status.
What is a cyclic variable?
Now, the question is, how can we loop through a value assignment? The answer is:Recursive functions. In table 1, we understand the characteristics of loops and how they can compare with the features of recursive functions.
Table 1. Comparison loop and recursive functions
Features |
Loop |
Recursive functions |
Repeated |
To obtain results, execute the same code block repeatedly.continue Execute commands repeatedly. |
To obtain results, the same code block is repeatedly executed, and repeated execution is implemented by repeatedly calling itself as a signal. |
Conditions for termination |
To ensure that a loop can be terminated, one or more conditions must be met for its termination, and one of those conditions must be ensured that it can meet in some cases. |
To ensure termination, recursive functions require a baseline condition to stop recursion. |
Status |
Updates the current status when the loop is ongoing. |
The current status is passed as a parameter. |
It can be seen that there are many similarities between recursive functions and loops. In fact, we can think that loops and recursive functions can convert each other. The difference is that recursive functions are rarely forced to modify any variable-you only need to pass the new value as a parameter to the callback function call. This allows you to avoid all the benefits of using updatable variables while performing repetitive and stateful behavior.
Converts a common loop to a recursive function.
Let's look at a common loop for printing a report and learn how to convert it into a recursive function.
- This cycle prints the page number and header at each page.
- It is assumed that the rows of the report are grouped according to a certain number standard and that it is useful to understand a total number of these groups.
- Print the total number of groups at the end of each group.
For demonstration purposes, we omitted all secondary functions, assuming they exist and are executed as expected. The following is the code of our report printing program:
Listing 5. RePort Printing Program implemented with a general Loop
void print_report(struct report_line *report_lines, int num_lines){ int num_lines_this_page = 0; int page_number = 1; int current_line; /* iterates through the lines */ int current_group = 0; /* tells which grouping we are in */ int previous_group = 0; /* tells which grouping was here on the last loop */ int group_total = 0; /* saves totals for printout at the end of the grouping */ print_headings(page_number); for(current_line = 0; current_line < num_lines; current_line++) { num_lines_this_page++; if(num_lines_this_page == LINES_PER_PAGE) { page_number++; page_break(); print_headings(page_number); } current_group = get_group(report_lines[current_line]); if(current_group != previous_group) { print_totals_for_group(group_total); group_total = 0; } print_line(report_lines[current_line]); group_total += get_line_amount(report_lines[current_line]); }} |
The program intentionally leaves some bugs. Check whether you can find them.
As we constantly modify state variables, it is difficult to predict whether they are correct at any specific time. The same program is implemented recursively:
Listing 6. Reporting programs implemented recursively
void print_report(struct report_line *report_lines, int num_lines){ int num_lines_this_page = 0; int page_number = 1; int current_line; /* iterates through the lines */ int current_group = 0; /* tells which grouping we are in */ int previous_group = 0; /* tells which grouping was here on the last loop */ int group_total = 0; /* saves totals for printout at the end of the grouping */ /* initialize */ print_headings(page_number); /* Seed the values */ print_report_i(report_lines, 0, 1, 1, 0, 0, num_lines);}void print_report_i(struct report_line *report_lines, /* our structure */ int current_line, /* current index into structure */ int num_lines_this_page, /* number of lines we've filled this page */ int page_number, int previous_group, /* used to know when to print totals */ int group_total, /* current aggregated total */ int num_lines) /* the total number of lines in the structure */{ if(current_line == num_lines) { return; } else { if(num_lines_this_page == LINES_PER_PAGE) { page_break(); print_headings(page_number + 1); print_report_i( report_lines, current_line, 1, page_number + 1, previous_group, group_total, num_lines); } else { int current_group = get_group(report_lines[current_line]); if(current_group != previous_group && previous_group != 0) { print_totals_for_group(group_total); print_report_i( report_lines, current_line, num_lines_this_page + 1, page_number, current_group, 0, num_lines); } else { print_line(report_lines[current_line]); print_report_i( report_lines, current_line + 1, num_lines_this_page + 1, page_number, current_group, group_total + get_line_amount(report_lines[current_line]), num_lines); } } }} |
Note that all the numbers we use are always consistent. In almost any situation, as long as multiple states are modified, some lines of code cannot always have consistent numbers during the state change process. If you add a line to the code that changes the state in the program in the future, and the judgment of the variable status does not match the actual situation, it will be very difficult. After several modifications, an unpredictable bug may be introduced due to the order and status issues. In this program, all state changes are implemented by re-running recursive Programs with completely consistent data.
Tail-Recursive Function
In this way, I have shown you how loops are associated with recursive functions, and how to convert loops into recursive, non-state changing functions, in order to achieve higher maintainability than the previous program design and ensure the correct results.
However, the increasing number of stack spaces is a concern for the use of recursive functions. Indeed, as the number of calls increases, some types of recursive functions will linearly increase the use of stack space-however, there is a type of function, that isTail recursionNo matter how deep the recursion is, the stack size remains unchanged.
Tail recursion
When we convert a loop into a recursive function, recursive calling is the last thing the function does. If you observeprint_report_i
, You will find that nothing happens after recursive calling in the function.
This behavior is similar to a loop. When the loop reaches the end of the loop, or it executescontinue
That is the last thing it will do in the code block. Similarly, whenprint_report_i
When it is called again, nothing will be done after recursive points.
The last thing a function does is a function call (recursive or non-recursive), which is calledTail-call). The recursion that uses the end call is calledTail recursion. Let's take a look at some function call examples to understand what the meaning of the End call is:
Listing 12. Tail call and non-tail call
int test1(){ int a = 3; test1(); /* recursive, but not a tail call. We continue */ /* processing in the function after it returns. */ a = a + 4; return a;}int test2(){ int q = 4; q = q + 5; return q + test1(); /* test1() is not in tail position. * There is still more work to be * done after test1() returns (like * adding q to the result */}int test3(){ int b = 5; b = b + 2; return test1(); /* This is a tail-call. The return value * of test1() is used as the return value * for this function. */}int test4(){ test3(); /* not in tail position */ test3(); /* not in tail position */ return test3(); /* in tail position */} |
It can be seen that to make the call a real tail call, the result is returned before the end call function returns.You cannot perform any other operations..
Note that the actual stack structure of the function is not needed because no more work is done in the function. The only problem is that many programming languages and compilers do not know how to remove useless stack structures. If we can find a method to remove these unwanted stack structures, our tail recursive functions can run in a fixed-size stack.
Tail call Optimization
The method for removing the stack structure after the tail call is calledTail call Optimization.
So what is this optimization? We can answer that question by asking other questions:
- Which local variable does the function need to use after it is called at the end? No.
- What processing will be performed on the returned value? There is no processing.
- Which parameter will be used to pass to the function? None.
It seems that once the control is passed to the function called at the end, there will be no more useful content in the stack. Although it still occupies space, the function stack structure is useless at this time. Therefore, tail call optimization is to use the next stack structure for function calling at the end.OverwriteThe current stack structure while maintaining the original return address.
What we do is to process the stack in essence. Activation record is no longer required, so we will delete it and redirect the function called at the end to the called function. This means that we must manually re-compile the stack to simulate a return address so that the function called at the end can directly return to the function that calls it.
Here is an assembly language template that optimizes tail calls for those who are really keen on underlying programming:
Listing 7. Assembly Language template called at the end
;;Unoptimized tail-callmy_function: ... ... ;PUSH ARGUMENTS FOR the_function HERE call the_function ;results are already in %eax so we can just return movl %ebp, %esp popl %ebp ret;;Optimized tail-call optimized_function: ... ... ;save the old return address movl 4(%ebp), %eax ;save old %ebp movl (%ebp), %ecx ;Clear stack activation record (assuming no unknowns like ;variable-size argument lists) addl $(SIZE_OF_PARAMETERS + 8), %ebp ;(8 is old %ebp + return address)) ;restore the stack to where it was before the function call movl %ebp, %esp ;Push arguments onto the stack here ;push return address pushl %eax ;set ebp to old ebp movl %ecx, %ebp ;Execute the function jmp the_function |
It can be seen that the tail call uses more commands, but they can save a lot of memory. There are some restrictions on using them:
- When a function returns a called function, the latter must not depend on the parameter list that is still in the stack.
- The called function must not worry about the position currently referred to by the stack pointer. (Of course, it can be assumed that it is out of the range of its local variables .) This means you cannot use
-fomit-frame-pointer
Compile, all registers must be stored in the stack with reference%ebp
Instead%esp
.
- There cannot be any variable-length parameter list.
When a function calls itself in the end, the method is simpler. We only need to move the new value of the parameter to the old value, and then immediately jump to the function position after saving the local variable to the stack. Because we only jump to the same function, the returned address and the old%ebp
Is the same, and the stack size will not change. Therefore, the only thing we need to do before the jump is to replace the old parameter with the new parameter.
In this way, your program will have the testability of functional programs and the speed and memory features of imperative programs after you have paid for some commands. The only problem is that only a very small number of compilers have implemented tail call optimization. Scheme implementation must implement this optimization, and many other functional languages must also implement it. However, note that sometimes functional languages use stacks much different from imperative languages (or do not use stacks at all ), therefore, the method for implementing tail call optimization may be quite different.
The latest GCC version also contains tail recursive Optimization in a restricted environment. For exampleprint_report_i
Functions use-O2 in GCC 3.4 for Tail call optimization compilation. Therefore, the stack size used during runtime is fixed, rather than linear growth.
Conclusion
Recursion is a great art that makes it easier to confirm the correctness of a program without sacrificing performance, but it requires programmers to study program design from a new perspective. For new programmers, imperative programming is usually a more natural and intuitive starting point, which is why most programming instructions focus on imperative languages and methods. However, as programs become more complex, recursive programming allows programmers to better organize code in a maintained and logically consistent manner.
Blog recommendations
- Reprinted (12:36:09)
- Reprinted (12:41:49)
- Reprinted (12:45:12)
- Reprinted (14:17:09)
- Some reposts (22:18:07)