FazBrowse Wikipedia Viewer | Main Page |
Search Wikipedia:
|
Viewing: https://en.wikipedia.org/wiki/Recursive_algorithm

Recursion (computer science)

Contents
History
Structure of a recursive function
    Base case
    Recursive case
    Coinductively defined data and corecursion
    Anonymous recursion
    Hybrid algorithm
Recursion versus iteration
    Expressive power
    Performance issues
    Stack space
    Vulnerability
    Multiply recursive problems
    Refactoring recursion
Tail-recursive functions
Recursive procedures
    Factorial
    Greatest common divisor
    Filesystem traversal
Recursion in Logic Programming
Infinite recursion
References

Snowflake pattern made using recursion
Honeycomb pattern made using recursion
Tree fractal created using the Logo programming language
Recursive drawing of a Sierpiński Triangle through turtle graphics
Examples made using recursion

In computer science, recursion is a method of solving a computational problem where the solution depends on solutions to smaller instances of the same problem. Recursion solves such recursive problems by using functions that call themselves from within their own code. The approach can be applied to many types of problems, and recursion is one of the central ideas of computer science.

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.

— Niklaus Wirth, Algorithms + Data Structures = Programs, 1976

Most computer programming languages support recursion by allowing a function to call itself from within its own code. Some functional programming languages (for instance, Clojure) do not define any built-in looping constructs, and instead rely solely on recursion. It is proved in computability theory that these recursion-only languages are Turing complete; this means that they are as powerful (they can be used to solve the same problems) as imperative languages based on control structures such as

History

edit

The development of recursion in computer science grew out of mathematical logic and later became an essential part of programming language design. The early work done by Church, Gödel, Kleene, and Turing on recursive function and computability laid the groundwork that made recursion possible in programming languages. Recursion has been used by mathematicians for a long time, but it only became a practical tool for programming in the late 1950s and early 1960s. Key figures such as John McCarthy and the ALGOL 60 design committee contributed to introducing recursion into programming.

John McCarthy took the first steps by creating the programming language LISP in 1960. In his paper Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I, McCarthy showed that recursion could be core in a programming language that works with symbols by processing them step by step. In LISP, recursion could be used in functions using simple rules, and there was also a way to evaluate them in the language. This demonstrated that recursion was a practical way to write programs and that it also describes the process of computation. Therefore, LISP became one of the first programming languages to use recursion as a main feature, and later on also influenced other languages that followed.

During that time, recursion was also added to ALGOL 60. The Report on the Algorithmic Language ALGOL 60, which was published in 1960, was the outcome of an international attempt at designing a standard language. Allowing procedures to call themselves was one of the new important features of the language. Before that, only loops were allowed to be used by programmers, so it was a significant change. Recursion allowed programmers to describe algorithms in a more natural and flexible way.

Structure of a recursive function

edit

The definition of a recursive function is typically divided into two parts: one or more base case(s) and one or more recursive case(s). This structure mirrors the logic of mathematical induction, which is a proof technique where proving the base case(s) and the inductive step ensures that a given theorem holds for all valid inputs.

Base case

edit

The base case specifies input values for which the function can provide a result directly, without any further recursion. These are typically the simplest or smallest possible inputs (which can be solved trivially), allowing a computation to terminate. Base cases are essential because they prevent infinite regress. In other words, they define a stopping condition that terminates the recursion.

An example is computing the factorial of an integer n, which is the product of all integers from 0 to n. For this problem, the definition 0! = 1 is a base case. Without it, the recursion may continue indefinitely, leading to non-termination or even stack overflow errors in actual implementations.

Designing a correct base case is crucial for both theoretical and practical reasons. Some problems have a natural base case (e.g., the empty list is a base case in some recursive list-processing functions), while others require an additional parameter to provide a stopping criterion (e.g., using a depth counter in recursive tree traversal).

In recursive computer programming, omitting the base case or defining it incorrectly may result in unintended infinite recursion. In a study, researchers showed that many students struggle to identify appropriate base cases.

Recursive case

edit

The recursive case describes how to break down a problem into smaller sub-problems of the same form. Each recursive step transforms the input so that it approaches a base case, ensuring progress toward termination. If the reduction step fails to progress toward a base case, the algorithm can get trapped in an infinite loop.

