Computer Science
15-111 (Sections A & B), Spring 2007
Homework #3 (part 1)
Due: Fri
26-Jan-2007 at 10:30am (online submission).
SOLUTIONS
Place the written
portions of the following problems in the file “hw3-written.txt” in your online
submission of hw3.zip. Remember:
Show your work!
Note: The programs included here are not always written with ideal
style, and as such are not always complete solutions. They are meant to
be complete enough for you to understand everything needed for your solutions.
1. In a few words of plain English,
what does this method compute? Explain.
public static boolean foo(int[] x) {
for (int i=1; i<x.length; i++)
if ((x[i] <= 0) ||
(x[i-1] <= 0) ||
(x[i] / x[i-1] == 0))
return false;
return true;
}
The first two conditions test whether any element in x[] is negative. These will not trigger only if all the
elements are positive. Thus, for the
third condition, we assume x[i] and x[i-1] are positive. Since (x[i] / x[i-1]) uses integer division with truncation, it
will equal zero so long as (x[i] < x[i-1]).
This test is performed for every consecutive pair of elements in
x[]. Thus, the answer: This method computes whether the array x
contains positive integers
sorted from least to greatest.
Check: The following code prints
out: true false false
int[] a = { 1, 2, 3 };
int[] b = { 3, 2, 1 };
int[] c = { 3, 1, 2 };
System.out.printf("%b %b
%b\n", foo(a), foo(b), foo(c));
2. Write a program, Median.java, that
reads in a list of integers using scanner.nextInt(), ending when the sentinel
-1 is entered, and prints out the median
of the data (for a definition of median, see Wikipedia’s entry on “median”). Note that even for integral data, the median
may be a float – for example, the median of { 2, 3 } is 2.5. Note that we discussed a static method in the
Arrays class that is especially helpful here.
See Median.java.
3. Write a program,
RemoveDuplicates.java, that reads in a list of integers ending with the
sentinel -1, and then prints out the same numbers in sorted order, only with duplicates removed. For example, with user input underlined:
Enter data: 3 1 1 32 3 1 44 32 -1
Sorted data without duplicates: 1 3 32 44
Note that here, as in general, the sentinel is not considered part of the data.
See RemoveDuplicates.java
4. A “trace” of a program’s execution
shows each method call, and each value returned, nesting and indenting these
calls to indicate the call stack (thus, for example, any recursion that may
occur). For example, consider the
following method that computes the nth Fibonacci number:
public static int fib(int n) {
if (n < 2) return 1;
return fib(n-1) + fib(n-2);
}
Here is a trace of a call to fib(3), where arrows indicate return values:
fib(3)
fib(2)
fib(1)
--> 1
fib(0)
--> 1
--> 2
fib(1)
--> 1
--> 3
This trace clearly shows that fib is recursive, and for example that fib(3)
calls fib(2), which in turn calls fib(1) and fib(0), after which fib(3) calls
fib(1). This also shows that fib(1) is
called twice. Once by fib(2) and again by fib(3).
If it helps, for instructional or debugging purposes, you can instrument a
method to produce its own trace (though you must also be able to write traces
by hand!). For example, here is an
instrumented version of fib that produces precisely the trace above:
public static int fib(int n) {
return fib(n,0);
}
public static int fib(int n, int depth)
{
// output method call, indented by
recursive depth
for (int i=0; i<3*depth; i++)
System.out.print(" ");
System.out.printf("fib(%d)\n",n);
// compute the result, recursing
using the version
// that includes the depth in the
signature
int result;
if (n < 2) result = 1;
else result = fib(n-1,depth+1) +
fib(n-2,depth+1);
// output result, indented by
recursive depth
for (int i=0; i<3*depth; i++)
System.out.print(" ");
System.out.printf("-->
%d\n",result);
// and return the result
return result;
}
Notice that this uses method overloading
so that fib(int n) can still be called, although it is now just a wrapper for fib(int n, int depth) so
that we can keep track of the depth of
recursion, which we use to properly indent the output.
a. Trace a call to fib(4).
fib(4)
fib(3)
fib(2)
fib(1)
--> 1
fib(0)
--> 1
--> 2
fib(1)
--> 1
--> 3
fib(2)
fib(1)
--> 1
fib(0)
--> 1
--> 2
--> 5
b. Write a program, TracedFib1.java,
which reads in a positive integer M and prints out a table showing, for each value
N from 0 to M, the number of calls made to fib(1) while computing fib(N). For example, your program may run as follows
(where user input is underlined):
Enter an upper bound for fib(1)
tracing: 3
N
calls to fib(1) while computing fib(N)
0
0
1
1
2
1
3
2
To write this program, include a static variable fib1calls, and in your main method, repeatedly set this variable to
0, call fib(n), then print out the resulting value of fib1calls. Now, in fib
itself, test if n==1, and if so, increment fib1calls.
Here are the relevants portions:
private static int fib1calls;
public static int fib(int n) {
if (n == 1) fib1calls++;
if (n < 2) return 1;
else return fib(n-1) + fib(n-2);
}
public static void main(String[] args) {
//... //
for (int i=0; i<=max; i++) {
fib1calls = 0;
fib(i); // just ignore result
System.out.printf("%2d%11d\n",i,fib1calls);
}
}
c.
Briefly
explain why fib1calls must be a
static variable.
Its value must be computed
within fib(n), but its lifetime must extend beyond the call to fib(n). (Anything close to this explanation will
suffice.)
Actually, we could do
this without a static variable if we modified the signature of fib to take a
MutableInteger (which we’d invent), as changes to object parameters are visible
to the caller. To see this in action,
check out this code, which runs the same as the previous code, but which does
not have a static variable:
// First we
define the following (very simple) class:
class MutableInteger {
public int value;
}
// Then we rewrite the excerpted code from above as such:
public static int fib(int n, MutableInteger
fib1calls) {
if (n == 1) fib1calls.value++;
if (n < 2) return 1;
else return fib(n-1,fib1calls) + fib(n-2,fib1calls);
}
public static void main(String[] args) {
//... //
MutableInteger fib1calls = new MutableInteger();
for (int i=0; i<=max; i++) {
fib1calls.value = 0;
fib(i,fib1calls); // just ignore result
System.out.printf("%2d%11d\n",i,fib1calls.value);
}
d. Extend the table from part (b) up to
N=10.
N calls to fib(1) while computing fib(N)
0 0
1 1
2 1
3 2
4 3
5 5
6 8
7
13
8
21
9
34
10 55
e. Given this table, how many calls to
fib(1) are required, in general, when computing fib(n) recursively? (Note: Given that Fibonacci numbers grow
exponentially fast, this should help you understand why the recursive version
of fib slows down so much around n=40.)
When computing fib(n) recursively, there are fib(n-1) calls to fib(1)! Notice this is not
fib(n), according to our definition of fib(n), since fib1calls(0)=0 and
fib(0) = 1 by our definition (which is not entirely standard).
In any case, since fib(40) > 100 million, that’s a whole lot of recursive
calls to compute fib(40), especially when you consider that the iterative version
performs 40 simple arithmetic calculations to compute the same value!
5. Memoization
(a kind of dynamic programming) is a
technique which uses caching in order to combine the benefits of the clarity of
recursive solutions with the speed of iterative solutions. Write a program, MemoizedFib.java (note that
there is no “r” in Memoized), that uses memoization to dramatically speed up
our recursive fibonacci method. The
basic idea is to start with the recursive solution, and to store the results of
calls to fib(i), so that repeated calls just use the stored values rather than
recursively recomputing them. To do
this, create an array of int’s, fibValues. For this problem, you can fix the maximum
value of N to 99, and so you can set the size of fibValues to 100. You should
also have a second array, isComputed,
containing boolean values such that isComputed[n] is true if and only if
fibValues[n] holds the value of fib(n) (hence, initially isComputed[n] should
be false for all values of n, which conveniently is the default when you create
an array of booleans). Now, on a call to
fib(n), first check if isComputed[n] is true.
If so, just return fibValues[n].
Otherwise, compute fib(n) recursively (still using memoization in your
recursive calls!), and then set fibValues[n] to the result and isComputed[n] to
true before returning the result. Write
some test code that verifies that your memoized fib computes the same values as
your recursive fib. Notice, though, that
the runtimes of memoized fib are basically the same as iterative fib.
Here is the relevant code:
private static int[] fibValues = new int[100];
private static boolean[] isComputed = new boolean[100];
public static int memoizedFib(int n) {
if (isComputed[n]) return fibValues[n];
int result;
if (n < 2) result= 1;
else result = memoizedFib(n-1) +
memoizedFib(n-2);
fibValues[n] = result;
isComputed[n] = true;
return result;
}
public static void main(String[] args) {
// This prints the table lickety-split!
for (int i=0;i<40; i++)
System.out.printf("%d%10d\n",i,memoizedFib(i));
// This is silent, so the memoized
method works!
for (int i=0;i<40; i++)
if (fib(i) != memoizedFib(i))
System.out.printf("memoizedFib(%d)
!= fib(%d)\n",i,
}
6. Consider the following method:
public static int foo(int x, int y) {
if ((x < 1) || (y < 1)) return
0;
else return foo(x-1,y-1)+x+y-1;
}
a. Trace a call to foo(8,3)
foo(8,3)
foo(7,2)
foo(6,1)
foo(5,0)
--> 0
--> 6
--> 14
--> 24
While you should generate the trace by hand, you should also be able to
instrument foo() so that it does this for you, as such:
public static
int foo(int x, int y, int depth) {
for (int i=0; i<3*depth; i++)
System.out.print(" ");
System.out.printf("foo(%d,%d)\n",x,y);
int result;
if ((x < 1) || (y < 1)) result =
0;
else result =
foo(x-1,y-1,depth+1)+x+y-1;
for (int i=0; i<3*depth; i++)
System.out.print(" ");
System.out.printf("-->
%d\n",result);
return result;
}
public static void main(String[] args) { foo(8,3,0); }
b. Trace a call to foo(8,4)
foo(8,4)
foo(7,3)
foo(6,2)
foo(5,1)
foo(4,0)
--> 0
--> 5
--> 12
--> 21
--> 32
c.
In
just a few words of English, assuming its arguments are both non-negative, what
does this method compute? (Hint: it is a common arithmetic expression).
For non-negative x and y, foo(x,y)
= x*y. It computes the product.
d.
Explain
why your answer to part (c) is correct.
There are multiple ways to do this. For
full credit, though, you cannot just “handwave”, but must have a mathematically
sound explanation. Of the various
approaches, here is my preference, as it explains the logic of the recursive
case clearly:
First, we multiply (x – 1)(y – 1):
(x – 1)(y – 1) = xy – x – y + 1
Next, we solve this expression for xy:
xy = (x – 1)(y – 1) + x + y – 1
Notice that this is exactly the logic of the recursive case in the code
above! Not a coincidence at all!!
Since our base case does compute the product xy when (x == 0) or (y == 0), and
since our recursive case computes the product xy assuming f(x-1,y-1) computes (x-1)(y-1), we conclude f(x,y)=xy for
all cases.
Now, for full rigor, we’d need to do a bona fide mathematical induction proof
at this point (which would be very close to the above explanation, but with a
little more rigor), but we’ll settle on this explanation.