Fundamentals of Computer Science II (CSC-152 99S)


Notes on Assignment 6: Algorithm Design and Analysis

Problem 1: Big-O Identities

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.

  1. Because f(N) is in O(g(N)), there exist N1 and D1 such that for all N > N1, |f(N)| <= |D1*g(N)|
  2. Because g(N) is in O(h(N)), there exist N2 and D2 such that for all N > N2, |g(N)| <= |D2*h(N)|
  3. Let N3 = max(N1, N2)
  4. Let D3 = max(1,D1) * max(1,D2)
  5. Then for all N > N3, |f(N)| <= |D1*g(N)| [By the first rule and the definition of N3.]
  6. Then for all N > N3, |g(N)| <= |D2*h(N)| [By the second rule and the definition of N3.]
  7. Then for all N > N3, |D1*g(N)| <= |D1*D2*h(N)| [Since D1 > 0, we can safely multiply both sides of an inequality for D1 without affecting the inequality.]
  8. Then for all N > N3, |f(N)| <= |D1*D2*h(N)| [We can plug together various inequalities using transitivity of inequality.]
  9. Then for all N > N3, |f(N)| <= |D3*h(N)| [D3 >= D1*D2.]
  10. 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.

1.a. Simplification

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

  1. If f(N) is in O(g(N) + h(N)), then there exist N1 and D1 > 0 such that for all N greater than N1, |f(N)| <= |D1*(g(N) + h(N))|. [By the definition of Big-O.]
  2. If g(N) is in O(h(N)), then there exist N2 and D2 > 0 such that for all N greater than N2, |g(N)| <= |D2*h(N)|
  3. Let N3 = max(N1,N2).
  4. Let D3 = D1 * (1 + D2).
  5. For all N > N3, |f(N)| <= |D1*(g(N) + h(N))|. [By step 1 and definition of N3.]
  6. For all N > N3, |f(N)| <= D1*|g(N)+h(N)|. [If D>0, then |D*X| = |D|*|X|.]
  7. For all N > N3, |f(N)| <= D1*|g(N)| + D1*|h(N)|. [|X+Y| <= |X|*|Y|.]
  8. For all N > N3, |f(N)| <= D1*|D2*h(N)| + D1*|h(N)|. [By step 2 and definition of N3.]
  9. For all N > N3, |f(N)| <= |(D1*D2+D1)*h(N)|. [Various arithmetical manipulations; D1 > 0.]
  10. For all N > N3, |f(N)| <= |D3*h(N)|. [Definition of D3.]
  11. f(N) is in O(h(N)). [Definition of Big-O.]

1.b. Eliminating Constants

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

  1. If f(N) is in O(C*g(N)), then there exist N1 and D1 > 0 such that for all N greater than N1, |f(N)| <= |D1*C*g(N)|. [By the definition of Big-O.]
  2. Let N2 = N1.
  3. Let D2 = |D1*C|.
  4. For all N > N2, |f(N)| <= |D1*C*g(N)|. [By step 1 and definition of N2.]
  5. For all N > N2, |f(N)| <= |D2*g(N)|. [By definition of D2.]
  6. f(N) is in O(g(N)). [By definition of Big-O.]

Note that we had to make sure that D2 was greater than 0, and did so by making it the absolute value of D1*C.

2. The Fibonacci Numbers

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
 */
public long fib(long n) {
  if (n <= 1) return n;
  else return fib(n-1) + fib(n-2);
} // fib(long)

2.a. Bounding the Running Time

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) <= 2k*f(n-k). When k is (n-1), we have f(n) <= 2n-1*f(1). Since it takes about one step to compute f(1), we can say that f(n) <= 2n-1. Hence, f(n) is in O(2n).

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(2n/2).

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

2.b. Using Dynamic Programming

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.


import SimpleOutput;

/**
 * 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
 */
public class Fibonacci {
 
  // +--------+--------------------------------------------------
  // | 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.
   */
  protected long FIB[];


