CS 228 meeting -*- Outline -*- * recursion (Heddington Ch. 6) ** intro *** recursion is a good tool for defining certain concepts, thinking e.g., fibonacci numbers (Heddington 6.1), factorial (Heddington 6.8) ------------------ RECURSIVE DEFINITIONS n! = 1.0, if n = 0 (base) n! = n * (n-1)!, if n > 1 (recursion) dsum(x) = 0, if x = 0 dsum(x) = x%10 + dsum(x/10), if x > 0 ------------------ e.g., Context-free grammars, BNF (Heddington 6.2) as they point out (6.6) recursion is sufficient for programming *** potentially inefficient **** algorithmic inefficiency some fully recursive algortihms are much slower than their iterative counterparts (e.g., fibonnaci) **** compilers usually give general, but space inefficient implementation so sometimes want to avoid writing recursive code ** operational view of recursion (6.3) ------------------- OPERATIONAL VIEW OF RECURSION double fact(unsigned int n) { if (n == 0) { return 1.0; } else { return n * fact(n-1); } } Control Flow Data -------------------- see 6.4 for how to translate recursive def to recursive code write out the following trace, draw pictures ------------------------------------- fact(x) x: 3 n * fact(n-1); n: 3 n * fact(n-1); n: 2 n * fact(n-1); n: 1 return 1.0; n: 0 return 1.0; n: 1 return 2.0; n: 2 return 6.0; n: 3 -------------------------------------- -------------------------------------- STACK SNAPSHOTS fact n: 3 main fact n: 2 fact n: 3 main fact n: 1 fact n: 2 fact n: 3 main fact n: 0 fact n: 1 fact n: 2 fact n: 3 main fact n: 1 fact n: 2 fact n: 3 main fact n: 2 fact n: 3 main fact n: 3 main ------------------------------ ------------------------------- INFORMATION STORED WHEN CALLING A (RECURSIVE) C++ FUNCTION For each call, allocate an *activation record* containing: - storage for parameters, with: * values (copies) of value parameters * address of reference parameters - storage for other local variables - return address ------------------------------- ** translating recursion to while-loops via tail-recursion summary so far: recursion is a useful way to define problems, etc. recursively coded functions can be expensive so sometimes need to eliminate recursion ------------------------------- ELIMINATING RECURSION recursive definition | | straightforward v fully recursive function (psuedo)code | | design sequence using | extra parameters v tail-recursive function (psuedo)code | | standard pattern v iteration coded as a while-loop ------------------------------- this is the basic idea, of course want to be able to go from recursive def to while-loop quickly, so intermediate steps more of an aid to understanding then necessity in practice *** example: factorial ---------------------------- DESIGNING THE TAIL RECURSIVE FACTORIAL recursive factorial step: n! = n * (n-1)! 1 recursive call, so add 1 accumulator, f design sequence of steps: f n 1.0 * 4 4.0 * 3 12.0 * 2 24.0 * 1 24.0 done when n = 0 ---------------------------- the accumulator idea should be familiar from 227 ---------------------------- TAIL RECURSION // FULLY RECURSIVE FACTORIAL double fact_rec(unsigned int n) { if (n == 0) { return 1.0; } else { return n * fact_rec(n-1); } } // TAIL-RECURSIVE FACTORIAL double fact_it(unsigned int n, double f) { if (n == 0) { return f; } else { return fact_it(n-1, n * f); } } double fact(unsigned int n) { return fact_it(n, 1.0); } ---------------------------- note that C++ compilers don't necessarily do tail recursion elimination as did Scheme, so this isn't really iterative yet in C++. to do real iteration have to use a loop. ---------------------------- TAIL RECURSION INTO WHILE LOOP // TAIL-RECURSIVE FACTORIAL double fact_it(unsigned int n, double f) { if (n == 0) { return f; } else { return fact_it(n-1, n * f); } } double fact(unsigned int n) { return fact_it(n, 1.0); } // WHILE-LOOP FACTORIAL double fact(unsigned int n) { // let oldN = n double f = 1.0; // INV: f * n! = oldN! while (!(n == 0)) { f = n * f; n = n-1; } // ASSERT: n = 0 AND f * n! = oldN! return(f); } ---------------------------- of course, can use a for loop in this case. the assertion inside the while loop is the *loop invariant* *** example: fibonacci ----------------------------------- FIBONACCI NUMBERS fib(0) = 0 fib(1) = 1 fib(n) = fib(n-1) + fib(n-2), if n > 1. e.g., fib(7) = 13 2 recursive calls, so use 2 accumulators sequence is: acc1 acc2 n i fib(i) 0 1 7 1 1 1 1 6 2 1 1 2 5 3 2 2 3 4 4 3 3 5 3 5 5 5 8 2 6 8 8 13 1 7 13 ----------------------------------- this should be familiar, note that acc2 does the fibonacci sequence starting at 1 ----------------------------------- FIBONACCI // FULLY RECURSIVE VERSION double fib(unsigned int n) { if (n < 2) { return n; } else { return fib(n-1) + fib(n-2); } } // TAIL RECURSIVE VERSION double fib_it(unsigned int n, double acc1, double acc2) { if (n == 1) { return acc2; } else { return fib_it(n-1, acc2, acc1 + acc2); } } double fib(unsigned int n) { if (n == 0) { return n; } else { return fib_it(n, 0.0, 1.0); } } --------------------------- this should be familiar --------------------------- FIBONACCI CONTINUED // TAIL RECURSIVE VERSION double fib_it(unsigned int n, double acc1, double acc2) { if (n == 1) { return acc2; } else { return fib_it(n-1, acc2, acc1 + acc2); } } double fib(unsigned int n) { if (n == 0) { return n; } else { return fib_it(n, 0.0, 1.0); } } // WHILE LOOP VERSION double fib(unsigned int n) { } ----------------------------------- develop the code below, writing it in above. double fib(unsigned int n) { double acc1 = 0.0; double acc2 = 1.0; double temp; if (n == 0) { return n; } else { // let oldN = n // INV: acc2 = fib(oldN - n + 1) while (!(n == 1)) { n = n-1; temp = acc1; acc1 = acc2; acc2 = temp + acc2; } // ASSERT: n = 1 AND acc2 = fib(oldN) return acc2; } } *** summary, the general case ------------------------------- GENERAL TRANSLATION OF TAIL RECURSION T tail_rec_fun(S x, T acc) { if (base_case(x)) { return acc; } else { tail_rec_fun(to_base(x), to_ans(x, acc)); } } T the_fun(S x) { ... tail_rec(x, init); ... } T the_fun(S x) { T acc = init; ... while (!(base_case(x))) { acc = to_ans(x, acc); x = to_base(x); } return acc; ... } ------------------------------- ------------------------------- SHORTCUT METHOD FOR IMPLEMENTING RECURSIVE DEFINITIONS 1. add accumulators, design sequence of variable values 2. use the form of the tail recursion *translation* as outline for the while-loop version ------------------------------- *** exponentiation if plenty of time: have them do the following exercise ------------------- FOR YOU TO WRITE USING A WHILE LOOP power(x, y) = 1, if y = 0 power(x, y) = x * power(x, y-1), if y > 0 double power(double x, unsigned int y); -------------------- ** parameter passing and recursion ---------------------- RECURSION AND PARAMETERS - fresh value parameters each activation (copying) ==> activations don't interfere - references & pointers refer to "original" storage (no copying) ==> activations can interfere - global variables (one copy) ==> activations can interfere ---------------------- *** global variables ----------------------------------- PROBLEMS WITH GLOBALS AND RECURSION double t; double fib_wrong(unsigned int n) { if (n < 2) { return n; } else { t = fib_wrong(n-1); t = t + fib_wrong(n-2); return t; } } stack snapshots for the call fib(4): fib n: 1 fib n: 0 fib n: 2 fib n: 2 fib n: 2 fib n: 3 fib n: 3 fib n: 3 fib n: 4 fib n: 4 fib n: 4 main t: ? main t: 1.0 main t: 1.0 fib n: 1 fib n: 3 fib n: 3 fib n: 3 fib n: 4 fib n: 4 fib n: 4 main t: 1.0 main t: 1.0 main t: 2.0 fib n: 1 fib n: 0 fib n: 2 fib n: 2 fib n: 2 fib n: 4 fib n: 4 fib n: 4 main t: 2.0 main t: 1.0 main t: 1.0 fib n: 2 fib n: 4 fib n: 4 main t: 1.0 main t: 1.0 -------------------------------------- you can use globals with recursion, but have to make sure each activation that sets them consumes the values before the next activation moral: be careful *** reference parameters and recursion --------------------- REFERENCE PARAMETERS AND RECURSION Recall (semantics): int i = 3; int & ir = i; int & ir2 = ir; ------------------------- draw picture of this the second initialization is just like passing a reference parameter to a recursive call, both point to same thing! so it's like having a single global shared... ------------------------- EUCLID'S ALGORITHM void gcd(int & x, int & y) { // PRE: x > 0 AND y > 0 // POST: x and y hold the gcd of x and y if (x > y) { x = x - y; gcd(x, y); } else if (y > x) { y = y - x; gcd(x, y); } } -------------------- trace this for gcd(a, b) where a = 17, and b = 11 main a: 17; b: 11 gcd main x,a: 17; y,b: 11 gcd gcd main x,a: 6; y,b: 11 gcd gcd gcd main x,a: 6; y,b: 5 ... So this works as you would expect, every activation manipulating the global a and b, but they don't interefere so ok. Similarly, if you need to work with a global or an array, pass by reference can work example: Heddington's Fill in section 6.3, has PixelMap as an array parameter to a recursive function.