Lecture 15 – More Recursion

 

Announcements

·       P3 due 3/15; don't need to handle bad input file – questions?

·       H5 due Thursday at midnight 

 

Today: 

·       practice steps for writing recursive programs

base case, assume it works for smaller problem,

    combine the "pieces"

·       auxiliary recursive methods, more than one recursive call

·       analyzing the running time of recursive programs

 

Example:  StringToLinkedList  (from exam)

given:  a String of length N

goal: create and return a linked list of length N with one letter in each node (no header node)

"HELLO"  Þ 

L

 
 

 

 

 


public static ListNode StringToLinkedList( String s ){

to use recursion:

·       assume can use recursive call on smaller string to get partial solution  -- what size should the smaller string be?

(length N-1, N/2, … ?)  answer:  String of length N-1

(all but the 1st character), to get linked list of length N-1

StringToLinkedList("ELLO") Þ

 

 

 


  Listnode tmp = StringToLinkedList (s.substring(1));

            //note:  s.substring(1) = "" if s="a"

·       put one node on the front with 1st letter for full solution:

  Listnode L = new ListNode (S.substring(0,1),tmp);

  return L; 

·       must write base case:

  if (s.length()==0) return null;

or maybe:

  if (s.length()==1) return new ListNode(s);

is there any difference between these two base cases?

1st case will work if the method is called with an empty string;  - thus, 1st case is a more robust solution

note:  might also want to handle the case where s == null

          e.g.,  return null or throw exception

How does recursion really work?

·       at runtime, a stack of activation records (ARs) is maintained – one AR for each active method

       active method:  has been called, has not yet returned

·       each AR includes:

o      the method's parameters

o      the method's local variables

o      the return address

i.e., where (in the code of the calling method) to execute when this method returns

(calling methods next instruction after the call)

·       when method is called, its AR is pushed on the AR stack

·       when the method returns, its ARpopped; and the return address in the popped AR gives the instruction to execute next

Example (no recursion)                           Act out AR Stack

1.      void printChar(char c) {            (3 whiteboards)

2.      System.out.println(c);

3.    }

4.    void main(…) {

5.   

c:    ‘a’

ret addr:  7

 
  char ch = 'a';

6.      printChar(ch);

7.   

ch:    ‘a’

ret addr:  --

 
  ch = 'b';

8.      printChar(ch);

9.      System.out.println("done");

10.                       }

Output:

a

b

Example (with recursion)                      Act out printInt(2);

1.      void printInt(int k) {

2.      if (k <= 0) return;

3.      System.out.println(k);

4.      printInt(k-1);

5.    }

k:  0

ret addr:  5

 
simulate printInt(2):

Output:

2

k:  1

ret addr:  5

 

k:  2

ret addr:  --

 
1

 

More examples

·       sometimes you need an "auxiliary" method

1.      to do something just once

(at beginning or end of all recursive calls)

2.    to pass more parameters

 

Example of 1:  print a linked list backwards,

with a "header" & ending with a newline

public void printBackwards(ListNode L) {

  System.out.println("The list backwards is: "):

  printList(L);  //not recursive call

  System.out.println();

}

 

private void printList(ListNode L) { // aux. method

  if (L == null) return;

  printList(L.getNext());  // recursive call

  System.out.print (L.getData());

}

(printList is the code from last lecture)

Another Example of 1:  write StringToLinkedList,

but throw an exception if S is null or empty (initially)

You try

think about doing it with an auxiliary method

think about doing it without an auxiliary method

is the auxiliary method better?

 

solution with auxiliary method:

  public static ListNode StringToLinkedList(String S){

    if ( S==null || S.equals(""))

      throw new …

    return StoLLAux(S);

  }

  private static StoLLAux(String s) {

    if (S.length() == 1) return new ListNode(s);

    return (new ListNode(s.substring(0,1),

                          StoLLAux(s.substring(1));

  }

 

can we do the recursion in one method? – yes!

is the auxiliary method better?

answer: because the test for null or "" is only done once

Example of 2:  auxiliary method to pass in parameters

 

determine whether there is a path in the array A

 

public static boolean solveMaze(String[][] A){

  // could test:  if (A == null) throw new …;

  return isPath(A,0,0);

}

 

private static int isPath(int[][]A, int x int y){

  // code to test for path from x,y to right corner

 

}

Sometimes have more than one recursive call

Example 1:  Fibonacci number

definition: 

f(1) = 1

                f(2) = 1

                f(n) = f(n-1) + f(n-2)    for n>2

recursive code:

  public int fib(int n){

    if (n==1 || n==2) return 1;

    return (fib(n-1) + fib(n-2));

  }

 

compiler will translate line 3 to something like:

        t1 = fib(n-1)

    t2 = fib(n-2)

    t3 = t1 + t2

        return(t3)

·       note:  can't count on ordering of 1st and 2nd statement

[doesn't matter in this case, but if you have m1() + m2(),

        don't assume that m1() is called before m2()]

 

simulate fib(4):

                                                fib(4)

                                               +

                                fib(3)                   fib(2)

                                 +

                   fib(2)             fib(1)

·       note:  we are re-doing work (call fib(2) twice)

·       recursive method is simple, but very inefficient

Example 2:

find a forward path from (0,0) to (M-1,N-1) in an M´N array

 

public static boolean isPath(String[][]A, int x, int y) {

   if ( x<0 || y<0  // safety check for x,y

         || x >= A.length     // base case

         || y >= A[0].length) return false;

   // operations on the "current piece" – position x,y

   if (A[x][y].equals("x")

                      // efficiency insert:||A[x][y].equals("v")

        ) return false;

   if (x==A.length-1 && y==A[0].length-1) return true;

                      // efficiency insert: A[x][y] = "v";

   // recursive calls

   return (isPath(A,x+1,y) || isPath(A,x,y+1));

}

 

 

x

 

 

 

 

x

x

 

 

 

 

simulate isPath(A,0,0) for A=  

 

 

                                          (A,0,0)

                       (A,0,1)             ||              (A,1,0)

                                                    (A,2,0)    || 

                                           (A,3,0)  ||  (A,2,1)

                                                      (A,3,1) || (A,2,2)

                                                              (A,3,2) || (A,2,3)

(need to simulate for other inputs too)              

Analyzing efficiency/time complexity for recursive methods

 

informal method:

1.      how many total times will the method be called

2.    how much work is done for each call

(excluding the recursive call)

        multiply 1 ´ 2

 

example:

printInt(int k){

  if (k<=0) return;

  System.out.println(k);

  printInt(k-1);

}

 

·       printInt is called (k+1) times

·       each time, constant number of operations

(excluding recursive call)

so, total time is O(k)

 

formal method:  write & solve a recurrence equation

e.g., for printInt:

time(0) = 1

time(k) = 3 + time(k-1)

 

solution:  look at time(1), time(2), time(3)… & find pattern 

     Time(k) = 3k + 1

        i.e.,  3k + 1  =  3 + [3(k-1) + 1]

 

You try:  time complexity of fib(n)

        try informal & formal analysis

 

   if (n==1 || n==2) return 1;

   return (fib(n-1) + fib(n-2);

 

T(1) = T(2) = 1

T(n) = 1 + T(n-1) + T(n-2)

 

Note:  T(n-1) ³ T(n-2)   so T(n) ³ 1 + 2 ´T(n-2)

        n            T(n)

       1,2             1

       3,4             3

       5,6            7

       7,8            15

       9,10          31           Note:  T(n) = 1 + 2 ´T(n-2)

 

guess:  T(n) = 2n/2 – 1  = O(2n)       exponential time!

 

verify:    2n/2 – 1 =  1 + 2 ´[2(n-2)/2 – 1]

 

(better to use an iterative solution, calculate T(n-1) only once)

 

 

You try:  time complexity of isPath(A,x,y)

        try informal analysis, for array of size m´n

        assume added statements to mark a visited node with "v"

solution:

informal

·       number of recursive calls for array of size m´n:  c´m´n

·       each call:  work is constant (excluding recursive calls)

so total work is O(mn)

 

 

Summary

·       use recursion for clarity, not for space/time savings

·       always include a base case

·       make progress toward the base case for any input values

·       execution starts up again just after the recursive call

·       analyze time efficiency via informal reasoning or recurrence relations

 

 

(one minute paper)