  // +---------+-------------------------------------------------
  // | 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
   */
  public long fib(long n) {
    // Base case: the 0th Fibonacci number is 0, the 1st
    // Fibonacci number is 1.
    if (n <= 1) return n;
    // Recursive case: the nth Fibonacci number is
    // the n-1st Fibonacci number + the n-2nd Fibonacci number.
    return fib(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
   */
  public long fibdp(long n) {
    // Base case: the 0th Fibonacci number is 0, the 1st
    // Fibonacci number is 1.
    if (n <= 1) return n;
    // 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 = new long[(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 = new long[(int) n+1];
      // Copy over the elements.
      for (int i = 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) return FIB[(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.
    return FIB[(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
   */
  public long fibit(long n) {
    int i;       // Counter variable
    int ithfib;  // The ith Fibonacci number
    int prevfib; // The previous Fibonacci number
    int nextfib; // The next Fibonacci number
    // The technique doesn't work for n = 0, so use the
    // default answer for that particular case.
    if (n == 0) return 0;
    // 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.
    return ithfib;
  } // 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
   */
  public long fibrec(long n) {
    if (n == 0) return 0;
    else return fibrec(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
   */
  public long fibrec(long n, long i, long ithfib, long prevfib) {
    // Base case: i is n, so the ith Fibonacci number is the
    //   nth Fibonacci number.
    if (i == n) return ithfib;
    // Recursive case
    else return fibrec(n, i+1, ithfib+prevfib, ithfib);
  } // fibrec(long,long,long,long)

  // +------+----------------------------------------------------
  // | Main |
  // +------+

  /**
   * Get some values of N and compute.
   */
  public static void main(String[] args) {
    // Create an array of inputs.
    long[] vals = new long[args.length];
    // Create an array of results.
    long[] results = new long[args.length];
    // Two time values, used to determine how long the various
    // computations take.
    long start;
    long stop;
    // The wonderful thing that does all the computation.
    Fibonacci computer = new Fibonacci();
    // And something for output.
    SimpleOutput out = new SimpleOutput();
    // 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 (int i = 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 (int i = 0; i < vals.length; ++i) {
      results[i] = computer.fib(vals[i]);
    } // for
    stop = System.currentTimeMillis();
    // Report
    out.println("Naive recursive method: ");
    for (int i = 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 takes
    start = System.currentTimeMillis();
    for (int i = 0; i < vals.length; ++i) {
      results[i] = computer.fibdp(vals[i]);
    } // for
    stop = System.currentTimeMillis();
    // Report
    out.println("Dynamic programming method: ");
    for (int i = 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 takes
    start = System.currentTimeMillis();
    for (int i = 0; i < vals.length; ++i) {
      results[i] = computer.fibit(vals[i]);
    } // for
    stop = System.currentTimeMillis();
    // Report
    out.println("Iterative method: ");
    for (int i = 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 takes
    start = System.currentTimeMillis();
    for (int i = 0; i < vals.length; ++i) {
      results[i] = computer.fibrec(vals[i]);
    } // for
    stop = System.currentTimeMillis();
    // Report
    out.println("Other recursive method: ");
    for (int i = 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.


import SimpleOutput;
import Fibonacci;

/**
 * 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
 */
public class FibTimer {
  // +------+----------------------------------------------------
  // | Main |
  // +------+

  /**
   * Get some values of N and compute.
   */
  public static void main(String[] args) {
    // Create an array of inputs.
    long[] vals = new long[args.length];
    // Create an array of results.
    long[] results = new long[args.length];
    // Two time values, used to determine how long the various
    // computations take.
    long start;
    long stop;
    // The wonderful thing that does all the computation.
    Fibonacci computer = new Fibonacci();
    // And something for output.
    SimpleOutput out = new SimpleOutput();
    // 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.
    int REPETITIONS = 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 (int i = 0; i < args.length; ++i) {
      try { vals[i] = Long.parseLong(args[i]); }
      catch (Exception e) { }
    } // for

    // Determine how long the dynamic processing technique takes
    start = System.currentTimeMillis();
    for (int j = 0; j < REPETITIONS; ++j) {
      for (int i = 0; i < vals.length; ++i) {
        results[i] = computer.fibdp(vals[i]);
      } // for
    } 
    stop = System.currentTimeMillis();
    // Report
    out.println("Dynamic programming method: ");
    for (int i = 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 takes
    start = System.currentTimeMillis();
    for (int j = 0; j < REPETITIONS; ++j) {
      for (int i = 0; i < vals.length; ++i) {
        results[i] = computer.fibit(vals[i]);
      } // for
    } // for
    stop = System.currentTimeMillis();
    // Report
    out.println("Iterative method: ");
    for (int i = 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 takes
    start = System.currentTimeMillis();
    for (int j = 0; j < REPETITIONS; ++j) {
      for (int i = 0; i < vals.length; ++i) {
        results[i] = computer.fibrec(vals[i]);
      } // for
    }
    stop = System.currentTimeMillis();
    // Report
    out.println("Other recursive method: ");
    for (int i = 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

2.c. Re-Bounding the Running Time

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

2.d. Iterative Computation

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.
    */
   public long fibitdp(long n) {
     if (n == 0) return 0;
     long[] fibs = new long[n+1];
     fibs[0] = 0;
     fibs[1] = 1;
     for (int i = 2; i &;lt= n; ++i) {
       fibs[i] = fibs[i-1] + fibs[i-2];
     }
     return fibs[i];
   } // fibitdp(long)

3. Trinary Search

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
 */
public int trinarySearch(int x, int[] A) 
  throws Exception
{
  // Use the marvelous helper function
  return trinarySearch(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
 */
public int trinarySearch(int x, int[] A, int lb, int ub) 
  throws Exception
{
  // Base case: empty subarray.  x is not in A.
  if (ub < ub) throw new Exception("Not found");
  // Base case: single-element subarray.  See if x is that element.
  else if (lb == ub) {
    if (x == A[lb]) return lb;
    else throw new Exception("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.
    int splitOne = 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.
    int splitTwo = lb + 2/3 * (ub-lb);
    
    // Recursive case: in the first third of the array.
    if (x <= splitOne) return trinarySearch(x, A, lb, splitOne);
    // Recursive case: in the second third of the array.
    else if (x < splitTwo) return trinarySearch(x, A, splitOne+1, splitTwo-1);
    // Recursive case: in the third third of the array.
    else return trinarySearch(x, A, splitTwo, ub);
  }
} // trinarySearch(int, int[], int, int)

3.a. Errors in Trinary Search

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 floats and then convert back to ints, but I think that's more complicated.

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.

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.


import SimpleOutput;

/**
 * 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
 */
public class TrinarySearch {

  // +---------+-------------------------------------------------
  // | 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
   */
  public int trinarySearch(int x, int[] A) 
    throws Exception
  {
    // Use the marvelous helper function
    return trinarySearch(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
   */
  public int trinarySearch(int x, int[] A, int lb, int ub) 
    throws Exception
  {
    // Base case: empty subarray.  x is not in A.
    if (ub < lb) throw new Exception("Not found");
    // Base case: single-element subarray.  See if x is that element.
    else if (lb == ub) {
      if (x == A[lb]) return lb;
      else throw new Exception("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.
      int splitOne = 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.
      int splitTwo = lb + (2 * (ub-lb))/3;
      
      // Recursive case: in the first third of the array.
      if (x <= A[splitOne]) 
        return trinarySearch(x, A, lb, splitOne);
      // Recursive case: in the second third of the array.
      else if (x <= A[splitTwo]) 
        return trinarySearch(x, A, splitOne+1, splitTwo);
      // Recursive case: in the third third of the array.
      else 
        return trinarySearch(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.
   */
  public static void main(String[] args) {
    // Create something that can search.
    TrinarySearch searcher = new TrinarySearch();
    // The array we'll be using
    int[] values;
    // The position found.
    int pos;
    // For output.
    SimpleOutput out = new SimpleOutput();

    // For each reasonable size of array
    for (int size = 1; size <= 10; ++size) {
      // A note.
      out.println("Checking array of size " + size);
      // Build the array.
      values = new int[size];
      // Fill in the elements of the array with the numbers
      // from 2 to 2*size.
      for (int i = 1; i <= size; ++i) {
        values[i-1] = 2*i;
      }
      // Search for values between 1 and 2*size + 1.
      for (int val = 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.
          else if (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(int x, int[] A) 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.

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.

3.b. 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(log3 n).

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

Hence, the running time is really just O(log2 n).


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