420 likes | 596 Views
Introduction to Recursion and Recursive Algorithms. CS2110. Different Views of Recursion. Recursive Definition : n! = n * (n-1)! (non-math examples are common too) Recursive Procedure : a procedure that calls itself.
E N D
Different Views of Recursion Recursive Definition: n! = n * (n-1)! (non-math examples are common too) Recursive Procedure: a procedure that calls itself. Recursive Data Structure: a data structure that contains a pointer to an instance of itself: public class ListNode { Object nodeItem; ListNode next, previous; … }
Recursion in Algorithms • Recursion is a technique that is useful • for defining relationships, and • for designing algorithms that implement those relationships. • Recursion is a natural way to express many algorithms. • For recursive data-structures, recursive algorithms are a natural choice
What Is Recursion? • A Definition Is Recursive If It Is Defined In Terms Of Itself • We use them in grammar school e.g. what is a noun phrase? • a noun • an adjective followed by a noun phrase • Descendants • the person’s children • all the children’s descendants
What Is Recursion? • Think self-referential definition • A definition is recursive if it is defined in terms of itself • Exponentiation - x raised to the y power • if y is 0, then 1 • otherwiseit’s x * (x raised to the y-1 power)
Other recursive definitions in mathematics • Factorial:n! = n (n-1)! and 0! = 1! = 1 • Fibonacci numbers: F(0) = F(1) = 1 F(n) = F(n-1) + F(n-2) for n > 1 • Note base case • Definition can’t be completely self-referential • Must eventually come down to something that’s solved “directly”
I know the steps needed to write a simple recursive method in Java • Strongly Agree • Agree • Disagree • Strongly Disagree
Recursive Factorial public static int factorial (int n) { if (n == 1) return 1; else return n * factorial(n-1); } • Exercise: trace execution (show method calls) for n=5
Why Do Recursive Methods Work? Activation Records on the Run-time Stack are the key: • Each time you call a function (any function) you get a new activation record. • Each activation record contains a copy of all local variables and parameters for that invocation. • The activation record remains on the stack until the function returns, then it is destroyed. Try yourself: use your IDE’s debugger and put a breakpoint in the recursive algorithm Look at the call-stack.
Broken Recursive Factorial public static int Brokenfactorial(int n){ int x = Brokenfactorial(n-1); if (n == 1) return 1; else return n * x; } • What’s wrong here? Trace calls “by hand” • BrFact(2) -> BrFact(1) -> BrFact(0) -> BrFact(-1) -> BrFact(-2) -> … • Problem: we do the recursive call first before checking for the base case • Never stops! Like an infinite loop!
Recursive Design Recursive methods/functions require: 1) One or more (non-recursive) base cases that will cause the recursion to end. if (n == 1) return 1; 2) One or more recursive cases that operate on smaller problems and get you closer to the base case. return n * factorial(n-1); Note: The base case(s) should always be checked before the recursive call.
Rules for Recursive Algorithms • Base case - must have a way to end the recursion • Recursive call - must change at least one of the parameters and make progress towards the base case • exponentiation (x,y) • base: if y is 0 then return 1 • recursive: else return (multiply x times exponentiation(x,y-1))
How to Think/Design with Recursion • Many people have a hard time writing recursive algorithms • The key: focus only at the current “stage” of the recursion • Handle the base case, then… • Decide what recursive-calls need to be made • Assume they work (as if by magic) • Determine how to use these calls’ results
Example: List Processing • Is an item in a list? First, get a reference current to the first node • (Base case) If current is null, return false • (Base case #2) If the first item equals the target, return true • (Recursive case – might be on the remainder of the list) • current = current.next • return result of recursive call on new current
Next: Trees and Grammars • Lab on binary tree data structures • Maybe: HW5 on grammars • Lecture Later: Recursion vs. iteration • Which to choose? • Does it matter? • Maybe Later: recursion as an design strategy
Next: Time Complexity and Recursion • Recursion vs. iteration • Which to choose? • Does it matter? • Later: recursion as an design strategy
Recursion vs. Iteration Interesting fact: Any recursive algorithm can be re-written as an iterative algorithm (loops) • Not all programming languages support recursion: COBOL, early FORTRAN. • Some programming languages rely on recursion heavily: LISP, Prolog, Scheme.
To Recurse or Not To Recurse? • Recursive solutions often seem elegant • Sometimes recursion is an efficient design strategy • But sometimes it’s definitely not • Important! we can define recursively and implement non-recursively • Many recursive algorithms can be re-written non-recursively • Use an explicit stack • Remove tail-recursion (compilers often do this for you)
To Recurse or Not to Recurse? • Sorting • Selection sort vs. mergesort – which to choose? • Factorial • Could just write a loop. • Any advantage to the recursive version? • Binary search • We’ll see two versions. Which to choose? • Fibonacci • Let’s consider Fibonacci carefully…
Recursive Fibonacci method • This is elegant code, no?long fib(int n) { if ( n == 0 ) return 1; if ( n == 1 ) return 1; return fib(n-1) + fib(n-2);} • Let’s start to trace it for fib(6)
Trace of fib(5) • For fib(5), we call fib(4) and fib(3) • For fib(4), we call fib(3) and fib(2) • For fib(3), we call fib(2) and fib(1) • For fib(2), we call fib(1) and fib(0). Base cases! • fib(1). Base case! • For fib(2), we call fib(1) and fib(0). Base cases! • For fib(3), we call fib(2) and fib(1) • For fib(2), we call fib(1) and fib(0). Base cases! • fib(1). Base case!
Fibonacci: recursion is a bad choice • Note that subproblems (like fib(2) ) repeat, and solved again and again • We don’t remember that we’ve solved one of our subproblems before • For this problem, better to store partial solutions instead of recalculating values repeatedly • Turns out to have exponential time-complexity!
Non-recursive Fibonacci • Two bottom-up iterative solutions: • Create an array of size n, and fill with values starting from 1 and going up to n • Or, have a loop from small values going up, but • only remember two previous Fibonacci values • use them to compute the next one • (See next slide)
Iterative Fibonacci long fib(int n) { if ( n < 2 ) return 1; long answer; long prevFib=1, prev2Fib=1; // fib(0) & fib(1) for (int k = 2; k <= n; ++k) { answer = prevFib + prev2Fib; prev2Fib = prevFib; prevFib = answer; } return answer; }
Next: Putting Recursion to Work • Divide and Conquer design strategy • Its form • Examples: • Binary Search • Merge Sort • Time complexity issues
Divide and Conquer • It is often easier to solve several small instances of a problem than one large one. • divide the problem into smaller instances of the same problem • solve (conquer) the smaller instances recursively • combine the solutions to obtain the solution for original input • Must be able to solve one or more small inputs directly • This is an algorithm design strategy • Computer scientists learn many more of these
General Strategy for Div. & Conq. Solve (an input I) n = size(I) if (n <= smallsize) solution = directlySolve(I); else divide I into I1, …, Ik. for each i in {1, …, k} Si = solve(Ii); solution = combine(S1, …, Sk); return solution;
Why Divide and Conquer? • Sometimes it’s the simplest approach • Divide and Conquer is often more efficient than “obvious” approaches • E.g. BinarySearch vs. Sequential Search • E.g. Mergesort, Quicksort vs. SelectionSort • But, not necessarily efficient • Might be the same or worse than another approach • We must analyze each algorithm's time complexity • Note: divide and conquer may or may not be implemented recursively
Top-Down Strategy • Divide and Conquer algorithms illustrate a top-down strategy • Given a large problem, identify and break into smaller subproblems • Solve those, and combine results • Most recursive algorithms work this way • The alternative? Bottom-up • Identify and solve smallest subproblems first • Combine to get larger solutions until solve entire problem
first mid last Binary Search of a Sorted Array • Strategy • Find the midpoint of the array • Is target equal to the item at midpoint? • If smaller, look in the first half • If larger, look in second half
Binary Search (non-recursive) int binSearch ( array[], target) { int first = 0; int last = array.length-1; while ( first <= last ) { mid = (first + last) / 2; if ( target == array[mid] ) return mid; // found it else if ( target < array[mid] ) // must be in 1st half last = mid -1; else // must be in 2nd half first = mid + 1 } return -1; // only got here if not found above }
Binary Search (recursive) int binSearch ( array[], first, last, target) { if ( first <= last ) { mid = (first + last) / 2; if ( target == array[mid] ) // found it! return mid; else if ( target < array[mid] ) // must be in 1st half return binSearch( array, first, mid-1, target); else // must be in 2nd half return binSearch(array, mid+1, last, target); } return -1; } • No loop! Recursive calls takes its place • But don’t think about that if it confuses you! • Base cases checked first? (Why? Zero items? One item?)
Algorithm: Mergesort • Specification: • Input: Array E and indexes first, and Last, such that the elements E[i] are defined for first <= i <= last. • Output: E[first], …, E[last] is sorted rearrangement of the same elements • Algorithm: void mergeSort(Element[] E, int first, int last){ if (first < last) { int mid = (first+last)/2; mergeSort(E, first, mid); mergeSort(E, mid+1, last); merge(E, first, mid, last); // merge 2 sorted halves } }
Exercise: Trace Mergesort Execution • Can you trace MergeSort() on this list?A = {8, 3, 2, 9, 7, 1, 5, 4};
Efficiency of Mergesort • Wait for CS2150 and CS4102 to study efficiency of this and other recursive algorithms • But… • It is more efficient that other sorts likeSelection Sort, Bubble Sort, Insertion Sort • It’s O(n lg n) which is the same order-class as the most efficient sorts (also quicksort and heapsort) • The point is that the D&C approach matters here, and a recursive definition and implementation is a “win”
Merging Sorted Sequences • Problem: • Given two sequences A and B sorted in non-decreasing order, merge them to create one sorted sequence C • Input size: C has n items, and A and B have n/2 • Strategy: • Determine the first item in C: it should be the smaller of the first item in A and the first in B. • Suppose it is the first item of A. Copy that to C. • Then continue merging B with “rest of A” (without the item copied to C). Repeat!
Algorithm: Merge merge(A, B, C) if (A is empty) append what’s left in B to C else if (B is empty) append what’s left in A to C else if (first item in A <= first item in B) append first item in A to C merge (rest of A, B, C) else // first item in B is smaller append first item in B to C merge (A, rest of B, C) return
Summary of Recursion Concepts • Recursion is a natural way to solve many problems • Sometimes it’s a clever way to solve a problem that is not clearly recursive • Sometimes recursion produces an efficient solution (e.g. mergesort) • Sometimes it doesn’t (e.g. fibonacci) • To use recursion or not is a design decision for your “toolbox”
Recursion: Design and Implementation • “The Rules” • Identify one or more base (simple) cases that can be solved without recursion • In your code, handle these first!!! • Determine what recursive call(s) are needed for which subproblems • Also, how to use results to solve the larger problem • Hint: At this step, don’t think about how recursive calls process smaller inputs! Just assume they work!
Exercise: Find Max and Min • Given a list of elements, find both the maximum element and the minimum element • Obvious solution: • Consider first element to be max • Consider first element to be min • Scan linearly from 2nd to last, and update if something larger then max or if something smaller than min • Class exercise: • Write a recursive function that solves this using divide and conquer. • Prototype: void maxmin (list, first, last, max, min); • Base case(s)? Subproblems? How to combine results?
What’s Next? • Recursive Data Structures • Binary trees • Representation • Recursive algorithms • Binary Search Trees • Binary Trees with constraints • Parallel Programming using Threads