420 likes | 535 Vues
This comprehensive guide explores recursion in programming, detailing its principles, rules, and practical examples. It emphasizes the need for base cases to prevent infinite recursion and introduces various recursive methods, including calculating factorials and printing strings. You'll discover how recursive calls manage their own local variables and process expressions. By examining specific implementation examples and discussing potential pitfalls, the guide equips you with the necessary knowledge to effectively utilize recursion in your coding projects.
E N D
Recursion Joe Meehean
Recursive Methods void g(){ ... h(); ... } void h(){ ... g(); ... } void f(){ ... f(); ... } • call themselves directly • or indirectly
Recursive Method Example int factorial(int x){ inthandback; if( x <= 1 ){ handback = 1; }else{ handback = x * factorial(x-1); } return handback; } factorial(4) = 4 * factorial(4 – 1) factorial(3) = 3 * factorial(3 – 1) … factorial(1) = 1
Conceptual Recursion • When a recursive call is made: • marks current place in code • clones itself • copy of code • new parameters • new set of uninitialized local variables • clone executes starting at beginning • When clone returns • clone is cleaned up • previous version starts at just afterrecursive method call
CLASS ACTIVITY void doClap(int k){ if(k == 0 ) return; clap k times; doClap(k – 1); return; } 1st person: doClap(3)
Incorrect Recursion void doClap(int k){ clap k times; doClap(k + 1); return; } • doClap(3) • 3, 4, 5, 6, … • memory access error • need code to stop recursion eventually
Recursion Rule 1 void doClap(int k){ if( k == 0 ) return; clap k times; doClap(k + 1); return; } • Still has infinite recursion? • Must have a base case • Condition under which no recursive call is made • Helps prevent infinite recursion
Recursion Rule 2 void doClap(int k){ if( k == 0 ) return; clap k times; doClap(k - 1); return; } • Does doClap follow rules 1 & 2? • Infinite recursion? • If so, how should we change the code Must make progress towards base case
Recursion Rule 2 void doClap(int k){ if( k <= 0 ) return; clap k times; doClap(k - 1); return; } • Does doClap follow rules 1 & 2? • Infinite recursion? • If so, how should we change the code Must make progress towards base case
Another Example void printStr(string s, int k){ if( k >= s.length() ) return; cout << s[k]; printStr(s, k+1); } printStr(“hello”, 0) => “hello” printStr(“hello”, 2) => “llo”
Recursion Rule 3 void printStr(string s, int k){ if( k >= s.length() ) return; cout << s[k]; printStr(s, k+1); } • Design rule • assume that all recursive calls work • even if you haven’t finished writing it yet
Another Example void printStr(string s, int k){ if( k >= s.length() ) return; cout << s[k] << endl; printStr(s, k+1); } printStr(“hello”, 0) h printStr(“hello”, 1) e printStr(“hello”, 2) l printStr(“hello”, 3) l printStr(“hello”, 4) o printStr(“hello”, 5)
Another Example void printStr(string s, int k){ if( k >= s.length() ) return; printStr(s, k+1); cout << s[k] << endl; } printStr(“hello”, 0) h printStr(“hello”, 1) e printStr(“hello”, 2) l printStr(“hello”, 3) l printStr(“hello”, 4) o printStr(“hello”, 5)
Understanding Recursion • A method may have statements • before recursive call • after recursive • both • After statements are done only • when the recursive call is finished • and all the recursive calls recursive calls have finished
DISCUSSION BREAK!!! void printStr(string s, int k){ if( k >= s.length() ) return; cout << s[k] << endl; printStr(s, k+1); cout << s[k] << endl; } Whats printed for printStr(“STOP, 0)?
DISCUSSION BREAK!!! void printStr(string s, int k){ if( k >= s.length() ) return; cout << s[k] << endl; printStr(s, k+1); cout << s[k] << endl; } • Whats printed for printStr(“STOP, 0)? • STOPPOTS • How can we change it to print • STOP<BREAK>POTS
DISCUSSION BREAK!!! void printStr(string s, int k){ if( k >= s.length() ){ cout << “<BREAK>” << endl; return; } cout << s[k] << endl; printStr(s, k+1); cout << s[k] << endl; } • How can we change it to print • STOP<BREAK>POTS
Local Variables void mystery(int j){ if( j <= 0 ) return; intk = j * 2; mystery(j – 1); cout << k << “ “; } • Each clone gets own local variables • Outputs => 2 4 6
Recursive Call in Expressions // sums #s from 1 to max // Iterative version intdoSum(int max){ int sum = 0; for(inti = 1; i <= max; i++){ sum += i; } return sum; }
Recursive Call in Expressions // sums #s from 1 to max // Iterative version intdoSum(int max){ int sum = 0; for(inti = 1; i <= max; i++){ sum += i; } return sum; } // Recursive version intdoSum(int max){ if( max <= 0 ) return 0; return max + doSum(max – 1); }
Recursive Call in Expressions doSum(3) + 3 doSum(2) + 2 doSum(1) + 1 doSum(0)
Recursion in Practice • Can do recursion with int parameters • count up or down to base case • Or, with recursive data structures • a linked list is either empty • or a node followed by a linked list
Recursion in Practice void printList(Node<T> *node){ if( node == NULL ) return; cout << node->data << endl; printList(node->next); } Prints list in order
DISCUSSION BREAK!!! void printList(Node<T> *node){ if( node == NULL ) return; cout<< node->data << endl; printList(node->next); } How can we change this code to print list backwards?
DISCUSSION BREAK!!! void printList(Node<T> *node){ if( node == NULL ) return; printList(node->next); cout << node->data << endl; } Prints list backwards Rare case where recursion makes code faster as well as simpler
How does recursion really work? • Activation records (ARs) • stack of ARs maintained at runtime • 1 AR for each active method • active methods have been called, but not returned yet • Each AR includes: • function parameters • local variables • return address
How does recursion really work? • Activation Record Stack • when method is called, its AR is pushed • when method returns, its AR is popped • return address in popped AR tells program where to return control flow
Recursion Examples • Auxiliary recursion • wrapper method to do something ONCE before or after recursion • OR pass more parameters • recursion in auxiliary method • wrapper sometimes called a driver • Multiple recursive calls per method • need to recurse down a few different paths • more on this when we talks about trees
Example 1 // prints a header and the endline // calls aux to print the list void printList(Node<T> *node){ cout << “The list contains: “; printAux(node); cout << endl; } // prints a single string containing // each element in brackets void printAux(Node<T>* node){ if( node == NULL ) return; cout << “[“ << node->data << “]”; printAux(node->next); }
Example 2 // public method calls private method // with member variable int List::numNodes(){ return numAux(phead_); } // actually counts the nodes int List::numAux(Node<T>* node){ if( node == NULL ){ return 0; } return 1 + numAux(node->next); }
Example 3: Extra parameters intweirdSum(vector<int>& intVect){ return sumVector(intVect, 1); } // actually sums the entries intsumVector( vector<int>& intVect, intpos){ if( pos >= intVect.size() ) return 0; return intVect[pos] + sumAux(intVect, pos + 1); } Sum all elements in a vector but the first value
Example 4: Multi-recursive calls int fib(int n){ if( n <= 2) return 1; return fib(n – 2) + fib(n – 1); } • Fibonacci • fib(1) = 1, fib(2) = 1 • fib(N) for N > 2 = fib(N-1) + fib(N-2) • fib(3) = fib(2) + fib(1) = 1 + 1 = 2
Fibonacci Call Trace fib(4) 3 + 1 2 fib(3) fib(2) 2 + 1 1 fib(1) fib(2) Note: fib(2) is called twice Redoing work
Recursion Rule 4 • Compound interest rule • Never duplicate work • don’t solve the same recursive instance in separate recursive calls • repeats work you’ve already done • more on how to prevent this in CS242
What good is it? • Recursion DOES NOT make your code faster • Recursion DOES NOT use less memory • Recursion DOES make your code simpler • sometimes
When is it a bad idea? • When you don’t get anything from it? • Tail recursion • the only recursive call is the last line • local variables stored just to be thrown away • Tail recursion can be replaced with a while loop
When is it a bad idea? void printStr(string s, int k){ if( k >= s.length() ) return; cout << s[k] << endl; printStr(s, k+1); } void printStrin(string s, int k){ while( true ){ if( k >= s.length() ) return; cout<< s[k] << endl; k = k + 1; } }
When is it a bad idea? void printString(string s, int k){ while( k < s.length()){ cout<< s[k] << endl; k = k + 1; } }
Recursion Rules Recap Base case Make progress Design rule Compound interest rule