Return to lecture notes index

15-100 Lecture 23 (Friday, Nov 3, 2006)

Today's Challenge

Or goal today is to construct an OrderedContainer. Basically, what we'd like to do is to make sure that, as we add things to our container, we add them in the right place to keep things in sorted order. For example, if we add names, we want them to be in alphabetical order. If we add other types of Comparable things, we want them to be in the order prescribed by use of compareTo().

This will result in traversals, such as toString(), appearing in order. And, it will enable searches, such as contains(), to become much faster. The cost will be, of course, a slower insert.

Comparable

Although we'll keep the basic structure of the Container, we now need to limit it to storing only Comparable Objects. The reason for this is that we need to esnure that anything that goes in can be placed into the OrderedContain in the proper order. This means being able to compare items pair-wise.

The basics of the OrderedContainer are a quick adapatation of the old Container -- a quick search-and-replace, in effect:

class OrderedContainer {

  private Comparable[] list;
  private int nextSlot;
  
  private static final int DEFAULT_SIZE = 10;
  private static final double GROWTH_FACTOR = 2.0;
  private static final String NL = System.getProperty ("line_separator");
  

  public OrderedContainer() {
    list = new Comparable[DEFAULT_SIZE];
    nextSlot = 0;
  }
  
  
  public OrderedContainer (int size) {
    list = new Comparable[size];
    nextSlot = 0;
  }
  

  public String toString() {
  
    String rep = "";
    
    for (int index=0; index < nextSlot; index++) {
      rep += items[index] + NL;
    }
  }

  private void grow() {

    Comparable[] biggerList =
                 new Comparable[(int)(GROWTH_FACTOR * list.length)];

    for (int index=0; index < nextSlot; index++)
      biggerList[index] = list[index];

    list = biggerList;
  }
}

Insert: A New Strategy

When it comes time to add an item into the container, we're going to adopt a different strategy. In the simple Container, we simply add()'ed the new item to the end. But, if we follow that strategy now, the container won't be ordered. And, sorting it will be a pain!

So, we're going to put the item into the right spot from the get-go. To do this, we first need to, as we did before, ensure that we've got enough space -- grow()'ing, if necessary.

Next, let's observe that in the past, we would have put the item into the nextSlot. In effect, we an view this nextSlot as a hole to be filled. At the end of the process, if the array is to accomodate one more item, regardless of that new item's eventual location, the nextSlot will need to be filled.

So, the name of the game for us is to start at the end, and ask the question, "Can this new item go into the nextSlot?" It can if, and only if, it is not less than nextSlot's predecessor. Otherwise placing it there would be placing it out of order.

If it can't go into the nextSlot, we know that we need to shift nextSlot's predecessor into nextSlot, in effect moving the hole one slot toward the front. Now, we repeat the process, bubbling the hole toward the front of the array until either (a) the new item is greater than this hole's predecessor, or the hole is at index 0.

If the new item is greater than the hole's predecessor we can drop it off into the hole. This is because anything after it got moved after it because it is greater. And, anything before it remains before it, because it is less than it. The hole is, in effect, in a position that is "just right".

Below is an example:

  OrderedContainer: 2 4 6 8
  Number to insert: 3

  Moving the hole:

    2 4 6 8 _  //hole starts at back, compare 3 and 8 (need to move hole)
    2 4 6 _ 8  //compare 3 and 6 (need to move 6 back and hole forward)
    2 4 _ 6 8  //compare 3 and 4 (need to move hole)
    2 _ 4 6 8  //compare 3 and 2 (you have found the correct spot!)
    2 3 4 6 8  //insert 3 into the created hole and exit

If the hole works its way to index 0, it is because everything within the list is greater than the item, so it is safe to drop the item off into the 0th slot.

The solution looks like this:

  public void add (Comparable item) {

    // Nothing new here: Grow if needed
    if (nextSlot == list.length)
      grow();
 
    // Start the hole at the end and bubble toward the 0 index
    int hole;
    for (hole=nextSlot; hole > 0; hole--) {

      // Stop bubbling if the hole is in the right place
      if (list[hole-1].compareTo(item) < 0)
        break;

      // move the whole, if not. 
      list[hole] = list[hole-1];
    }

    // Drop the new item off into the hole
    list[hole] = item;
    nextSlot++;
  }

Binary Search

Last class, when we implemented contains(), we did it with what is often known as a "brute force" or "linear" search. It is known as a linear search because we walk straight down the line doing the search. the name "brute force" comes from the fact that we are considering all persistently considering all possibilities, rather than using any smarts, in order to seach for the item.

In the case of an unordered list, there was nothing else we could do. The number could be anywhere. And, there was no telling where. But, this time through, the list is ordered. Does that change anything?

Well, I think we'd all agree that we're more likely to find "zebra" near the end of a dictionary -- and "aardvark" near the beginning. So, using intuition, we see that it does.

