Recursion Chapter 8 CS 3358 Summer II 2013 Jill Seaman What is recursion?! Generally, when something contains a reference to itself! Math: defining a function in terms of itself! Computer science: when a function calls itself Sections 8.1-8.4, (8.5 if you can) 1 2 How can a function call itself?! What happens when this function is called? How can a function call itself?! Infinite Recursion: void message() { cout << \n ; message(); message(); 3... 4
Recursive message() modified! How about this one? void message(int n) { if (n > 0) { cout << \n ; message(n-1); message(5); 5 Tracing the calls! 6 nested calls to message: message(5): outputs This is a recursive function calls message(4): outputs This is a recursive function calls message(3): outputs This is a recursive function calls message(2): outputs This is a recursive function calls message(1): outputs This is a recursive function calls message(0): does nothing, just returns! depth of recursion (#times it calls itself) = 5. 6 Why use recursion?! It is true that recursion is never required to solve a problem - Any problem that can be solved with recursion can also be solved using iteration.! Recursion requires extra overhead: function call + return mechanism uses extra resources However:! Some repetitive problems are more easily and naturally solved with recursion - Iterative solution may be unreadable to humans 7 Why use recursion?! Recursion is the primary method of performing repetition in most functional languages. - Implementations of functional languages are designed to process recursion efficiently - Iterative constructs that are added to many functional languages often don t fit well in the functional context.! Once programmers adapt to solving problems using recursion, the code produced is generally shorter, more elegant, easier to read and debug. 8
How to write recursive functions! Branching is required (If or switch)! Find a base case - one (or more) values for which the result of the function is known (no repetition required to solve it) - no recursive call is allowed here! Develop the recursive case - For a given argument (say n), assume the function works for a smaller value (n-1). - Use the result of calling the function on n-1 to form a solution for n 9 factorial! Mathematical definition of n! (factorial of n) if n=0 then n! = 1 if n>0 then n! = 1 x 2 x 3 x... x (n-1) x n! What is the base case?! If we assume (n-1)! can be computed, how can we get n! from that? 10 factorial! Mathematical definition of n! (factorial of n) if n=0 then n! = 1 if n>0 then n! = 1 x 2 x 3 x... x n! What is the base case? - n=0 (result is 1)! If we assume (n-1)! can be computed, how can we get n! from that? - n! = n * (n-1)! 11 factorial int factorial(int n) { if (n==0) return 1; return n * factorial(n-1); int number; cout << Enter a number ; cin >> number; cout << The factorial of << number << is << factorial(number) << endl; 12
Tracing the calls Tracing the calls! Calls to factorial: factorial(4): return 4 * factorial(3); calls factorial(3): return 3 * factorial(2); calls factorial(2): return 2 * factorial(1); calls factorial(1): return 1 * factorial(0); calls factorial(0): return 1;! each return statement must wait for the result of the recursive call to compute its result 13! Calls to factorial: factorial(4): return 4 * factorial(3); =4 * 6 = 24 calls factorial(3): return 3 * factorial(2); =3 * 2 = 6 calls factorial(2): return 2 * factorial(1); =2 * 1 = 2 calls factorial(1): return 1 * factorial(0); =1 * 1 = 1 calls factorial(0): return 1;! Every call except the last makes a recursive call! Each call makes the argument smaller 14 Recursive functions over ints! Many recursive functions (over integers) look like this: type f(int n) { if (n==0) //do the base case //... f(n-1)... Recursive functions over lists! You can write recursive functions over lists using the length of the list instead of n - base case: length=0 ==> empty list - recursive case: assume f works for list of length n-1, what is the answer for a list with one more element?! We will do examples with: 15 - arrays - vectors - linked lists - strings 16
sum of the list! Recursive function to compute sum of a list of numbers! What is the base case? - length=0 (empty list) sum = 0! If we assume we can sum the first n-1 items in the list, how can we get the sum of the whole list from that? - sum (list) = sum (list[0..n-2]) + list[n-1] sum of a list: array int sum(int a[], int size) { //size is number of elems if (size==0) return sum(a,size-1) + a[size-1]; call sum on first n-1 elements For a list with size = 4: sum(a,4) The last element sum(a,3) + a[3] = sum(a,2) + a[2] + a[3] = sum(a,1) + a[1] + a[2] + a[3] = sum(a,0) + a[0] + a[1] + a[2] + a[3] = 0 + a[0] + a[1] + a[2] + a[3] Assume I am given the answer to this 17 18 sum of a list: vector int sum(vector<int> v) { if (v.size()==0) { int x = v.back(); v.pop_back(); return x + sum(v); v.back() returns the last element! v.pop_back() creates the shorter vector! Aren t we changing x each time (size = 0 at end)? - No (why not?) int main () { vector<int> x; x.push_back(10); x.push_back(20); x.push_back(30); cout << "sum "<< sum(x) << endl; cout << "size "<< x.size()<< endl; - But something bad is happening each time. 19 sum of a list: vector int sum(vector<int> v) { if (v.size()==0) { int x = v.back(); v.pop_back(); return x + sum(v);! Aren t we changing x each time (size = 0 at end)? - No (why not?) int main () { vector<int> x; x.push_back(10); x.push_back(20); x.push_back(30); cout << "sum "<< sum(x) << endl; cout << "size "<< x.size()<< endl; Pass by value ==> v is a copy of x, so x is unchanged - But something bad is happening each time. Pass by value ==> v makes a copy of x, for EACH recursive call 20
sum of a list: vector without copying int sumrec(vector<int> & v) { Use pass by reference if (v.size()==0) (it will change x) { int x = v.back(); v.pop_back(); return x + sumrec(v); int sum (const vector<int> & v) { vector<int> x (v); //make ONE copy only return sumrec(x);! Sometimes an auxiliary or driver function is needed to set things up before starting recursion. 21 sum of a list: linked list! Add a sum function to List_3358_LL.h // this is the public one int List_3358::sum() { return sumnodes(head); // this one is private int List_3358::sumNodes(Node *p) { if (p==null) { int x = p->value; return x + sumnodes(p->next); advances p to next Node, (makes the shorter list) sumnodes(p) will sum the Nodes starting with the one p points to until the end of the list (NULL) 22 Summary of the list examples! How to determine empty list, single element, and the shorter list to perform recursion on. Array Vector Linked list p points to first node Base case size==0 v.size()==0 p==null last(or first) element shorter list (recursive call) a[size-1] v.back() p->value use size-1 v.pop_back()* p->next count character occurrences in a string! Recursive function to count the number of times a specific character appears in a string! We will use the string member function substr to make a smaller string - str.substr (int pos, int length); - pos is the starting position in str - length is the number of characters in the result string x = hello there ; cout << s.substr(3,5); lo th *may need to copy original vector 23 24
count character occurrences in a string int numchars(char target, string str) { if (str.empty()) { { int result = numchars(search, str.substr(1,str.size())); if (str[0]==target) return 1+result; return result; string a = "hello"; cout << a << numchars('l',a) << endl; 25 Three required properties of recursive functions! A Base case - a non-recursive branch of the function body. - must return the correct result for the base case! Smaller caller - each recursive call must pass a smaller version of the current argument.! Recursive case - assuming the recursive call works correctly, the code must produce the correct answer for the current argument. 26 greatest common divisor! Greatest common divisor of two non-zero ints is the largest positive integer that divides the numbers without a remainder! This is a variant of Euclid s algorithm: gcd(x,y) = y if y divides x evenly, otherwise: gcd(x,y) = gcd(y,remainder of x/y), or gcd(y,x%y) in c++! Code: greatest common divisor int gcd(int x, int y) { cout << "gcd called with " << x << " and " << y << endl; if (x % y == 0) { return y; { return gcd(y, x % y);! It s a recursive definition! If x < y, then x%y is x (so gcd(x,y) = gcd(y,x))! This moves the larger number to the first position. 27 cout << "GCD(9,1): " << gcd(9,1) << endl; cout << "GCD(1,9): " << gcd(1,9) << endl; cout << "GCD(9,2): " << gcd(9,2) << endl; cout << "GCD(70,25): " << gcd(70,25) << endl; cout << "GCD(25,70): " << gcd(25,70) << endl; 28
! Output: greatest common divisor gcd called with 9 and 1 GCD(9,1): 1 gcd called with 1 and 9 gcd called with 9 and 1 GCD(1,9): 1 gcd called with 9 and 2 gcd called with 2 and 1 GCD(9,2): 1 gcd called with 70 and 25 gcd called with 25 and 20 gcd called with 20 and 5 GCD(70,25): 5 gcd called with 25 and 70 gcd called with 70 and 25 gcd called with 25 and 20 gcd called with 20 and 5 GCD(25,70): 5 29! Series of : 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,...! Starts with 0, 1. Then each number is the sum of the two previous numbers F0 = 0 F1 = 1 Fi = Fi-1 + Fi-2 (for i > 1)! It s a recursive definition 30! Code: int fib(int x) { if (x<=1) return x; return fib(x-1) + fib(x-2); cout << "The first 13 fibonacci numbers: " << endl; for (int i=0; i<13; i++) cout << fib(i) << " "; cout << endl; The first 13 fibonacci numbers: 0 1 1 2 3 5 8 13 21 34 55 89 144 31! Modified code to count the number of calls to fib: int fib(int x, int &count) { count++; if (x<=1) return x; return fib(x-1, count) + fib(x-2, count); cout << "The first 40 fibonacci numbers: " << endl; for (int i=0; i<40; i++) { int count = 0; int x = fib(i,count); cout << "fib (" << i << ")= " << x << " # of recursive calls to fib = " << count << endl; 32
! Counting calls to fib: output The first 40 fibonacci numbers: fib (0)= 0 # of recursive calls to fib = 1 fib (1)= 1 # of recursive calls to fib = 1 fib (2)= 1 # of recursive calls to fib = 3 fib (3)= 2 # of recursive calls to fib = 5 fib (4)= 3 # of recursive calls to fib = 9 fib (5)= 5 # of recursive calls to fib = 15 fib (6)= 8 # of recursive calls to fib = 25 fib (7)= 13 # of recursive calls to fib = 41 fib (8)= 21 # of recursive calls to fib = 67 fib (9)= 34 # of recursive calls to fib = 109 fib (10)= 55 # of recursive calls to fib = 177 fib (11)= 89 # of recursive calls to fib = 287 fib (12)= 144 # of recursive calls to fib = 465 fib (13)= 233 # of recursive calls to fib = 753... 33 fib (40)= 102,334,155 # of recursive calls to fib = 331,160,281! Why are there so many calls to fib? fib(n) calls fib(n-1) and fib(n-2)! Say it computes fib(n-2) first.! When it computes fib(n-1), it computes fib(n-2) again fib(n-1) calls fib((n-1)-1) and fib((n-1)-2) = fib(n-2) and fib (n-3)! It s not just double the work. It s double the work for each recursive call.! Each recursive call does more and more redundant 34 work! Trace of the recursive calls for fib(5) Fib(3) Fib(4) Fib(5) Fib(3) Fib(2) Fib(2) Fib(1) Fib(2) Fib(1) Fib(1) Fib(0) Fib(1) Fib(0) Fib(1) Fib(0)! The number of recursive calls is - larger than the Fibonacci number we are trying to compute - exponential, in terms of n! Never solve the same instance of a problem in separate recursive calls. - make sure f(m) is called only once for a given m 35 36
! Find an item in a list, return the index or -1! Works only for SORTED lists! Compare target value to middle element in list. - if equal, then return index Iterative version int binarysearch(const int array[], int size, int value) { int first = 0, last = size - 1, middle, position = -1; bool found = false; // First array element // Last array element // Mid point of search // Position of search value // Flag while (!found && first <= last) { middle = (first + last) / 2; // Calculate mid point - if less than middle elem, search in first half if (array[middle] == value) { // If value is found at mid found = true; - if greater than middle elem, search in last half position = middle;! If search list is narrowed down to 0 elements, if (array[middle] > value) // If value is in lower half return -1 last = middle - 1;! Divide and conquer style algorithm first = middle + 1; // If value is in upper half 37 38 return position; Example The target of your search is 42. Given the following list of integers, record the values of first, last, and middle during a binary search. Assume the following numbers are in an array.!1 7 8 14 20 42 55 67 78 101 112 122 170 179 190 Repeat!! the exercise with a target of 82 39 5 Recursive version! Convert the iterative version to recursive! What is the base case? - empty list: result = -1 (not found)! What is the recursive case? - split list into: middle value, first half, last half - if middle value equals target, then return its index - if less than middle elem, search in first half two base cases - if greater than middle elem, search in last half two recursive cases! Need to add parameters for first and last index of the current subpart of the list to search. 40
Recursive version Running time efficiency int binarysearchrec(const int array[], int first, int last, int value) { int middle; // Mid point of search if (first > last) //check for empty list return -1; middle = (first + last)/2; //compute middle index if (array[middle]==value) return middle; if (value < array[middle]) //recursion return binarysearchrec(array, first,middle-1, value); return binarysearchrec(array, middle+1,last, value);! What is the Big-O analysis of the running time?! N is the length of the list to search! Worst case: keep dividing N by 2 until it is less than 1.! This is equivalent to doubling 1 until it gets to N. 1*2 = 2 2*2 = 4 4*2 = 8 8*2 = 16 16*2 = 32 After 6 steps we have 2 6 int binarysearch(const int array[], int size, int value) { return binarysearchrec(array, 0, size-1, value); 32*2 = 64 After k steps we have 2 k 41 42 Running time efficiency! How many steps does it take to double 1 and get to N? 2 k = N! How do we solve that for k?! Definition of logarithm (see math textbook): logbn = k if B k = N! So solving for k: k = log2n The logarithm is the exponent Running time efficiency! How many steps does it take to repeatedly double 1 and get to N? log2n! How many steps does it take to repeatedly divide N by 2 and get to 1? log2n! Since (worst case) binary search repeatedly divides the length of the list by 2, until it gets down to one, its running time is 43 O(log N) 44