Search: in
Recursion (computer science)
Recursion (computer science) in Encyclopedia Encyclopedia
  Tutorials     Encyclopedia     Videos     Books     Software     DVDs  
       





Recursion (computer science)

Recursion in computer science is a method where the solution to a problem depends on solutions to smaller instances of the same problem.[1] The approach can be applied to many types of problems, and is one of the central ideas of computer science.[2]

"The power of recursion evidently lies in the possibility of defining an infinite set of objects by a finite statement. In the same manner, an infinite number of computations can be described by a finite recursive program, even if this program contains no explicit repetitions." [3]

Most computer programming languages support recursion by allowing a function to call itself within the program text. Some functional programming languages do not define any looping constructs but rely solely on recursion to repeatedly call code. Computability theory has proven that these recursive-only languages are mathematically equivalent to the imperative languages, meaning they can solve the same kinds of problems even without the typical control structures like while and for .

Tree created using the Logo programming language and relying heavily on recursion.
Tree created using the Logo programming language and relying heavily on recursion.

Contents


Recursive data types

Many computer programs must process or generate an arbitrarily large quantity of data. Recursion is one technique for representing data whose exact size the programmer does not know: the programmer can specify this data with a self-referential definition. There are two types of self-referential definitions: inductive and coinductive definitions.

Inductively defined data

An inductively defined recursive data definition is one that specifies how to construct instances of the data. For example, linked lists can be defined inductively (here, using Haskell syntax):

data ListOfStrings = EmptyList | Cons String ListOfStrings

The code above specifies a list of strings to be either empty, or a structure that contains a string and a list of strings. The self-reference in the definition permits the construction of lists of any (finite) number of strings.

Another example of inductive definition is the natural numbers (or non-negative integers):

A natural number is either 1 or n+1, where n is a natural number.

Similarly recursive definitions are often used to model the structure of expressions and statements in programming languages. Language designers often express grammars in a syntax such as Backus-Naur form; here is such a grammar, for a simple language of arithmetic expressions with multiplication and addition:

This says that an expression is either a number, a product of two expressions, or a sum of two expressions. By recursively referring to expressions in the second and third lines, the grammar permits arbitrarily complex arithmetic expressions such as (5 * ((3 * 6) + 8)), with more than one product or sum operation in a single expression.

Coinductively defined data and corecursion

A coinductive data definition is one that specifies the operations that may be performed on a piece of data; typically, self-referential coinductive definitions are used for data structures of infinite size.

A coinductive definition of infinite streams of strings, given informally, might look like this:

A stream of strings is an object s such that  head(s) is a string, and  tail(s) is a stream of strings.

This is very similar to an inductive definition of lists of strings; the difference is that this definition specifies how to access the contents of the data structure namely, via the accessor functions head and tail -- and what those contents may be, whereas the inductive definition specifies how to create the structure and what it may be created from.

Corecursion is related to coinduction, and can be used to compute particular instances of (possibly) infinite objects. As a programming technique, it is used most often in the context of lazy programming languages, and can be preferable to recursion when the desired size or precision of a program's output is unknown. In such cases the program requires both a definition for an infinitely large (or infinitely precise) result, and a mechanism for taking a finite portion of that result. The problem of computing the first n prime numbers is one that can be solved with a corecursive program (e.g. here).

Recursive algorithms

A common computer programming tactic is to divide a problem into sub-problems of the same type as the original, solve those problems, and combine the results. This is often referred to as the divide-and-conquer method; when combined with a lookup table that stores the results of solving sub-problems (to avoid solving them repeatedly and incurring extra computation time), it can be referred to as dynamic programming or memoization.

A recursive function definition has one or more base cases, meaning input(s) for which the function produces a result trivially (without recurring), and one or more recursive cases, meaning input(s) for which the program recurs (calls itself). For example, the factorial function can be defined recursively by the equations 0! = 1 and, for all n > 0, n! = n(n - 1)!. Neither equation by itself constitutes a complete definition; the first is the base case, and the second is the recursive case.