If we search from beginning to end, each item we consider is either the correct item or eliminates exactly one possibility -- leaving the rest. But, what happens if, instead of starting at the beginning or end, we start in the middle? Then, if it isn't right, we know whether it comes before or after the middle, right? Consider the dictionary -- if it isn't the word we are reading, it either comes before it or after it under the alphabet. So, right away, we can throw away the other half of the words, those that live on the wrong half.

If we perform this "divde and conquer" strategy over-and-over, we've got a winning approach. Eventually, we'll either find it, or we'll be looking at a one-word list -- and have no where to go.

This technique is known as a "binary search", because it cuts the list in half each time. Its power is easy to see, especially in early iterations, when it throws away a huge number of possibilities per comparison.

Let's consider an example:

Looking for: "K" 
 ---
| B | begining of list
 ---
| C |
 ---
| F |
 ---
| K |
 ---
| L | <- index
 ---
| M |
 ---
| O |
 ---
| P |
 ---
| V | end of list
 ---
Check the index: K < L


 ---
| B | begining of list
 ---
| C | <- index
 ---
| F |
 ---
| K | end of list
 ---
| L | 
 ---
| M |
 ---
| O |
 ---
| P |
 ---
| V |
 ---
Check the index: K > C


---
| B |
 ---
| C |
 ---
| F | <- index   begining of list
 ---
| K | end of list
 ---
| L | 
 ---
| M |
 ---
| O |
 ---
| P |
 ---
| V |
 ---
Check the index: K > F

 ---
| B |
 ---
| C |
 ---
| F |
 ---
| K | <- index,  begining of list and end of list
 ---
| L | 
 ---
| M |
 ---
| O |
 ---
| P |
 ---
| V |
 ---
Check the index: K = K

Done

Please consider the following implementation in the context of the contains() method:

 public boolean contains (Comparable item) {

    /*
     * Here we select the left and right bounds of the list to
     * search. Initially, we start searching the whole thing, so
     * [0...nextSlot-1]. From this, we find the length of the
     * list (right - left). Half of that length, (right-left)/2, is
     * the distance form the left side to the middle. So, the middle,
     * pivot value, is   left + (right-left)/2
     */
    int left = 0;
    int right = nextSlot-1;
    int pivot = left + (right - left)/2;

    /*
     * Now we loop until the partition is empty, signified
     * by a right value less than a left value. If they are equal,
     * it is a one item partition.
     *
     * Inside, we return true, if we find it. At the end, false, if we
     * didn't
     */
    while (right >= left) {

      // Do the comparison
      int direction = list[pivot].compareTo(item);

      // Found it!
      if (direction == 0) return true;

      // Didn't find it. Do we look left or right?
      if (direction > 0)
        // Look left
        right = pivot - 1;
      else  
        // Look right
        left = pivot + 1;

      // Find the new middle
      pivot = left + (right-left)/2;
    } // go back to top and repeat

    // We stopped because we ran out of numbers. Not here.
    return false;
  }

OrderedContainer, so far

class OrderedContainer {

  private Comparable[] list;
  private int nextSlot;
  
  private static final int DEFAULT_SIZE = 10;
  private static final double GROWTH_FACTOR = 2.0;
  private static final String NL = System.getProperty ("line_separator");
  

  public OrderedContainer() {
    list = new Comparable[DEFAULT_SIZE];
    nextSlot = 0;
  }
  
  
  public OrderedContainer (int size) {
    list = new Comparable[size];
    nextSlot = 0;
  }
  
  
  public void add (Comparable item) {
 
    if (nextSlot == list.length)
      grow();
  
    int hole;
    for (hole=nextSlot; hole > 0; hole--) {
      if (list[hole-1].compareTo(item) < 0)
        break;

      list[hole] = list[hole-1];
    }

    list[hole] = item;
    nextSlot++;
  }
  
  
  public boolean contains (Comparable item) {

    int left = 0;
    int right = nextSlot-1;
    int pivot = left + (right - left)/2;

    while (right >= left) {
      int direction = list[pivot].compareTo(item);

      // Found it!
      if (direction == 0) return true;

      // Look left
      if (direction > 0) 
        right = pivot - 1;
      else  // Look right
        left = pivot + 1;

      pivot = left + (right-left)/2;
    }

    return false;
  }
  
  public boolean remove (Comparable item) {
  
    int index;
  
    for (index=0; index < nextSlot; index++) {
    
      if (list[index].equals(item))
        break;  
    }
    
    if (index == nextSlot)
      return false;
    
    for (int hole=index; hole < (nextSlot-1); hole++) {
      list[hole] = list[hole+1];
      list[hole+1] = null;
    }
    
    nextSlot--;
  
  }
  
  
  private void grow() {
  
    Comparable[] biggerList = 
                 new Comparable[(int)(GROWTH_FACTOR * list.length)];
    
    for (int index=0; index < nextSlot; index++) 
      biggerList[index] = list[index];
    
    list = biggerList;
  }

  
  public String toString() {
  
    String rep = "";
    
    for (int index=0; index < nextSlot; index++) {
      rep += items[index] + NL;
    }
  }

}