- 101 Views
- Uploaded on
- Presentation posted in: General

Introduction to Recursion and Recursive Algorithms

Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.

- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -

Introduction to Recursion andRecursive Algorithms

CS2110

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 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

- 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

- We use them in grammar school e.g. what is a noun phrase?

- 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)

- Exponentiation - x raised to the y power

- 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”

- Strongly Agree
- Agree
- Disagree
- Strongly Disagree

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

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.

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 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.

- 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))

- exponentiation (x,y)

- 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

- 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

- 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

- Recursion vs. iteration
- Which to choose?
- Does it matter?

- Later: recursion as an design strategy

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.

- 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)

- 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…

- 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)

- 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(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(4), we call fib(3) and fib(2)

- 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!

- 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)

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;

}

- Divide and Conquer design strategy
- Its form
- Examples:
- Binary Search
- Merge Sort

- Time complexity issues

- 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

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;

- 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

- 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

- 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

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

}

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?)

- 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

}

}

- Can you trace MergeSort() on this list?A = {8, 3, 2, 9, 7, 1, 5, 4};

- 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”

- 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!

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

- 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”

- “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!

- Identify one or more base (simple) cases that can be solved without recursion

- 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?

- Write a recursive function that solves this using divide and conquer.

- Recursive Data Structures
- Binary trees
- Representation
- Recursive algorithms

- Binary Search Trees
- Binary Trees with constraints

- Binary trees
- Parallel Programming using Threads