New students in computer science often find it difficult to understand the concept of recursive programming. Recursive thinking is difficult because it is very much like cyclic reasoning ( circular reasoning
). It is also not an intuitive process; when we direct others to do things, we rarely command them in a recursive manner.
Introduction
Recursive algorithm is an algorithm that directly or indirectly calls its own function or method. The essence of recursive algorithm is to decompose the problem into the sub-problem of the similar problem of scale reduction, and then recursive call method to represent the solution of the problem. Recursive algorithm is effective to solve a large class of problems, it can make the algorithm concise and easy to understand. Recursive algorithm, in fact, is the program itself called. It manifests itself in a program that often encounters the coding strategy of invoking itself, so that we can take advantage of the idea of a big complex problem to convert a large and complicated layer into a small problem that is similar to the original problem to solve the strategy. Recursion often gives us a very concise and very intuitive code situation, so that our coding greatly simplifies, but recursive thinking is really our conventional thinking inverse, we are usually from the top down thinking problems, and recursive trend from the bottom up thinking. This way we can see that we can solve very large problems with very few statements, so the main embodiment of recursive strategy is that the small amount of code solves a very complex problem.
The recursive algorithm solves the problem characteristic:
- Recursion is the invocation of itself in a method.
- When using the increment-to-return strategy, there must be a definite recursive end condition called a recursive exit.
- Recursive algorithm is usually very concise, but the recursive algorithm is less efficient in solving problems. Therefore, the recursive algorithm is generally not advocated for the design of the program.
- In the process of recursive invocation, the system opens up a stack for each layer's return point, local quantity and so on. Too many recursive times can cause stack overflow, etc., so generally do not advocate the use of recursive algorithm design program.
Recursive algorithm requirements. The recursive algorithm embodies the "repetition" generally has three requirements:
(1) The size of each call is reduced (usually halved);
(2) is a close link between two repetitions, the previous time to prepare for the next time (usually the previous output as the last input);
(3) is in the small scale of the problem must be given directly to the solution and no longer recursive call, so each recursive call is conditional (to the size of the scale does not reach the direct solution), unconditional recursive call will become a dead loop and can not end normally.
Calculate factorial from a recursive classic example
Computational factorial is a classic example of recursive programming. The factorial of a number is calculated by using that number to multiply all the smaller numbers, including 1. For example, factorial(5)
equivalent to 5*4*3*2*1
, but factorial(3)
equivalent to 3*2*1
.
An interesting feature of factorial is that the factorial of a number is equal to the number of the start (starting numbers) multiplied by the factorial of the number of entering primary one. For example, factorial(5)
with the 5 * factorial(4)
same. You will most likely write factorial functions like this:
123 |
int factorial (int n) { return n * factorial (n- 1);} |
( Note: The program examples in this article are written in C language )
However, the problem with this function is that it will run forever because it has no place to terminate. The function is continuously called factorial
. When the calculation is 0 o'clock, there is no condition to stop it, so it continues to invoke the factorial of 0 and negative numbers. Therefore, our function needs a condition that tells it when to stop.
Since the factorial of the number less than 1 does not make any sense, we stop when we calculate to the number 1 and return the factorial of 1 (that is, 1). Therefore, the true recursive function is similar to the following:
123456 |
int factorial (int n) { if (n = = 1) return 1; Else return n * factorial (n- 1);} |
Visible, this function can be terminated as long as the initial value is greater than 0. The location of the stop is called the baseline condition (base case). The baseline condition is the bottom-most position of the recursive program, where there is no need to operate again, and a result can be returned directly. All recursive programs must have at least one baseline condition and must ensure that they eventually reach a baseline condition, otherwise the program will run forever until the program lacks memory or stack space.
Fibonacci sequence
The Fibonacci sequence (Fibonacci Sequence), which was first used to describe the number of rabbits growing, was used in this sequence. Mathematically, the faipot sequence is defined in a recursive way:
In this way, the recursive procedure of the Fibonacci sequence can be written very clearly:
123456 |
int Fibonacci (int n) { if (n <= 1) return n; Else return Fibonacci (n1) + Fibonacci (n-2); } |
Basic steps for recursive programs
Each recursive program follows the same basic steps:
(1) 初始化算法。递归程序通常需要一个开始时使用的种子值(seed value)。要完成此任务,可以向函数传递参数,或者提供一个入口函数, 这个函数是非递归的,但可以为递归计算设置种子值。(2) 检查要处理的当前值是否已经与基线条件相匹配。如果匹配,则进行处理并返回值。(3) 使用更小的或更简单的子问题(或多个子问题)来重新定义答案。(4) 对子问题运行算法。(5) 将结果合并入答案的表达式。(6) 返回结果。
Using inductive definitions
Sometimes it is difficult to get a simpler sub-problem when writing a recursive program. However, using the () dataset defined by the generalization inductively-defined
can make it easier to obtain sub-problems. The dataset defined by the generalization is a data structure based on its own definition-this is called the inductive definition ( inductive definition
).
For example, a linked list is defined by itself. A linked list consists of two parts: the data it holds, and a pointer to another node structure (or NULL, ending the list). Since the node structure contains a pointer to the structure of the node, it is called the inductive definition.
Using inductive data to write a recursive process is very simple. Note that very similar to our recursive program, the definition of a list also includes a baseline condition--here is a NULL pointer. Because the null pointer ends a linked list, we can also use null pointer conditions as the baseline condition for many of the recursive programs based on the linked list.
Let's look at two examples below.
Linked List Summation example
Let's look at some examples of recursive functions based on linked lists. Suppose we have a list of numbers, and we want to add them up. Perform each step of the recursive process sequence to determine how it applies to our summation function:
(1) 初始化算法。这个算法的种子值是要处理的第一个节点,将它作为参数传递给函数。(2) 检查基线条件。程序需要检查确认当前节点是否为 NULL 列表。如果是,则返回零,因为一个空列表的所有成员的和为零。(3) 使用更简单的子问题重新定义答案。我们可以将答案定义为当前节点的内容加上列表中其余部分的和。为了确定列表其余部分的和, 我们针对下一个节点来调用这个函数。(4) 合并结果。递归调用之后,我们将当前节点的值加到递归调用的结果上。
So we can simply write out the recursive program of the sum of the linked list, the example is as follows:
12345 |
int sum_list (struct List_node *l) {if (l = = NULL)return 0; return l.data + sum_list (l.next);} |
Hanoi Tower Problem
The Hanoi (Hanoi Tower) problem is also a classic recursive problem, which is described as follows:
汉诺塔问题:古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上()。有一个和尚想把这64个盘子从A座移到B座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下,小盘在上。
Hanoi Tower Solving
- If you have only 1 plates, you do not need to use Tower B to move the plate directly from a to C.
- If there are 2 plates, you can first move plate 1 on plate 2 to B; Move plate 1 to C; Move plate 2 to C. This shows: You can use B to move 2 plates from A to C, of course, you can also use C to move 2 plates from A to B.
- If there are 3 plates, then according to the conclusion of the 2 plates, C can be used to move the two plates on plate 1 from A to B, the plate 1 from a to c,a into empty seats, and the two plates on the B to C by means of block A.
And so on, the above ideas can be extended to the case of n plates, the smaller n-1 plate as a whole, that is, we ask for the sub-problem, in order to use the B-tower as an example, you can use the empty tower B on plate A above the n-1 plate from A to B; move a largest plate to the c,a into empty tower With empty tower A, the n-2 plate on the B tower is moved to a, and the c largest plate is moved to c,b into empty tower ...
According to the above analysis, it is not difficult to write the program:
12345678910 |
void Hanoi (int n, char A, char B, char C) { if (n==1) { //end cond Ition Move (A, b); //' move ' can is defined to be a print function } else{ Hanoi (n-1,a,c,b); Move sub [n-1] pans from A to B move (A,C); //move the bottom (max) pan to C Hanoi (n-1,b,a,c); Move sub [n-1] pans from B to C }} |
Turn loops into recursion
Learn about the characteristics of loops in the table below to see how they compare to the properties of recursive functions.
Properties |
Loops |
Recursive Functions |
Repeat |
The same block of code is executed repeatedly in order to achieve the result, and repeated execution is performed to complete the code block or execute the Continue command signal. |
In order to obtain the result, the same code block is executed repeatedly, and repeated execution is performed repeatedly by invoking itself as a signal. |
Termination conditions |
To ensure termination, a loop must have one or more conditions in which it can be terminated, and it must be guaranteed to satisfy one of these conditions in some circumstances. |
To ensure termination, the recursive function needs to have a baseline condition to stop the function from recursion. |
State |
Updates the current state when the loop is in progress. |
The current state is passed as a parameter. |
As can be seen, recursive functions have many similarities with loops. In fact, loops and recursive functions can be thought of as being able to convert each other. The difference is that using recursive functions is rarely forced to modify any one variable--just pass the new value as an argument to the next function call. This allows you to get all the benefits of avoiding the use of updatable variables while being able to perform repetitive, stateful behavior.
The following is also a factorial example, the loop is written as:
12345678 |
int factorial (int n) { int product = 0; While (n>0) { product *= n; n--;} return product;} |
Recursive notation has been introduced in the second section, here is not repeated, you can compare.
Introduction to Tail recursion
One of the concerns about the use of recursive functions is the growth of stack space. Indeed, as the number of calls increases, some kinds of recursive functions linearly increase the use of the stack space-however, there is a class of functions, that is, the tail-recursive function, no matter how deep the recursion is, the size of the stack remains unchanged. The tail recursion belongs to the linear recursion, more accurately a subset of the linear recursion.
The last thing a function does is a function call (recursive or non-recursive), which is called a tail call ( tail-call
). Recursion using tail calls is called trailer recursion . 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.
Let's take a look at some of the tail and non-trailer function examples to understand what the tail call means:
123456789101112131415161718192021222324252627 |
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 no in tail position. * There is still + work to be * do 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 * /} |
Visible, to make the call a true tail call, no other action can be performed on its result until the tail call function returns.
note that the actual stack structure of the function is not needed because no more is done in the function. The only problem is that many programming languages and compilers do not know how to remove unused stack structures. If we can find a way to get rid of these unwanted stack structures, our tail-recursive function can be run in a fixed-size stack.
The method of removing the stack structure after a tail call is called tail-Call optimization .
So what is this optimization? We can answer that question by asking other questions:
(1) 函数在尾部被调用之后,还需要使用哪个本地变量?哪个也不需要。(2) 会对返回的值进行什么处理?什么处理也没有。(3) 传递到函数的哪个参数将会被使用?哪个都没有。
It seems that once control is passed to the tail-called function, there is no more useful content in the stack. While still occupying space, the stack structure of the function is actually useless at this point, so the tail call optimization is to use the next stack structure to overwrite the current stack structure at the end of the function call, while maintaining the original return address.
What we do is essentially processing the stack. No more activity record () is required activation record
, so we will delete it and return the function redirect of the tail call to the function that called us. This means that we have to rewrite the stack manually to mimic a return address so that the tail-called function can return directly to the function that called it.
Conclusion
Recursion is a great art that makes it easier to verify the correctness of a program without sacrificing performance, but it requires programmers to look at programming in a new light. Imperative programming is often a more natural and intuitive starting point for new programmers, which is why most programming instructions focus on the cause of imperative languages and methods. However, as programs become more complex, recursive programming allows programmers to better organize code in a maintainable and logically consistent way.
Reproduced in the recursive algorithm detailed
[Go] recursive algorithm detailed