Recursion What is recursion? In the context of a programming language - it is simply an already active method (or subprogram) being invoked by itself directly or being invoked by another method (or subprogram) indirectly. Types of Recursion Direct Recursion: procedure Alpha; begin Alpha end; Indirect (or Mutual) Recursion: procedure Alpha; begin Beta end; procedure Beta; begin Alpha end; Difficult in some programming languages because of forward reference. Illustration Suppose we wish to formulate a list of instructions to explain to someone how to climb to the top of a ladder. Consider the following pseudo-code statements Iterative statements: procedure Climb_Ladder; begin for (i = 0; i < numRungs; i++) move up one rung; end; Recursive statements: procedure Climb_Ladder; begin if (at top) //the escape mechanism then stop; else begin move up one rung; Climb_Ladder; end end; Why use recursion? There is a common belief that it is easier to learn to program iteratively, or to use nonrecursive methods, than it is to learn to program recursively. Some programmers report that they “would be fired” if they were to use recursion in their jobs. In fact, though, recursion is a method-based technique for implementing iteration. Hard to argue conclusively for recursion! However, recursive programming is easy once one has had the opportunity to practice the style. Recursive programs are often more succinct, elegant, and easier to understand than their iterative counterparts. Some problems are more easily solved using recursive methods than iterative ones. Example: Fibonacci Numbers F0 F1 F2 F3 F4 F5 F6 F7 … 0 1 1 2 3 5 8 13 … Fibonacci numbers have an incredible number of properties that crop up in computer science. • There is a journal, The Fibonacci Quarterly, that exists for the sole purpose of publishing theorems involving Fibonacci numbers. Examples: • 1. The sum of the squares of two consecutive Fibonacci numbers is another Fibonacci number. 2. The sum of the first “n” Fibonacci numbers is one less than Fn+2 • Because the Fibonacci numbers are recursively defined, writing a recursive procedure to calculate Fn seems reasonable. • Also, from my earlier argument, we should expect the algorithm to be much more elegant and concise than an equivalent iterative one. • Consider an iterative solution! Non-recursive Fibonacci Numbers private static int fibonacci(int n) { int Fnm1, Fnm2, Fn; if (n <=1) else } return n //F0 = 0 and F1 = 1; { } Fnm2 = 0; Fnm1 = 1; for (int i = 2; i <= n; i++) { Fn := Fnm1 + Fnm2; Fnm2 := Fnm1; Fnm1 := Fn; } return Fn Not very elegant!! Note: F0 = 0 F1 = 1 F2 = 1 F3 = 2 F4 = 3 . . . Fibonacci Numbers Recursive Definition: Fib(n) = ì ï ï ï ï ï í ï ï ï ï ïî 0, if n =0 1, if n =1 Fib(n-1)+Fib(n-2), otherwise Series: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... Java: int fib(int n) { if (n == 0) return 0; else if (n == 1) return 1; else return( fib(n-1) + fib(n-2)); } The underlying problem with the recursive method is that it performs lots of redundant calculations. • Example: on a reasonably fast computer, to recursively compute F40 takes almost a minute. A lot of time considering that the calculation requires only 39 additions. • Example: to compute F5 F5 + F4 F3 F2 F1 + + F2 F1 F1 + F3 + F2 F0 F1 + F0 F1 F0 REDUNDANT!! It turns out that the number of recursive calls is larger than the Fibonacci number we’re trying to calculate - and it has an exponential growth rate. • Example: n=40, F40 = 102,334,155 The total number of recursive calls is greater than - 300,000,000 History of Fibonacci Numbers The origin of the Fibonacci numbers and their sequence can be found in a thought experiment posed in 1202. It was related to the population growth of pairs of rabbits under ideal conditions. The objective was to calculate the size of a population of rabbits during each month according to the following rules (the original experiment was designed to calculate the population after a year): 1. Originally, a newly-born pair of rabbits, one male and one female, are put in a field. 2. Rabbits take one month to mature. 3. It takes one month for mature rabbits to produce another pair of newlyborn rabbits. 4. Rabbits never die. 5. The female always produces one male and one female rabbit every month from the second month on. Rabbit Genealogical Tree Solving Problems We encourage students to solve “large” problems by breaking their solutions up into “smaller” problems These “smaller” problems can then be solved and combined (integrated) together to solve the “larger” problem. Referred to as: Stepwise Refinement, Decomposition, Divide-and-conquer Basis of Recursion If the subproblems are similar to the original -- then we may be able to employ recursion. Two requirements: (1) (2) the subproblems must be simpler than the original problem. After a finite number of subdivisions, a subproblem must be encountered that can be solved outright. How is memory managed? Consider the following Java program: public class MemoryDemo { int I, J, K; public static void main(String[] args) { . . . three(); . . . } private static void one { float X, Y, Z; } . . . return; private static void two { char B, C, D; } } . . . one() . . . private static void three; { boolean P, Q, R; . . . two(); . . . } Say that the main program MemoryDemo has invoked method three, three has invoked two, and two has invoked one. Recall that parameters and local variables are allocated memory upon entry to a method and memory is deallocated upon exit from the method . Thus, the memory stack would currently look like: Activation Records: ---Available space --X Y Z One B C D Two P Q R Three I J K MemoryDemo Is memory managed differently for recursion? No!! Consider the following Java program: public class MemoryDemo { int I, J, K; public static void main(String[] args) { . . . recursiveOne(); . . . } } private static void recursiveOne(); { float X,Y,Z; . . . recursiveOne(); . . . } Parameters and local variables are still allocated memory upon entry to a method and deallocated upon exit from the method. Thus, during the recursion, the memory stack would look like: . . . . . . X Y Z RecursiveOne 3 X Y Z RecursiveOne 2 X Y Z RecursiveOne 1 I J K MemoryDemo Observation! Some “escape mechanism” must be present in the recursive method (or subprogram) in order to avoid an infinite loop. Iterative methods (with infinite loops) are terminated for exceeding time limits. Recursive methods (with infinite loops) are terminated due to memory consumption. Iteration Entry Initialization Decision Done Not Done Update Computation Return Recursion Entry Prologue (SAVE state of calling program Save formal parameters, local variables, return address Test Intermediate Level (continue) Partial computation Stop recursion Body Procedure call To itself Final Computation Restore (most recently Saved) formal parameters, Local variables, return address Exit To return address Epilogue (restore SAVE state) Consider an Example (Summing a series of integers from 1 to n) Iteratively public class SumDemo { public static void main(String args[]) { int sum; int n = 10; // could have user input sum = iterativeSum(n); System.out.println("Sum = " + sum); } private static int iterativeSum(int n) { int tempSum = n; while ( n > 1) { n--; tempSum += n; //tempSum = tempSum + n } return (tempSum); } } Recursively public class SumDemo { public static void main(String args[]) { int sum; int n = 10; //could have user input this sum = recursiveSum(n); System.out.println("Sum = " + sum); } private static int recursiveSum(int n) { if (n <= 1) return n; else return ( n + recursiveSum(n-1)); } } Formal Representation Methods Used by computer scientists for producing a mathematically rigorous representation of a set of user requirements. Two classes of Representation Methods State oriented notations: Examples (Decision Tables, Finite-state Machines) Relational notations: Examples (Recurrence Relations, Algebraic Axioms) Using representation notations Recall the earlier example of “summing the integers from 1 to n” This can be described as a series using the following representation: 1 Sum = n + (n-1) + (n-2) + ... + 1 or å i i=n Alternatively, recursive definitions (recurrence relations) can be employed: ì n, if n £1 ï Sum(n) = íï în + Sum(n -1), otherwise Consider another example Computing Factorials Definitions: Iterative – n! = ì ï ï ï ï ï í ï ï ï ï ï î 1, if n=0 1*2*3*...*n, if n 0 Recursive – ìï 1, if n = 0 n! = í ïî(n -1)!*n, otherwise Java: public int factorial(int n) { if (n == 0) return 1; else return ( n * factorial(n-1)); } Computing a power (xn) Recall that standard Java does not provide an exponentiation operator: the method pow in the Math class must be used (e.g., Math.pow(2,3)) Calculating powers can also be done using the relationship: xn = exp(n * ln(x)) ì 1, if n = 0 Could also be done recursively: ï Recursive Definition: power(x,n)= í x, if n = 1 ï î x * power(x, n -1), otherwise Java: public float power(double x, int n { if (n == 0) return 1.0; else if (n == 1) return x; else return ( x * power(x,n-1)); } Summing the elements of an array named List that contains n values. **Assuming lower bound of 0 as for Java Recursive definition: list[0], if n=1 Sum(list,n) = list[n-1]+Sum(list,n-1), otherwise Java: ì ïï í ï ïî public int sumArray(int[] list, int n) { // list is the array to be summed; n represents the number //of elements currently in the array if (n == 1) return list[0]; else return (list[n-1] + sumArray(list,n-1)); } //note: non-primitive parameters are passed by reference in java - the array is not duplicated. Might be in other languages. Using Scope! Java: public int sumArray(int n) { // access the array (in this case list) to be // summed through scope; n represents the number of // elements currently in the array if (n == 1) return list[0]; else return (list[n-1] + sumArray(n-1)); } //in languages where parameters are duplicated – access // arrays through scope Sequentially search the elements of an array for a given value(known to be in the array) Recursive Definition Search(List, i, Value) ì i, if List[i] = Value =í îSearch(List,i +1,Value), otherwise Note: (1) Assumes that the desired Value is in the array, (2) Initial call is: positionOfValue = Search(list,0,Value) (3) Returns the array position if Value is found in the array Java: public int search(int[] List, int i, int Value) { if (List[i] == Value) return i; else return Search(List,i+1,Value); } Sequentially search the elements of an array for a given value(may not in the array) Recursive Definition Search(List,i,value,numEls) = ì ï ï ï í ï ï ï î i, if List[i]==value -99, if i==numEls Search(List,i+1,Value,numEls), otherwise Note: (1) Assumes that the desired value may not be in the array, returns -99 is not (2) Initial call is: positionOfValue = Search(list,0,value,numEls) (3) numEls is the number or elements in the array (its size) Java: public int search(int[] List, int i, int value, int numEls) { if (List[i] == value) return i; else if (i == numEls) return -99; else return Search(List,i+1,value,numEls); } Other Examples: Reversing a character string passed as parameter s public void reverse (String s) { char t = s.charAt(0); if (s.length() > 1) reverse(s.substring(1,s.length())); System.out.print(t); } Converting an integer value to binary private static void convertToBinary(int x); { int t = x/2; // integer divide, truncate remainder if (t != 0) convertToBinary (t); System.out.print(x % 2); } Another Example: A boolean method that checks to see if two arrays passed as parameters are identical in size and content (n is the size of the array). areIdentical(x,y,n) ì false, if (x.length != y.length) // different sizes ïï(x[0] == y[0]), if (n == 1) // t | f only one element =í x[n - 1]!= y[n - 1]) // f , if elements != ï false, if ( ïî areIdentical(x, y,n -1), otherwise // continue public boolean areIdentical(int[] x, int[] y, int n) { if (x.length != y.length) return false; else if (n == 1) return (x[0] == y[0]); else if (x[n-1] != y[n-1]) return false; else return areIdentical(x,y,n-1); } Palindromes Consider the following – assume no mixed case boolean is_palindrome(String s) { // separate case for shortest strings if (s.length() <= 1) return true; // get first and last character char first = s.charAt(0); char last = s.charAt(s.length()-1); if (first == last) { //substring(1,s.length(0-1) returns a string //between 1st and length-1 (not inclusive) String shorter = s.substring(1, s.length()-1); return is_palindrome(shorter); } else return false; } Still Another Example: What does the following java method do? public int whoKnows(int[] x, int n) { if (n == 1) return x[0]; else return Math.min(x[n-1], whoKnows(x, n-1)); } Traversing a Sequential Linked List Iteratively: method invoked by - traverseList(p) Algorithm traverseList(Node t) Node temp = t; while (temp != null) { visit (temp.data()); temp = temp.next(); //move to next node } return Recursively: method invoked by - traverseList(p) Algorithm traverseList(Node t) if (t != null) { visit (t.data()); traverseList(t.next); } return Reversing Nodes in a “null-terminated” Sequential Linked List Iteratively: method invoked by - p = reverseList(p) Algorithm reverseList(Node t) returns Node Node p = t; Node q = null; while (p != null) { r = q; q = p; p = p.next; q.next = r; } return q Recursively: method invoked by - p = reverseList(p,null) Algorithm reverseList(Node x, Node y) returns Node Node t; if (x == null) then return y; else { t = reverseList(x.next, x); x.next = y; return t; } More Linked List Examples Algorithm to copy a linked list public Node copyList (Node p) { Node q; q = null; if (p != null) { q = new Node(); q.data = p.data; q.link = copyList(p.link); } return q; } Are two linked lists identical? public boolean identicalLists (Node s, node t) { boolean x = false; if ((s == null) && (t == null)) return (true); else if ((s != null) && (t != null)) { if (s.data == t.data) x = true; else x = false; if (x) return (identicalLists(s.link, t.link); } } Linked List Examples Algorithm to count the number of nodes in a linked list public int countNodes (Node s) { if (s != null) return ( 1 + countNodes(s.next); else return 0; } Other applications: Traversing non-linear data structures (trees) Sorting (Quicksort, etc.) Searching (Binary search) Find all URLs reachable, directly or indirectly, for a given Web Page Mutual Recursion Definition: a form of recursion where two mathematical or computational objects are defined in terms of each other. Very common in some problem domains, such as recursive descent parsers. Example: function1() { //do something f2(); //do something } function2() { //do something f1(); //do something } Mutual Recursion A standard example of mutual recursion (admittedly artificial) is determining whether a non-negative integer is even or is odd. Uses two separate functions and calling each other, decrementing each time. Example: boolean even( int number ) { if( number == 0 ) return true; else return odd(Math.abs(number)1); } boolean odd( int number ) { if( number == 0 ) return false; else return even(Math.abs(number)1); } Mutual Recursion A contrived, and not efficient at all, example of calculating Fibonacci numbers using mutual recursion Example: int Babies(int n) { if(n==1) return 1; else return Adults(n-1); } int Adults(int n) { if(n==1) return 0; else return Adults(n-1) + Babies(n-1); } int Fib_Mutual_Rec(int n) { return Adults(n) + Babies(n); // return Adults(n+1); // is also valid // return Babies(n+2); // is also valid } Primitive Recursion vs Non-primitive Recursion All examples that we have seen to this point have used primitive recursion. Non- primitive recursion: Ackerman(m,n) = ì n + 1, if ï í Ack(m - 1,1), if ï î Ack(m - 1, Ack(m, n - 1)), if m=0 m ¹ 0 and n = 0 m ¹ 0 and n ¹ 0 Pitfalls of Recursion Missing base case – failure to provide an escape case. No guarantee of convergence – failure to include within a recursive function a recursive call to solve a subproblem that is not smaller. Excessive space requirements - a function calls itself recursively an excessive number of times before returning; the space required for the task may be prohibitive. Excessive recomputation – illustrated in the recursive Figonacci method which ignors that several sub-Fibonacci values have already been computed. Disadvantages of Recursion Method calls may be time-consuming. Recursive methods may take longer to run. More dynamic memory is used to support recursion.