In the factorial example, the recursive case is defined as: 0."}}'>n!=n⋅(n−1)!,∀n>0. Here, each invocation of the function decreases the input

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):

<expr> ::= <number> | (<expr> * <expr>) | (<expr> + <expr>)

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 complicated 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

edit

Most basic examples of recursion, and 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). For example, if f calls f, that is direct recursion, but if f calls g which calls f, then that is indirect recursion of f. 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.

Indirect recursion is also called mutual recursion, which is a more symmetric term, though this is simply a difference of emphasis, not a different notion. That is, if f calls g and then g calls f, which in turn calls g again, from the point of view of f alone, f is indirectly recursing, while from the point of view of g alone, it is indirectly recursing, while from the point of view of both, f and g are mutually recursing on each other. Similarly a set of three or more functions that call each other can be called a set of mutually recursive functions.

Anonymous recursion

edit
"}},"i":0}}]}'/>

[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.

— Felleisen, Findler, Flatt, and Krishnaurthi, How to Design Programs, 2001

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, etc. 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.

intfac1(intn){ if(n<=0){ return1; }else{ returnfac1(n-1)*n; } }

= 2);\n if (n == 2) {\n return 2;\n } else {\n return fac2(n - 1) * n;\n }\n}\nint fac2wrapper(int n) {\n if (n <= 1) {\n return 1;\n } else {\n return fac2(n);\n }\n}\n"}}'>intfac2(intn){ // assert(n >= 2); if(n==2){ return2; }else{ returnfac2(n-1)*n; } } intfac2wrapper(intn){ if(n<=1){ return1; }else{ returnfac2(n); } }

Short-circuiting the base case, also known as arm's-length recursion, consists of checking the base case before making a recursive call – i.e., checking if the next call will be the base case, instead of calling and then checking for the base case. Short-circuiting is particularly done for efficiency reasons, to avoid the overhead of a function call that immediately returns. Note that since the base case has already been checked for (immediately before the recursive step), it does not need to be checked for separately, but one does need to use a wrapper function for the case when the overall recursion starts with the base case itself. For example, in the factorial function, properly the base case is 0! = 1, while immediately returning 1 for 1! is a short circuit, and may miss 0; this can be mitigated by a wrapper function. The box shows C code to shortcut factorial cases 0 and 1.

Short-circuiting is primarily a concern when many base cases are encountered, such as Null pointers in a tree, which can be linear in the number of function calls, hence significant savings for O(n) algorithms; this is illustrated below for a depth-first search. Short-circuiting on a tree corresponds to considering a leaf (non-empty node with no children) as the base case, rather than considering an empty node as the base case. If there is only a single base case, such as in computing the factorial, short-circuiting provides only #include<assert.h> // Wrapper function to handle empty tree booltree_contains(structBinaryTree*node,inti){ if(!node){ returnfalse;// empty tree }else{ returntree_contains_aux(node,i);// call auxiliary function } } // Assumes node != NULL booltree_contains_aux(structBinaryTree*node,inti){ assert(node); if(node->data==i){ returntrue;// found }else{ // recurse return(node->left&&tree_contains_aux(node->left,i)) ||(node->right&&tree_contains_aux(node->right,i)); } }

Note the use of short-circuit evaluation of the Boolean && (AND) operators, so that the recursive call is made only if the node is valid (non-Null). Note that while the first term in the AND is a pointer to a node, the second term is a Boolean, so the overall expression evaluates to a Boolean. This is a common idiom in recursive short-circuiting. This is in addition to the short-circuit evaluation of the Boolean || (OR) operator, to only check the right child if the left child fails. In fact, the entire control flow of these functions can be replaced with a single Boolean expression in a return statement, but legibility suffers at no benefit to efficiency.

Hybrid algorithm

edit

Recursive algorithms are often inefficient for small data, due to the overhead of repeated function calls and returns. For this reason efficient implementations of recursive algorithms often start with the recursive algorithm, but then switch to a different algorithm when the input becomes small. An important example is merge sort, which is often implemented by switching to the non-recursive insertion sort when the data is sufficiently small, as in the tiled merge sort. Hybrid recursive algorithms can often be further refined, as in Timsort, derived from a hybrid merge sort/insertion sort.

Recursion versus iteration

edit

