Lecture
15 – More Recursion
Announcements
·
P3 due 3/15; don't need to handle bad input
file – questions?
·
H5 due Thursday at
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)