[Instructions] [Search] [Current] [Syllabus] [Links] [Handouts] [Outlines] [Labs] [More Labs] [Assignments] [Quizzes] [Examples] [Book] [Tutorial] [API]

One of the reasons that computer scientists developed a formal description of Big-O is that it permits us to develop and apply a number of important identities. For example, we might wish to consider whether BigO is transitive. That is, if f(N) is in O(g(n)) and g(n) is in O(h(n)), can we say that f(N) is in O(h(n))? The answer is yes. Here's a proof.

- Because f(N) is in O(g(N)), there exist N
_{1}and D_{1}such that for all N > N_{1}, |f(N)| <= |D_{1}*g(N)| - Because g(N) is in O(h(N)), there exist N
_{2}and D_{2}such that for all N > N_{2}, |g(N)| <= |D_{2}*h(N)| - Let N
_{3}= max(N_{1}, N_{2}) - Let D
_{3}= max(1,D_{1}) * max(1,D_{2}) - Then for all N > N
_{3}, |f(N)| <= |D_{1}*g(N)| [*By the first rule and the definition of N*.]_{3} - Then for all N > N
_{3}, |g(N)| <= |D_{2}*h(N)| [*By the second rule and the definition of N*.]_{3} - Then for all N > N
_{3}, |D_{1}*g(N)| <= |D_{1}*D_{2}*h(N)| [*Since D*.]_{1}> 0, we can safely multiply both sides of an inequality for D_{1}without affecting the inequality - Then for all N > N
_{3}, |f(N)| <= |D_{1}*D_{2}*h(N)| [*We can plug together various inequalities using transitivity of inequality.*] - Then for all N > N
_{3}, |f(N)| <= |D_{3}*h(N)| [*D*.]_{3}>= D_{1}*D_{2} - Hence f(N) is in O(h(N))
[
*By the definition of Big-O*.]

In this problem, you will consider two such proofs.

As I mentioned earlier, this problem was not graded because a number of you had significant difficulties with proofs and proof techniques. Instead, we spent some time discussing it in class.

We've said that Big-O notation allows us to discard lower-order terms. Let's try to formalize that idea. Prove that if f(N) is in O(g(N)+h(N)), and g(N) is in O(h(N)), then f(N) is in O(h(N)).

- If f(N) is in O(g(N) + h(N)), then there exist
N
_{1}and D_{1}> 0 such that for all N greater than N_{1}, |f(N)| <= |D_{1}*(g(N) + h(N))|. [*By the definition of Big-O*.] - If g(N) is in O(h(N)), then there exist
N
_{2}and D_{2}> 0 such that for all N greater than N_{2}, |g(N)| <= |D_{2}*h(N)| - Let N
_{3}= max(N_{1},N_{2}). - Let D
_{3}= D_{1}* (1 + D_{2}). - For all N > N
_{3}, |f(N)| <= |D_{1}*(g(N) + h(N))|. [*By step 1 and definition of N*]_{3}. - For all N > N
_{3}, |f(N)| <= D_{1}*|g(N)+h(N)|. [*If D>0, then |D*X| = |D|*|X|*.] - For all N > N
_{3}, |f(N)| <= D_{1}*|g(N)| + D_{1}*|h(N)|. [*|X+Y| <= |X|*|Y|*.] - For all N > N
_{3}, |f(N)| <= D_{1}*|D_{2}*h(N)| + D_{1}*|h(N)|. [*By step 2 and definition of N*]_{3}. - For all N > N
_{3}, |f(N)| <= |(D_{1}*D_{2}+D_{1})*h(N)|. [*Various arithmetical manipulations; D*.]_{1}> 0 - For all N > N
_{3}, |f(N)| <= |D_{3}*h(N)|. [*Definition of D*.]_{3} - f(N) is in O(h(N)).
[
*Definition of Big-O*.]

We've said that Big-O notation allows us to discard constant multipliers. Let's try to formalize that idea. Prove that if f(N) is in O(C*g(N)) and C != 0 then f(N) is in O(g(N)).