Recursion and iteration are equally expressive: recursion can be replaced by iteration with an explicit call stack, while iteration can be replaced with tail recursion. Which approach is preferable depends on the problem under consideration and the language used. In imperative programming, iteration is preferred, particularly for simple recursion, as it avoids the overhead of function calls and call stack management, but recursion is generally used for multiple recursion. By contrast, in functional languages recursion is preferred, with tail recursion optimization leading to little overhead. Implementing an algorithm using iteration may not be easily achievable.

Compare the templates to compute xn defined by xn = f(n, xn-1) from xbase:

function recursive(n) if n == base return xbase else return f(n, recursive(n - 1)) function iterative(n) x = xbase for i = base + 1 to n x = f(i, x) return x

For an imperative language the overhead is to define the function, and for a functional language the overhead is to define the accumulator variable x.

For example, a factorial function may be implemented iteratively in C by assigning to a loop index variable and accumulator variable, rather than by passing arguments and returning values by recursion:

0) {\n product *= n;\n --n;\n }\n return product;\n}\n"}}'>unsignedintfactorial(unsignedintn){ unsignedintproduct=1;// empty product is 1 while(n>0){ product*=n; --n; } returnproduct; }

Expressive power

edit

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.

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 for loops are routinely rewritten in recursive form in functional languages. 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, may 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, although tail call elimination may be a feature that is not covered by a language's specification, and different implementations of the same language may differ in tail call elimination capabilities.

Performance issues

edit

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 compiler 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.

Stack space

edit

In some programming languages, the maximum size of the call stack 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. Note the caveat below regarding the special case of tail recursion.

Vulnerability

edit

Because recursive algorithms can be subject to stack overflows, they may be vulnerable to pathological or malicious input. Some malware specifically targets a program's call stack and takes advantage of the stack's inherently recursive nature. Even in the absence of malware, a stack overflow caused by unbounded recursion can be fatal to the program, and exception handling logic may not prevent the corresponding process from being terminated.

Multiply recursive problems

edit

Multiply recursive problems are inherently recursive, because of prior state they need to track. One example is tree traversal as in depth-first search; though both recursive and iterative methods are used, they contrast with list traversal and linear search in a list, which is a singly recursive and thus naturally iterative method. Other examples include divide-and-conquer algorithms such as Quicksort, and functions such as the Ackermann function. 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.

Refactoring recursion

edit

Recursive algorithms can be replaced with non-recursive counterparts. One method for replacing recursive algorithms is to simulate them using heap memory in place of stack memory. An alternative is to develop a replacement algorithm entirely based on non-recursive methods, which can be challenging. For example, recursive algorithms for matching wildcards, such as Rich Salz' wildmat algorithm, were once typical. Non-recursive algorithms for the same purpose, such as the Krauss matching wildcards algorithm, have been developed to avoid the drawbacks of recursion and have improved only gradually based on techniques such as collecting tests and profiling performance.

Tail-recursive functions

edit

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:
= y >= 0\n * @returns the GCD of x and y\n */\nint gcd(int x, int y) {\n if (y == 0) {\n return x;\n } else { \n return gcd(y, x % y);\n }\n}\n"}}'>/** * @brief computes GCD using Euclidean algorithm * @param x an int * @param y an int, such that x >= y >= 0 * @returns the GCD of x and y */ intgcd(intx,inty){ if(y==0){ returnx; }else{ returngcd(y,x%y); } }
#include<stdio.h> voidrecursiveFunction(intnum){ if(num<4){ recursiveFunction(num+1); } printf("%d\n",num); }

The output of function 2 is that of function 1 with the lines swapped.

In the case of a function calling itself only once, instructions placed before the recursive call are executed once per recursion before any of the instructions placed after the recursive call. The latter are executed repeatedly after the maximum recursion has been reached.

Also note that the order of the print statements is reversed, which is due to the way the functions and statements are stored on the call stack.

Recursive procedures

edit

Factorial

edit

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

0 \\\\\n \\end{cases}\n"}}'>fact⁡(n)={1if n=0n⋅fact⁡(n−1)if n>0
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:

t:

0 \\\\\n \\end{cases}\n\\end{align}\n"}}'>fact⁡(n)=factacc⁡(n,1)factacc⁡(n,t)={tif n=0factacc⁡(n−1,nt)if n>0

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

Greatest common divisor