The job of the recursive cases can be seen as breaking down complex inputs into simpler ones. In a properly designed recursive function, with each recursive call, the input problem must be simplified in such a way that eventually the base case must be reached. (Functions that are not intended to terminate under normal circumstances for example, some system and server processes -- are an exception to this.) Neglecting to write a base case, or testing for it incorrectly, can cause an infinite loop.

For some functions (such as one that computes the series for e = 1+1/1!+1/2!+1/3!...) there is not an obvious base case implied by the input data; for these one may add a parameter (such as the number of terms to be added, in our series example) to provide a 'stopping criterion' that establishes the base case. Such an example is more naturally treated by corecursion, where successive terms in the output are the partial sums; this can be converted to a recursion by using the indexing parameter to say "compute the nth term (nth partial sum)".

Structural versus generative recursion

Some authors classify recursion as either "structural" or "generative". The distinction is related to where a recursive procedure gets the data that it works on, and how it processes that data:

[Functions that consume structured data] typically decompose their arguments into their immediate structural components and then process those components. If one of the immediate components belongs to the same class of data as the input, the function is recursive. For that reason, we refer to these functions as (STRUCTURALLY) RECURSIVE FUNCTIONS.[4]

Thus, the defining characteristic of a structurally recursive function is that the argument to each recursive call is the content of a field of the original input. Structural recursion includes nearly all tree traversals, including XML processing, binary tree creation and search, et cetera. By considering the algebraic structure of the natural numbers (that is, a natural number is either zero or the successor of a natural number), functions such as factorial may also be regarded as structural recursion.

Generative recursion is the alternative:

Many well-known recursive algorithms generate an entirely new piece of data from the given data and recur on it. HtDP (How To Design Programs) refers to this kind as generative recursion. Examples of generative recursion include: gcd, quicksort, binary search, mergesort, Newton's method, fractals, and adaptive integration.[5]

This distinction is important in proving termination of a function. All structurally recursive functions on finite (inductively defined) data structures can easily be shown to terminate, via structural induction: intuitively, each recursive call receives a smaller piece of input data, until a base case is reached. Generatively recursive functions, in contrast, do not necessarily feed smaller input to their recursive calls, so proof of their termination is not necessarily as simple, and avoiding infinite loops requires greater care.

Recursive programs

Recursive procedures

Factorial

A classic example of a recursive procedure is the function used to calculate the factorial of a natural number:

\operatorname{fact}(n) = \begin{cases} 1 & \mbox{if } n = 0 \\ n \cdot \operatorname{fact}(n-1) & \mbox{if } n > 0 \\ \end{cases}
Pseudocode (recursive):
function factorial is:
input: integer n such that n >= 0
output: [n (n-1) (n-2) 1]
1. if n is 0, return 1 2. otherwise, return [ n factorial(n-1) ]
end factorial

The function can also be written as a recurrence relation:

b_n = nb_{n-1}
b_0 = 1

This evaluation of the recurrence relation demonstrates the computation that would be performed in evaluating the pseudocode above:

Computing the recurrence relation for n = 4:
b4           = 4 * b3
= 4 * 3 * b2 = 4 * 3 * 2 * b1 = 4 * 3 * 2 * 1 * b0 = 4 * 3 * 2 * 1 * 1 = 4 * 3 * 2 * 1 = 4 * 3 * 2 = 4 * 6 = 24

This factorial function can also be described without using recursion by making use of the typical looping constructs found in imperative programming languages:

Pseudocode (iterative):
function factorial is:
input: integer n such that n >= 0
output: [n (n-1) (n-2) 1]
1. create new variable called running_total with a value of 1
2. begin loop 1. if n is 0, exit loop 2. set running_total to (running_total n) 3. decrement n 4. repeat loop
3. return running_total
end factorial

The imperative code above is equivalent to this mathematical definition using an accumulator variable :