- If f(N) is in O(C*g(N)), then there exist
N
_{1}and D_{1}> 0 such that for all N greater than N_{1}, |f(N)| <= |D_{1}*C*g(N)|. [*By the definition of Big-O*.] - Let N
_{2}= N_{1}. - Let D
_{2}= |D_{1}*C|. - For all N > N
_{2}, |f(N)| <= |D_{1}*C*g(N)|. [*By step 1 and definition of N*.]_{2} - For all N > N
_{2}, |f(N)| <= |D_{2}*g(N)|. [*By definition of D*.]_{2} - f(N) is in O(g(N)).
[
*By definition of Big-O*.]

Note that we had to make sure that D_{2} was greater than 0, and
did so by making it the absolute value of D_{1}*C.

We've done some exploring of the Fibonacci sequence, 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ..., which might be defined by the following Java function.

/** * Compute the nth Fibonacci number. * Pre: n >= 0 * Pre: the nth Fibonacci number <= Long.MAX_VALUE * Post: returns the nth Fibonacci number */publiclongfib(longn) {if(n <= 1)returnn;elsereturnfib(n-1) + fib(n-2); } // fib(long)

Compute upper and lower bounds on the running time of the definition of
`fib`

given above. Express the running time in Big-O notation,
in terms of the
number of calls to `fib`

in order to compute `fib(n)`

.
The bounds should be as tight as you can
get them (preferably, both will be the same).

Hint: you can assume that the number of steps to compute fib(n) is at least as many as the number to compute fib(n-1).

Let f(n) be ``the number of steps to compute the nth Fibonacci number using this algorithm''. We'll compute both upper bounds and lower bounds on f(n). We know that for n > 1 f(n) = f(n-1) + f(n-2) + c, for some constant c.

We begin with an upper bound. Since f(n-1) > f(n-2) and c is expected to be small, we can say that f(n) <= 2*f(n-1).

That means that f(n) <= 2*2*f(n-2) and f(n) <= 2*2*2*f(n-3).

More generally, f(n) <= 2^{k}*f(n-k). When k is (n-1),
we have f(n) <= 2^{n-1}*f(1). Since it takes about one
step to compute f(1), we can say that f(n) <= 2^{n-1}.
Hence, f(n) is in O(2^{n}).

Now let's turn to the lower bound. Since f(n-2) < f(n-1),
and c is expected to be small, we can say that f(n) >= 2*f(n-2).
Using a similar analyses to the one above, we get that
f(n) is in O(2^{n/2}).

Both lower bound and upper bound are exponential. We can therefore say that f(n) is an exponential algorithm.

Improve the computation of the nth Fibonacci number using the technique of dynamic programming (caching previously computed results in a table). Test your revised function appropriately.

There are a number of ways to solve this problem. I expect that you saw some of them in CS151. We can do a straightforward recursive computation using a table. We can do this iteratively, counting up from 0 or 1, and keeping track of the last two values. We can do this recursively, also counting up from 0 or 1 to n. The question asked for the first solution, but I accepted all three. I did not grade you on testing (other than verifying that you did some testing).

Note that while I did not explicitly ask you to do this recursively, question d implies that you should have seriously considered using recursion of some sort. Note also that while loops are not recursive. If you told me that you were doing recursion, and had no recursive calls, then I took off some points.

Note also that I asked you to use dynamic programming, which does suggest that you should use a table. I didn't take off for this.

Here's a solution class,
`Fibonacci.java`

,
that I wrote to illustrate all three
solutions. Your own answer may vary.