edit

The Euclidean algorithm, which computes the greatest common divisor of two integers, can be written recursively.

Function definition:

0 \\\\\n \\end{cases}\n"}}'>gcd⁡(x,y)={xif y=0gcd⁡(y,remainder⁡(x,y))if y>0
Pseudocode (recursive):
function gcd is: input: integer x, integer y such that x > 0 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 gcd⁡(x,y)=gcd⁡(y,x%y) if hn=2hn−1+1

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

// Test if tree_node contains i; return 1 if so, 0 if not. inttree_contains(structBinaryTree*node,inti){ if(!node){ return0;// base case }elseif(node->data==i){ return1; }else{ returntree_contains(node->left,i)||tree_contains(node->right,i); } }

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

left); // go left\n printf(\"%d \", node->data); // print the integer followed by a space\n tree_print(node->right); // go right\n }\n}\n"}}'>// Inorder traversal: voidtree_print(structBinaryTree*node){ // base case if(node){ tree_print(node->left);// go left printf("%d ",node->data);// print the integer followed by a space tree_print(node->right);// go right } }

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

edit

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.

If the time-complexity of the function is in the form ε>0, then T(n)=Θ(nlogbalog⁡n)

  • If a⋅f(n/b)≤c⋅f(n) for some constant T(n)=Θ(f(n))
  • where f(n) represents the work that the function does independently of any recursion (e.g. partitioning, recombining) at each level of recursion.

    Recursion in Logic Programming

    edit

    In the procedural interpretation of logic programs, clauses (or rules) of the form ∀X,Y,Z(arc(X,Z)∧path(Z,Y)→path(X,Y)).

    The logical reading frees the reader from needing to know how the clause is used to solve problems. The clause can be used top-down, as in Prolog, to reduce problems to subproblems. Or it can be used bottom-up (or forwards), as in Datalog, to derive conclusions from conditions. This separation of concerns is a form of abstraction, which separates declarative knowledge from problem solving methods (see Algorithm#Algorithm = Logic + Control).

    Infinite recursion

    edit

    A common mistake among programmers is not providing a way to exit a recursive function, often by omitting or incorrectly checking the base case, letting it run (at least theoretically) infinitely by endlessly calling itself recursively. This is called infinite recursion, and the program will never terminate. In practice, this typically exhausts the available stack space. In most programming environments, a program with infinite recursion will not really run forever. Eventually, something will break and the program will report an error. However, if tail call optimization is used then the recursive calls may be optimized into an infinite loop that can run forever.

    Below is a Java code that would use infinite recursion:

    Kuhail, Mohammad A.; Negreiros, Joao; Seffah, Ahmed (2021). "Teaching Recursive Thinking using Unplugged Activities". World Transactions on Engineering and Technology Education. 19 (2): 169–175.
  • ↑ "Functional Programming | Clojure for the Brave and True". www.braveclojure.com. Retrieved 2020-10-21.
  • 1 2 3 McCarthy, John (1960). "Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I". Communications of the ACM. 3 (4): 184–195. doi:10.1145/367177.367199.
  • 1 2 Sweigart, Al (2022). The Recursive Book of Recursion: Ace the Coding Interview with Python and JavaScript. No Starch Press. ISBN 978-1-7185-0202-4.Bron, C. (May 1972). "Algorithm 426: Merge sort algorithm [M1]". Communications of the ACM. 15 (5): 357–358. doi:10.1145/355602.361317.
  • ↑ Felleisen et al. 2001, art V "Generative Recursion
  • 1 2 Hetland, Magnus Lie (2010), Python Algorithms: Mastering Basic Algorithms in the Python Language, Apress, p. 79, ISBN 9781430232384.
  • ↑ Lambda the Ultimate. "The Anatomy of a Loop". Lambda the Ultimate. Retrieved 2012-09-03.
  • ↑ "Depth First Search (DFS): Iterative and Recursive Implementation". Techie Delight. 2018.
  • ↑ Moertel, Tom (2013). "Tricks of the trade: Recursion to Iteration, Part 2: Eliminating Recursion with the Time-Traveling Secret Feature Trick".
  • ↑ "4.8. Infinite Recursion — How to Think Like a Computer Scientist - C++".
  • References

    edit

    View original on Wikipedia | Back | FazBrowse Home