\begin{array}{rcl} \operatorname{fact}(n) & = & \operatorname{fact_{acc}}(n, 1) \\ \operatorname{fact_{acc}}(n, t) & = & \begin{cases} t & \mbox{if } n = 0 \\ \operatorname{fact_{acc}}(n-1, nt) & \mbox{if } n > 0 \\ \end{cases} \end{array}

The definition above translates straightforwardly to functional programming languages such as Scheme; this is an example of iteration implemented recursively.

Fibonacci

Another well known mathematical recursive function is one that computes the Fibonacci numbers: \operatorname{fib}(n) = \begin{cases} 0 & \mbox{if } n = 0 \\ 1 & \mbox{if } n = 1 \\ \operatorname{fib}(n-1) + \operatorname{fib}(n-2) & \mbox{if } n >= 2 \\ \end{cases}

Pseudocode
function fib is: input: integer n such that n >= 0 
1. if n is 0, return 0 2. if n is 1, return 1 3. otherwise, return [ fib(n-1) + fib(n-2) ]
end fib

C language implementation:

Java language implementation:

     /**      * Recursively calculate the kth Fibonacci number.      *      * @param k indicates which (positive) Fibonacci number to compute.      * @return the kth Fibonacci number.      */     private static int fib(int k) {   // Base Cases:         //   If k == 0 then fib(k) = 0.  //   If k == 1 then fib(k) = 1.  if (k < 2) {             return k;  }  // Recursive Case:  //   If k >= 2 then fib(k) = fib(k-1) + fib(k-2).  return fib(k-1) + fib(k-2);     } 

C# language implementation:

 static int Fib(int n)         {             return n <= 1 ? n : Fib(n - 1) + Fib(n - 2);         } 

Python language implementation:

Scheme language implementation:

JavaScript language implementation:

 function fib (n) {     if (!n) {         return 0;     } else if (n <= 2) {         return 1;     } else {         return fib(n - 1) + fib(n - 2);     } } 

Common Lisp implementation:

Perl implementation:

Ruby implementation:

Recurrence relation for Fibonacci:
bn = bn-1 + bn-2
b1 = 1, b0 = 0

Computing the recurrence relation for n = 4:
  b4            = b3 + b2                 = b2 + b1 + b1 + b0                 = b1 + b0 + 1 + 1 + 0                 = 1 + 0 + 1 + 1 + 0                 = 3

This Fibonacci algorithm is a particularly poor example of recursion, because each time the function is executed on a number greater than one, it makes two function calls to itself, leading to an exponential number of calls (and thus exponential time complexity) in total.[6] The following alternative approach uses two accumulator variables TwoBack and OneBack to "remember" the previous two Fibonacci numbers constructed, and so avoids the exponential time cost:

Pseudocode
function fib is: input: integer Times such that Times >= 0, relative to TwoBack and OneBack              long TwoBack such that TwoBack = fib(x)              long OneBack such that OneBack = fib(x) 
1. if Times is 0, return TwoBack 2. if Times is 1, return OneBack 3. if Times is 2, return TwoBack + OneBack 4. otherwise, return [ fib(Times-1, OneBack, TwoBack + OneBack) ]
end fib

To obtain the tenth number in the Fib. sequence, one must perform Fib(10,0,1). Where 0 is considered TwoNumbers back and 1 is considered OneNumber back. As can be seen in this approach, no trees are being created, therefore the efficiency is much greater, being a linear recursion. The recursion in condition 4, shows that OneNumber back becomes TwoNumbers back, and the new OneNumber back is calculated, simply decrementing the Times on each recursion.

Alternatively, corecursion can produce the Fib. sequence in linear time.

Implemented in the Java or the C# programming language:

 public static long fibonacciOf(int times, long twoNumbersBack, long oneNumberBack) {      if (times == 0) {                            // Used only for fibonacciOf(0, 0, 1)         return twoNumbersBack;     } else if (times == 1) {                     // Used only for fibonacciOf(1, 0, 1)         return oneNumberBack;     } else if (times == 2) {                     // When the 0 and 1 clauses are included,         return oneNumberBack + twoNumbersBack;   // this clause merely stops one additional     } else {                                     // recursion from occurring         return fibonacciOf(times - 1, oneNumberBack, oneNumberBack + twoNumbersBack);     } } 

Greatest common divisor

Another famous recursive function is the Euclidean algorithm, used to compute the greatest common divisor of two integers. Function definition:

\gcd(x,y) = \begin{cases} x & \mbox{if } y = 0 \\ \gcd(y, \operatorname{remainder}(x,y)) & \mbox{if } x \ge y \mbox{ and } y > 0 \\ \end{cases}
Pseudocode (recursive):
function gcd is: input: integer x, integer y such that x >= y and y >= 0 
1. if y is 0, return x 2. otherwise, return [ gcd( y, (remainder of x/y) ) ]
end gcd

Recurrence relation for greatest common divisor, where x % y expresses the remainder of x / y:

\gcd(x,y) = \gcd(y, x % y)
\gcd(x,0) = x
Computing the recurrence relation for x = 27 and y = 9:
gcd(27, 9)   = gcd(9, 27 % 9)              = gcd(9, 0)              = 9
Computing the recurrence relation for x = 259 and y = 111:
gcd(259, 111)   = gcd(111, 259 % 111)                 = gcd(111, 37)                 = gcd(37, 0)                 = 37

The recursive program above is tail-recursive; it is equivalent to an iterative algorithm, and the computation shown above shows the steps of evaluation that would be performed by a language that eliminates tail calls. Below is a version of the same algorithm using explicit iteration, suitable for a language that does not eliminate tail calls. By maintaining its state entirely in the variables x and y and using a looping construct, the program avoids making recursive calls and growing the call stack.

Pseudocode (iterative):
function gcd is:
input: integer x, integer y such that x >= y and y >= 0
1. create new variable called remainder
2. begin loop 1. if y is zero, exit loop 2. set remainder to the remainder of x/y 3. set x to y 4. set y to remainder 5. repeat loop
3. return x
end gcd

The iterative algorithm requires a temporary variable, and even given knowledge of the Euclidean algorithm it is more difficult to understand the process by simple inspection, although the two algorithms are very similar in their steps.

Towers of Hanoi

For a full discussion of this problem's description, history and solution see the main article or one of the many references.[7][8] Simply put the problem is this: given three pegs, one with a set of N disks of increasing size, determine the minimum (optimal) number of steps it takes to move all the disks from their initial position to another peg without placing a larger disk on top of a smaller one.

Function definition:

\operatorname{hanoi}(n) = \begin{cases} 1 & \mbox{if } n = 1 \\ 2\cdot\operatorname{hanoi}(n-1) + 1 & \mbox{if } n > 1\\ \end{cases}

Recurrence relation for hanoi:

h_n = 2h_{n-1}+1
h_1 = 1
Computing the recurrence relation for n = 4:
hanoi(4)     = 2*hanoi(3) + 1              = 2*(2*hanoi(2) + 1) + 1              = 2*(2*(2*hanoi(1) + 1) + 1) + 1              = 2*(2*(2*1 + 1) + 1) + 1              = 2*(2*(3) + 1) + 1              = 2*(7) + 1              = 15


Example Implementations:

Pseudocode (recursive):
function hanoi is:
input: integer n, such that n >= 1
1. if n is 1 then return 1
2. return [2 * [call hanoi(n-1)] + 1]
end hanoi


Although not all recursive functions have an explicit solution, the Tower of Hanoi sequence can be reduced to an explicit formula.[9]

An explicit formula for Towers of Hanoi:
h1 = 1   = 21 - 1 h2 = 3   = 22 - 1 h3 = 7   = 23 - 1 h4 = 15  = 24 - 1 h5 = 31  = 25 - 1 h6 = 63  = 26 - 1 h7 = 127 = 27 - 1  In general: hn = 2n - 1, for all n >= 1

Binary search

The binary search algorithm is a method of searching an ordered array for a single element by cutting the array in half with each pass. The trick is to pick a midpoint near the center of the array, compare the data at that point with the data being searched and then responding to one of three possible conditions: the data is found at the midpoint, the data at the midpoint is greater than the data being searched for, or the data at the midpoint is less than the data being searched for.

Recursion is used in this algorithm because with each pass a new array is created by cutting the old one in half. The binary search procedure is then called recursively, this time on the new (and smaller) array. Typically the array's size is adjusted by manipulating a beginning and ending index. The algorithm exhibits a logarithmic order of growth because it essentially divides the problem domain in half with each pass.

Example implementation of binary search in C:

Recursive data structures (structural recursion)

An important application of recursion in computer science is in defining dynamic data structures such as Lists and Trees. Recursive data structures can dynamically grow to a theoretically infinite size in response to runtime requirements; in contrast, a static array's size requirements must be set at compile time.

"Recursive algorithms are particularly appropriate when the underlying problem or the data to be treated are defined in recursive terms." [10]

The examples in this section illustrate what is known as "structural recursion". This term refers to the fact that the recursive procedures are acting on data that is defined recursively.

As long as a programmer derives the template from a data definition, functions employ structural recursion. That is, the recursions in a function's body consume some immediate piece of a given compound value.[5]

Linked lists

Below is a simple definition of a linked list node. Notice especially how the node is defined in terms of itself. The "next" element of struct node is a pointer to another struct node, effectively creating a list type.

Because the struct node data structure is defined recursively, procedures that operate on them can be implemented naturally as a recursive procedure. The list_print procedure defined below walks down the list until the list is empty (or NULL). For each node it prints the data element (an integer). In the C implementation, the list remains unchanged by the list_print procedure.

Binary trees

Below is a simple definition for a binary tree node. Like the node for linked lists, it is defined in terms of itself, recursively. There are two self-referential pointers: left (pointing to the left sub-tree) and right (pointing to the right sub-tree).

Operations on the tree can be implemented using recursion. Note that because there are two self-referencing pointers (left and right), tree operations may require two recursive calls:

At most two recursive calls will be made for any given call to tree_contains as defined above.

The above example illustrates an in-order traversal of the binary tree. A Binary search tree is a special case of the binary tree where the data elements of each node are in order.

Filesystem traversal

Since the number of files in a filesystem may vary, recursion is the only practical way to traverse and thus enumerate its contents. Traversing a filesystem is very similar to that of tree traversal, therefore the concepts behind tree traversal are applicable to traversing a filesystem. More specifically, the code below would be an example of a preorder traversal of a filesystem.


This code blends the lines, at least somewhat, between recursion and iteration. It is, essentially, a recursive implementation, which is the best way to traverse a filesystem. It is also an example of direct and indirect recursion. "rtraverse" is purely a direct example; "traverse" is the indirect, which calls "rtraverse." This example needs no "base case" scenario due to the fact that there will always be some fixed number of files and/or directories in a given filesystem.

Recursion versus iteration

Expressive power

Most programming languages in use today allow the direct specification of recursive functions and procedures. When such a function is called, the program's runtime environment keeps track of the various instances of the function (often using a call stack, although other methods may be used). Every recursive function can be transformed into an iterative function by replacing recursive calls with iterative control constructs and simulating the call stack with a stack explicitly managed by the program.[11][12]

Conversely, all iterative functions and procedures that can be evaluated by a computer (see Turing completeness) can be expressed in terms of recursive functions; iterative control constructs such as while loops and do loops routinely are rewritten in recursive form in functional languages.[13][14] However, in practice this rewriting depends on tail call elimination, which is not a feature of all languages. C, Java, and Python are notable mainstream languages in which all function calls, including tail calls, cause stack allocation that would not occur with the use of looping constructs; in these languages, a working iterative program rewritten in recursive form may overflow the call stack.

Performance issues

In languages (such as C and Java) that favor iterative looping constructs, there is usually significant time and space cost associated with recursive programs, due to the overhead required to manage the stack and the relative slowness of function calls; in functional languages, a function call (particularly a tail call) is typically a very fast operation, and the difference is usually less noticeable.

As a concrete example, the difference in performance between recursive and iterative implementations of the "factorial" example above depends highly on the language used. In languages where looping constructs are preferred, the iterative version may be as much as several orders of magnitude faster than the recursive one. In functional languages, the overall time difference of the two implementations may be negligible; in fact, the cost of multiplying the larger numbers first rather than the smaller numbers (which the iterative version given here happens to do) may overwhelm any time saved by choosing iteration.

Other considerations

In some programming languages, the stack space available to a thread is much less than the space available in the heap, and recursive algorithms tend to require more stack space than iterative algorithms. Consequently, these languages sometimes place a limit on the depth of recursion to avoid stack overflows. (Python is one such language.[15]) Note the caveat below regarding the special case of tail recursion.

There are some types of problems whose solutions are inherently recursive, because of prior state they need to track. One example is tree traversal; others include the Ackermann function, depth-first search, and divide-and-conquer algorithms such as Quicksort. All of these algorithms can be implemented iteratively with the help of an explicit stack, but the programmer effort involved in managing the stack, and the complexity of the resulting program, arguably outweigh any advantages of the iterative solution.

Tail-recursive functions

Tail-recursive functions are functions in which all recursive calls are tail calls and hence do not build up any deferred operations. For example, the gcd function (shown again below) is tail-recursive. In contrast, the factorial function (also below) is not tail-recursive; because its recursive call is not in tail position, it builds up deferred multiplication operations that must be performed after the final recursive call completes. With a compiler or interpreter that treats tail-recursive calls as jumps rather than function calls, a tail-recursive function such as gcd will execute using constant space. Thus the program is essentially iterative, equivalent to using imperative language control structures like the "for" and "while" loops.

Tail recursion: Augmenting recursion:

The significance of tail recursion is that when making a tail-recursive call, the caller's return position need not be saved on the call stack; when the recursive call returns, it will branch directly on the previously saved return position. Therefore, on compilers that support tail-recursion optimization, tail recursion saves both space and time.

Order of execution

In a recursive function, the position in which additional statements (i.e., statements other than the recursive call itself) are placed is important. In the simple case of a function calling itself only once, a statement placed before the recursive call will be executed first in the outermost stack frame, while a statement placed after the recursive call will be executed first in the innermost stack frame. Consider this example:

Function 1

Function 2 with swapped lines

Direct and indirect recursion

Most of the examples presented here demonstrate direct recursion, in which a function calls itself. Indirect recursion occurs when a function is called not by itself but by another function that it called (either directly or indirectly). "Chains" of three or more functions are possible; for example, function 1 calls function 2, function 2 calls function 3, and function 3 calls function 1 again.

Often, "indirect recursion" is called mutual recursion.

See also

Notes and references

External links

ar: ( ) bar:Rekursive Programmierung ca:Algorisme recursiu cs:Rekurzivn funkce (programov n ) de:Rekursive Programmierung es:Recursi n (ciencias de computaci n) fa: fr:Algorithme r cursif ko: it:Algoritmo ricorsivo kk: pt:Recursividade (ci ncia da computa o) ru: simple:Recursive algorithm sv:Rekursiv algoritm vi: quy zh: ( )






Source: Wikipedia | The above article is available under the GNU FDL. | Edit this article



Search for Recursion (computer science) in Tutorials
Search for Recursion (computer science) in Encyclopedia
Search for Recursion (computer science) in Videos
Search for Recursion (computer science) in Books
Search for Recursion (computer science) in Software
Search for Recursion (computer science) in DVDs
Search for Recursion (computer science) in Store




Advertisement




Recursion (computer science) in Encyclopedia
Recursion_(computer_science) top Recursion_(computer_science)

Home - Add TutorGig to Your Site - Disclaimer

©2011-2013 TutorGig.info All Rights Reserved. Privacy Statement