importSimpleOutput;/** * A number of implementations of functions to compute the nth * Fibonacci number. Intended as part of an answer key for * HW6 of CS152 99S. * * This can be used as a utility class (in which case it is recommended * that you use fibdp), or it can serve as a tester for the various * Fibonacci methods. In the latter case, the command line syntax is: * % java Fiboncci i1 i2 ... in * (which computes the i1th, i2th, ... inth Fibonacci number using * each technique, and reports on the time). * * @author Samuel A. Rebelsky * @version 1.0 of March 1999 */publicclassFibonacci {// +--------+--------------------------------------------------// | Fields |// +--------+/** * An array of computed Fibonacci numbers. Used for the * dynamic programming version of the function. Eventually, * FIB[n] is the nth Fibonacci number. When initialized, * FIB[n] is 0. */protectedlongFIB[];// +---------+-------------------------------------------------// | Methods |// +---------+/** * Compute the nth Fibonacci number using the naive recursive * technique. * Pre: n >= 0 * Pre: the nth Fibonacci number >= Long.MAX_VALUE * Post: returns the nth Fibonacci number */publiclongfib(longn) {// Base case: the 0th Fibonacci number is 0, the 1st// Fibonacci number is 1.if(n <= 1)returnn;// Recursive case: the nth Fibonacci number is// the n-1st Fibonacci number + the n-2nd Fibonacci number.returnfib(n-1) + fib(n-2); } // fib(long)/** * Compute the nth Fibonacci number using the naive recursive * technique, supplemented by dynamic programming. * Pre: n >= 0 * Pre: the nth Fibonacci number >= Long.MAX_VALUE * Post: returns the nth Fibonacci number */publiclongfibdp(longn) {// Base case: the 0th Fibonacci number is 0, the 1st// Fibonacci number is 1.if(n <= 1)returnn;// Is there a table? If not, make one. Takes advantage// of the fact that Java uses 0 as the default value for// longs (so we'll assume that any 0 entry is not yet// filled in).if(FIB ==null) FIB =newlong[(int) n+1];// Is the table big enough? Needs to be checked in case// there are independent calls.if(FIB.length <= n) {// Remember the old table.long[] tmp = FIB;// Build a new table.FIB =newlong[(int) n+1];// Copy over the elements.for(inti = 0; i < tmp.length; ++i) { FIB[i] = tmp[i]; } // for } // if the table isn't big enough// Does the table have the entry? A nonzero entry means// that we've already computed the nth Fibonacci number.if(FIB[(int) n] != 0)returnFIB[(int) n];// If we've gotten this far, we need to compute the nth// Fibonacci number.FIB[(int) n] = fibdp(n-1) + fibdp(n-2);// And now that we've computed it, we can return it.returnFIB[(int) n]; } // fibdp(long)/** * Compute the nth Fibonacci number using an iterative * technique. We keep track of the ith and i-1st Fibonacci * numbers, use them to generate the i+1st Fibonacci number, * and then update i. * Pre: n >= 0 * Pre: the nth Fibonacci number >= Long.MAX_VALUE * Post: returns the nth Fibonacci number */publiclongfibit(longn) {inti;// Counter variableintithfib;// The ith Fibonacci numberintprevfib;// The previous Fibonacci numberintnextfib;// The next Fibonacci number// The technique doesn't work for n = 0, so use the// default answer for that particular case.if(n == 0)return0;// Initialize everything.i = 1; ithfib = 1; prevfib = 0;// Keep going until we reach n.while(i < n) { nextfib = ithfib + prevfib;// Move on to the next i.i = i + 1;// The old ith Fibonacci number is now the previous// Fibonacci number.prevfib = ithfib;// The old next Fibonacci number is now the new ith// Fibonacci number.ithfib = nextfib; } // while// i is now n, so the ith Fibonacci number is the nth Fibonacci// number.returnithfib; } // fibit(long)/** * Compute the nth Fibonacci number using a recursive algorithm * similar to the one used in fibit. * Pre: n >= 0 * Pre: the nth Fibonacci number >= Long.MAX_VALUE * Post: returns the nth Fibonacci number */publiclongfibrec(longn) {if(n == 0)return0;elsereturnfibrec(n, 1, 1, 0); } // fibrec/** * Compute the nth Fibonacci number given the ith and i-1st * Fibonacci numbers. * Pre: n >= 1 * Pre: the nth Fibonacci number >= Long.MAX_VALUE * Post: returns the nth Fibonacci number */publiclongfibrec(longn,longi,longithfib,longprevfib) {// Base case: i is n, so the ith Fibonacci number is the// nth Fibonacci number.if(i == n)returnithfib;// Recursive caseelsereturnfibrec(n, i+1, ithfib+prevfib, ithfib); } // fibrec(long,long,long,long)// +------+----------------------------------------------------// | Main |// +------+/** * Get some values of N and compute. */publicstaticvoidmain(String[] args) {// Create an array of inputs.long[] vals =newlong[args.length];// Create an array of results.long[] results =newlong[args.length];// Two time values, used to determine how long the various// computations take.longstart;longstop;// The wonderful thing that does all the computation.Fibonacci computer =newFibonacci();// And something for output.SimpleOutput out =newSimpleOutput();// Fill in the values. Use 0 if the user gives a bad// value. This is intended for testing only, so the// user interface is primitive at best.for(inti = 0; i < args.length; ++i) { try { vals[i] = Long.parseLong(args[i]); }catch(Exception e) { } } // for// "Prime" the various methods (so that there's not overhead// in loading their code, optimizing their code, or whatever// else the interpeter decides to do).computer.fib(3); computer.fibdp(3); computer.fibit(3); computer.fibrec(3);// Determine how long the basic method takes.start = System.currentTimeMillis();for(inti = 0; i < vals.length; ++i) { results[i] = computer.fib(vals[i]); } // for stop = System.currentTimeMillis();// Reportout.println("Naive recursive method:");for(inti = 0; i < vals.length; ++i) { out.println("fib(" + vals[i] + ") =" + results[i]); } // for out.println("COMPUTATION TOOK" + (stop-start) + "MILLISECONDS");// Determine how long the dynamic processing technique takesstart = System.currentTimeMillis();for(inti = 0; i < vals.length; ++i) { results[i] = computer.fibdp(vals[i]); } // for stop = System.currentTimeMillis();// Reportout.println("Dynamic programming method:");for(inti = 0; i < vals.length; ++i) { out.println("fibdp(" + vals[i] + ") =" + results[i]); } // for out.println("COMPUTATION TOOK" + (stop-start) + "MILLISECONDS");// Determine how long the iterative technique takesstart = System.currentTimeMillis();for(inti = 0; i < vals.length; ++i) { results[i] = computer.fibit(vals[i]); } // for stop = System.currentTimeMillis();// Reportout.println("Iterative method:");for(inti = 0; i < vals.length; ++i) { out.println("fibit(" + vals[i] + ") =" + results[i]); } // for out.println("COMPUTATION TOOK" + (stop-start) + "MILLISECONDS");// Determine how long the other recursive technique takesstart = System.currentTimeMillis();for(inti = 0; i < vals.length; ++i) { results[i] = computer.fibrec(vals[i]); } // for stop = System.currentTimeMillis();// Reportout.println("Other recursive method:");for(inti = 0; i < vals.length; ++i) { out.println("fibrec(" + vals[i] + ") =" + results[i]); } // for out.println("COMPUTATION TOOK" + (stop-start) + "MILLISECONDS"); } } // class Fibonacci

Unfortunately, it's not clear that there's a big difference between the three running times of the efficient methods. Consider the following:

ji Fibonacci 30 Naive recursive method: fib(30) = 832040 COMPUTATION TOOK 20276 MILLISECONDS Dynamic programming method: fibdp(30) = 832040 COMPUTATION TOOK 1 MILLISECONDS Iterative method: fibit(30) = 832040 COMPUTATION TOOK 0 MILLISECONDS Other recursive method: fibrec(30) = 832040 COMPUTATION TOOK 0 MILLISECONDS

Here's,
`FibTimer.java`

,
an attempt to get closer timing, by doing more repetitions.

importSimpleOutput;importFibonacci;/** * An attempt to compare the more efficient techniques for * computing Fibonacci numbers. Intended as part of an answer * key for HW6 of CS152 99S. * * @author Samuel A. Rebelsky * @version 1.0 of March 1999 */publicclassFibTimer {// +------+----------------------------------------------------// | Main |// +------+/** * Get some values of N and compute. */publicstaticvoidmain(String[] args) {// Create an array of inputs.long[] vals =newlong[args.length];// Create an array of results.long[] results =newlong[args.length];// Two time values, used to determine how long the various// computations take.longstart;longstop;// The wonderful thing that does all the computation.Fibonacci computer =newFibonacci();// And something for output.SimpleOutput out =newSimpleOutput();// It turns out to be best to repeat computation if we're// going to get any useful results (since Fibonacci numbers// get too large too fast, there's not a clear way to// distinguish them by large inputs). Of course, this may// give an unfair bias to the dynamic programming method.intREPETITIONS = 1000;// Fill in the values. Use 0 if the user gives a bad// value. This is intended for testing only, so the// user interface is primitive at best.for(inti = 0; i < args.length; ++i) { try { vals[i] = Long.parseLong(args[i]); }catch(Exception e) { } } // for// Determine how long the dynamic processing technique takesstart = System.currentTimeMillis();for(intj = 0; j < REPETITIONS; ++j) {for(inti = 0; i < vals.length; ++i) { results[i] = computer.fibdp(vals[i]); } // for } stop = System.currentTimeMillis();// Reportout.println("Dynamic programming method:");for(inti = 0; i < vals.length; ++i) { out.println("fibdp(" + vals[i] + ") =" + results[i]); } // for out.println("COMPUTATION TOOK" + (stop-start) + "MILLISECONDS");// Determine how long the iterative technique takesstart = System.currentTimeMillis();for(intj = 0; j < REPETITIONS; ++j) {for(inti = 0; i < vals.length; ++i) { results[i] = computer.fibit(vals[i]); } // for } // for stop = System.currentTimeMillis();// Reportout.println("Iterative method:");for(inti = 0; i < vals.length; ++i) { out.println("fibit(" + vals[i] + ") =" + results[i]); } // for out.println("COMPUTATION TOOK" + (stop-start) + "MILLISECONDS");// Determine how long the other recursive technique takesstart = System.currentTimeMillis();for(intj = 0; j < REPETITIONS; ++j) {for(inti = 0; i < vals.length; ++i) { results[i] = computer.fibrec(vals[i]); } // for } stop = System.currentTimeMillis();// Reportout.println("Other recursive method:");for(inti = 0; i < vals.length; ++i) { out.println("fibrec(" + vals[i] + ") =" + results[i]); } // for out.println("COMPUTATION TOOK" + (stop-start) + "MILLISECONDS"); } } // class FibTimer

And some results

% ji FibTimer 10 Dynamic programming method: fibdp(10) = 55 COMPUTATION TOOK 30 MILLISECONDS Iterative method: fibit(10) = 55 COMPUTATION TOOK 17 MILLISECONDS Other recursive method: fibrec(10) = 55 COMPUTATION TOOK 103 MILLISECONDS

Some more results.

% ji FibTimer 20 Dynamic programming method: fibdp(20) = 6765 COMPUTATION TOOK 30 MILLISECONDS Iterative method: fibit(20) = 6765 COMPUTATION TOOK 30 MILLISECONDS Other recursive method: fibrec(20) = 6765 COMPUTATION TOOK 188 MILLISECONDS

Some even more interesting results.

% ji FibTimer 50 Dynamic programming method: fibdp(50) = 12586269025 COMPUTATION TOOK 32 MILLISECONDS Iterative method: fibit(50) = -298632863 COMPUTATION TOOK 27 MILLISECONDS Other recursive method: fibrec(50) = 12586269025 COMPUTATION TOOK 457 MILLISECONDS

Compute an upper bound on the running time for your new method of computing
the Fibonacci numbers. Express the running time in Big-O notation,
in terms of the
number of calls to `fib`

in order to compute `fib(n)`

.
The bound should be as tight as you can
get it.

The iterative method and the recursive method based on that method are clearly O(n), since they both count from 1 up to n.

The dynamic programming method is also O(n). Why? Because each value in FIB gets filled in once. While the filling it seems to require a double recursive call, the second recursive call will take constant time (since the table will then be filled in).

Rewrite your efficient Fibonacci computer iteratively (using loops rather than recursion).

I didn't bother to test this one.

/** * Compute the ith Fibonacci number iteratively, using a * table to store results. */publiclongfibitdp(longn) {if(n == 0)return0;long[] fibs =newlong[n+1]; fibs[0] = 0; fibs[1] = 1;for(inti = 2; i &;lt= n; ++i) { fibs[i] = fibs[i-1] + fibs[i-2]; }returnfibs[i]; } // fibitdp(long)

Theodore and Themla Trinary enjoyed the use of binary search so much that they've decided to develop their own variant, based on dividing the subarray into three parts, rather than two. Here's their algorithm.

/** * Determine the index of x in array A. * Pre: A is sorted in increasing order. * Post: If x is in A, then returns i s.t. A[i] == x * Post: If x is not in A, then throws an exception */publicinttrinarySearch(intx,int[] A)throwsException {// Use the marvelous helper functionreturntrinarySearch(x, A, 0, A.length-1); } // trinarySearch(int, int[])/** * Determine the index of x in the subarray of A given by lb..ub. * Pre: A is sorted in increasing order. * Pre: If x is in A, then x is in the subarray. * Pre: If x is not in A, then x is not in the subarray. * Post: If x is in A, then returns i s.t. A[i] == x * Post: If x is not in A, then throws an exception */publicinttrinarySearch(intx,int[] A,intlb,intub)throwsException {// Base case: empty subarray. x is not in A.if(ub < ub)thrownewException("Not found");// Base case: single-element subarray. See if x is that element.elseif(lb == ub) {if(x == A[lb])returnlb;elsethrownewException("Not found"); } // single-element subarray// Recursive cases: split the array and search the appropriate subarray.else{// The first split point is one-third of the way from lb to ub. We// compute the distance from lb to ub, take 1/3 of that, and add it// to lb.intsplitOne = lb + 1/3 * (ub-lb);// The second split point is two-thirds of the way from lb to ub. We// compute the distance from lb to ub, take 2/3 of that, and add it// to lb.intsplitTwo = lb + 2/3 * (ub-lb);// Recursive case: in the first third of the array.if(x <= splitOne)returntrinarySearch(x, A, lb, splitOne);// Recursive case: in the second third of the array.elseif(x < splitTwo)returntrinarySearch(x, A, splitOne+1, splitTwo-1);// Recursive case: in the third third of the array.elsereturntrinarySearch(x, A, splitTwo, ub); } } // trinarySearch(int, int[], int, int)

Unfortunately, their code is riddled with errors. Identify and correct the errors, both syntactic and semantic. If you use a test suite to identify the errors, please include the test suite.

Here are the problems that I identified (and presumably that I intended).

Both `1/3`

and `2/3`

are 0, since that's
integer division. A better solution
is to use `(ub-lb)/3`

and `(2*(ub-lb))/3`

,
respectively. You could also
use

s and then convert back to __float__

s,
but I think that's more complicated.
__int__

In both cases, we're comparing to the split locations, rather than the
values at those locations. For example, we should use
`x <= A[splitOne]`

rather than
`x <= splitOne`

.

We need to be assured that the size of the subarray shrinks each
time, which requires a close look at the recursive call. First,
we think about the relative values and note that
`lb <= splitOne <= splitTwo <= ub`

. In
addition, if `lb < ub`

, then we have that
`lb <= splitOne <= splitTwo < ub`

.

- I note that
the first recursive call continues to use
`lb`

as the lower bound, so we should make sure that the upper bound changes. if`lb<ub`

then`splitOne`

must be less than`ub`

(by the rules of integer division). What if they're the same? Then we use the single-element base-case, and we're done. This one is okay. (*Hmmm ... I thought I'd intended this to be an error. Oh well.*) - I note that the second recursive call uses
`splitOne+1`

as the lower bound and`splitTwo-1`

as the upper bound. Is that reasonable? What if`ub = lb+1`

? Then both`splitOne`

and`splitTwo`

will be equal, and we'll get an error. - For the third recursive call we use
`splitTwo`

and`ub`

. As we've just noted,`splitTwo`

can equal`lb`

, so this won't shrink the array, and we may run forever.

To solve this pair of problems, I'll be a little bit more careful on how I divide up the array. The middle third will include the second splitter, which means that I'll have to be careful in my comparisons.

The base case needs to be `(ub < lb)`

rather than
`(ub < ub)`

.

Here's my class,
`TrinarySearch.java`

,
which includes both function and tester. Note that rather than somewhat
arbitrary testing, I've tried relatively comprehensive testing. Read
the comments for and body of `main`

for more information.

importSimpleOutput;/** * Trinary search. A strange variant of binary search. Intended * as an example for the answer key for HW6 of CS152 99S. * * @author Samuel A. Rebelsky * @version 1.0 of March 1999 */publicclassTrinarySearch {// +---------+-------------------------------------------------// | Methods |// +---------+/** * Determine the index of x in array A. * Pre: A is sorted in increasing order. * Post: If x is in A, then returns i s.t. A[i] == x * Post: If x is not in A, then throws an exception */publicinttrinarySearch(intx,int[] A)throwsException {// Use the marvelous helper functionreturntrinarySearch(x, A, 0, A.length-1); } // trinarySearch(int, int[])/** * Determine the index of x in the subarray of A given by lb..ub. * Pre: A is sorted in increasing order. * Pre: If x is in A, then x is in the subarray. * Pre: If x is not in A, then x is not in the subarray. * Post: If x is in A, then returns i s.t. A[i] == x * Post: If x is not in A, then throws an exception */publicinttrinarySearch(intx,int[] A,intlb,intub)throwsException {// Base case: empty subarray. x is not in A.if(ub < lb)thrownewException("Not found");// Base case: single-element subarray. See if x is that element.elseif(lb == ub) {if(x == A[lb])returnlb;elsethrownewException("Not found"); } // single-element subarray// Recursive cases: split the array and search the appropriate subarray.else{// The first split point is one-third of the way from lb to ub. We// compute the distance from lb to ub, take 1/3 of that, and add it// to lb.intsplitOne = lb + (ub-lb)/3;// The second split point is two-thirds of the way from lb to ub. We// compute the distance from lb to ub, take 2/3 of that, and add it// to lb.intsplitTwo = lb + (2 * (ub-lb))/3;// Recursive case: in the first third of the array.if(x <= A[splitOne])returntrinarySearch(x, A, lb, splitOne);// Recursive case: in the second third of the array.elseif(x <= A[splitTwo])returntrinarySearch(x, A, splitOne+1, splitTwo);// Recursive case: in the third third of the array.elsereturntrinarySearch(x, A, splitTwo+1, ub); } } // trinarySearch(int, int[], int, int)// +------+----------------------------------------------------// | Main |// +------+/** * Test the search method. Technique: build a number of sorted * arrays of different lengths (note that content should not * affect our search method, so we can use any sorted content * we choose) and consider every position in each array, as * well as every "between" position in each array. To make it * easier to do this, we make arrays of the form {2,4,6,8,...2*n} * and search for values from 1 to 2*n+1. */publicstaticvoidmain(String[] args) {// Create something that can search.TrinarySearch searcher =newTrinarySearch();// The array we'll be usingint[] values;// The position found.intpos;// For output.SimpleOutput out =newSimpleOutput();// For each reasonable size of arrayfor(intsize = 1; size <= 10; ++size) {// A note.out.println("Checking array of size" + size);// Build the array.values =newint[size];// Fill in the elements of the array with the numbers// from 2 to 2*size.for(inti = 1; i <= size; ++i) { values[i-1] = 2*i; }// Search for values between 1 and 2*size + 1.for(intval = 1; val <= 2*size+1; ++val) { try { pos = searcher.trinarySearch(val,values);// If it's odd, it shouldn't have a position.if(val % 2 == 1) out.println("ERROR! The position of" + val + "was given as" + pos);// If it's even, it's position should be (val/2)-1.elseif(pos != (val/2)-1) { out.println("ERROR! The position of" + val + "was given as" + pos); } }catch(Exception e) {// It's okay to throw exceptions for odd numbers// (which aren't in the array). But even numbers// should be there.if(val % 2 == 0) out.println("ERROR! Indicated that" + val + "was not in the array"); } // catch } // for each value } // for each size } // main } // TrinarySearch

A number of you decided that `trinarySearch(`

should not throw an exception. It's not clear why you made
that change. Good design says that when something fails, it throws an
exception.
__int__ x, __int__[]
A)

A large number of you neglected to observe that 1/3 and 2/3 are both 0. I have little confidence that your code would then have the appropriate running time.

Find a tight upper bound on the running time of the working trinary search, using Big-O notation.

In the worst case, we keep going until we run out of elements. Each
time through we need to do three comparisons (but that disappears
in the Big-O notation). Each time, we divide the
array in thirds. Hence, the running time is O(log_{3} n).

Note that log_{3} n = log_{3} 2 * log_{2} n.
Why? Note first that 2 = 3^{log32}, by the
definition of log. Note also that n = 3^{log3n} =
2^{log2n} =
(3^{log32})^{log2n} = 3
^{log32*log2n}. Hence log_{3} n
= log_{3} 2 * log_{2}n. Q.E.D.

Hence, the running time is really just O(log_{2} n).

[Instructions] [Search] [Current] [Syllabus] [Links] [Handouts] [Outlines] [Labs] [More Labs] [Assignments] [Quizzes] [Examples] [Book] [Tutorial] [API]

**Disclaimer** Often, these pages were created "on the fly" with little, if any, proofreading. Any or all of the information on the pages may be incorrect. Please contact me if you notice errors.

This page may be found at http://www.math.grin.edu/~rebelsky/Courses/CS152/99S/Assignments/notes.06.html

Source text last modified Sat Mar 27 12:02:05 1999.

This page generated on Tue Apr 6 10:38:46 1999 by SiteWeaver. Validate this page's HTML.

Contact our webmaster at rebelsky@math.grin.edu