Recursion CS 367 – Introduction to Data Structures Definition • Mathematically – a recursive function is one that is defined in terms of an absolute case and itself – example n= 1 n*(n-1)! Consider n = 3: n 0! 1! 2! 3! n=0 n>0 result 1 1 * 0! = 1 2 * 1! = 2 3 * 2! = 6 Definition • Programming – a recursive function is one that knows the answer to a specific case; in all other cases, the function calls itself again – example int factorial(int n) { if(n == 0) return 1; else return n * factorial(n – 1); } Function Call • To understand recursion, we need a better idea of what happens on a function call – remember the program stack? • on a function call, the compiler creates an activation record – – – – parameters passed in local variables return address return value (where applicable) • activation record gets pushed on the program stack Activation Records f3() f2() f1() main() parameters local variables return address return value parameters local variables return address return value parameters local variables return address return value Calling a Function • The program compiler takes care of building the activation record for each function – this means you don’t have to worry about writing it – it also means that a single function call is really a lot more code than you think • this means it takes awhile to do Calling a Function • High level code void main() { int z = double(5); } int double(int x) { int y = x * 2; return y; } • Compiler code # main ld r1, 5 push addr push r1 jmp # double # double pop r2 mult r3, r2, 2 pop r2 push r3 jmp r2 Calling a Function • The previous compiler code is very incomplete (and not quite correct either ) • Previous code does prove a point – calling a function requires more memory • must create and store the activation record – calling a function takes more time • must run extra code to build the activation record • Fastest program would be one that only had a main function – this is very unrealistic – why? – if it were easy to do, still would be a bad idea – why? Anatomy of a Recursive Call • Consider the factorial example used earlier int factorial(int n) { if(n == 0) { return 1; } else { return n * factorial(n-1); } } • Now consider the following code for main() void main() { int f = factorial(3); print(f); } Anatomy of a Recursive Call factorial(3) factorial(2) n=1 f3() ret = 1 * f4 n=2 f2() ret = 2 * f3 n=3 f1() ret = 3 * f2 n=2 f2() ret = 2 * f3 n=3 f1() ret = 3 * f2 n=3 f1() ret = 3 * f2 main() f = factorial(1) main() f = main() f = main() f = return return return factorial(0) f3() f3() f2() f1() n=0 ret = 1 n=1 ret = 1 * n=2 ret = 2 * n=3 ret = 3 * main() f = n=1 f3() ret = 1 * 1 n=2 f2() ret = 2 * f3 n=3 f1() ret = 3 * f2 main() f = n=2 f2() ret = 2 * 1 n=3 f1() ret = 3 * f2 main() f = n=3 f1() ret = 3 * 2 main() f = Anatomy of a Recursive Call • Obviously, the last step in the previous example is missing (not enough room) – f1 will return 6 and the value (f) in main will be equal to 6 • Major point – using recursion can get very expensive • consume lots of memory • execute a lot of extra instructions – what is the alternative? Loops • Almost any recursive function can be rewritten as a loop – a loop will be much less time consuming • no extra activation records to construct and store – consider the factorial example one last time int factorial(int n) { int sum = 1; for(int i=1; i<=n; i++) sum *= i; return sum; } Loops • Notice no function calls inside this version of factorial() – will run much faster than the recursive version • Why use recursion? – in some cases it is easier to understand and write a recursive function Reverse() • Consider a function that prints a string of characters in the reverse order – ABC will be printed out as CBA void reverse() { char ch = getChar(); if(ch != ‘\n’) { reverse(); System.out.println(ch); } } Reverse() • Note that this recursive function doesn’t return anything – simply reads a character and waits to print it until the one after it is printed • very easy to understand • very quick to program – how would you convert this to an iterative solution? Reverse() • Sol’n in Java • Sol’n in C/C++ void reverse() { String stack = new String(); stack = buffer.readLine(); for(top=stack.length()-1; top>=0; top--) System.out.print(stack.charAt(top)); } void reverse() { char stack[80]; int top=0; stack[top] = getch(); while(stack[top] != ‘\n’) stack[++top] = getch(); for(top -= 1; top >= 0; top--) System.out.print(stack[top]); } Reverse() • Iterative sol’n must first read whole string into memory – then it can use a loop to print the string out – Java sol’n looks so simple because of the String.length() operator • it tells us exactly how long the string is • many languages don’t have this feature • There are fewer steps and less code in the recursive sol’n – because we’re interacting with I/O, recursive nature isn’t such a big deal • why? Backtracing • One of the best uses of recursion is in navigating a large, directed search space – in other words, if you are at a certain point, P, in the search space, you need to pick the next spot to search • if you go in one direction, you can only go back the other direction by first returning to point P – examples • • • • finding a path from one city to another 8 queens example best move in a game searching through a tree data structure (next week) Finding a Path • Consider the graph of flights from the lecture on stacks Z Key : city (represented as C) Y C1 W R C2 : flight from city C to city C 1 2 S P T Example flight goes from W to S W X Q S Finding a Path • We showed how to use a stack to find a route from P to Y • We can also do it using recursion public boolean findPath(City origin, City destination) { // pick a possible city to go to – make that city the origin if(findPath(nextCity, destination)) // found the city else // that path didn’t work, try another one } • The above recursive sol’n is still going to need some kind of loop inside it – have to check all the possible routes from a city