Intro to Abstract Data Types and Sequences


Contents


Introduction

What makes a program good?
  1. it works
  2. it is easy to understand and modify
  3. it is reasonably efficient
To help achieve (2) (which helps with (1)) use abstraction / modularity / information hiding. One example is the use of abstract data types (ADTs). The basic idea of an abstract data type is to separate conceptual objects and operations from their actual implementation. The benefits of using ADTs include:

There are two parts to each ADT:

  1. the public or external part, which consists of:
    • the conceptual picture (the user's view of what the object looks like, how the structure is organized)
    • the conceptual operations (what the user can do to the ADT)
  2. the private or internal part, which consists of:
    • the representation (how the structure is actually stored)
    • the implementation of the operations (actual algorithms used)

Note: There are many possible operations that could be defined for each ADT. They often fall into these categories:

  1. initialize
  2. add data
  3. access data
  4. remove data

The Sequence ADT

Our first ADT is the sequence: an ordered collection of items, one of which is the "current" item. Recall that the external view includes the "conceptual picture" and the set of "conceptual operations". The conceptual picture us something like this:

      item 1     item 2     item 3     . . .     item n
                   ^
                   |
                current
and one reasonable set of operations is:

OPERATION DESCRIPTION
size return the number of items in the sequence
addBefore add a given item just before the current item, or at the front of the sequence if there is no current item; make the new item the current one
addAfter add a given item just after the current item, or at the end of the sequence if there is no current item; make the new item the current one
removeCurrent remove the current item (error if there is no current item); if the current item is the last item in the sequence, then after the remove operation there is no current item; otherwise, make the next item the current item
start make the first item in the sequence be the current item.
getCurrent return the current item (error if there is no current item)
advance advance the current item (error if there is no current item)
isCurrent return true if there is a current item; otherwise, return false
lookup return true if a given item is in the sequence; otherwise, return false

Many other operations are possible; when designing an ADT, you should try to provide enough operations to make the ADT useful in many contexts, but not so many that it gets confusing. It is not always easy to achieve this goal; it will sometimes be necessary to add operations in order for a new application to use an existing ADT.

Before discussing how to implement sequences, let's make sure you understand what the start, getCurrent, advance, and isCurrent operations are supposed to do. Their purpose is to allow a user of a sequence to iterate through the sequence; i.e., to access each item in the sequence in turn, starting with the first item. Suppose we have the following sequence of words (reading the sequence from left to right):

banana, apple, lemon, mango
After invoking the start operation, the sequence would contain the same items, but the first item would be the special "current" item:
banana, apple, lemon, mango
  ^
  |
 current
The getCurrent operation would return the current item, "banana", and the advance operation would change the current item to be the second one, "apple":
banana, apple, lemon, mango
          ^
          |
         current
If we invoke advance two more times, we will have this situation:
banana, apple, lemon, mango
                        ^
                        |
                      current
One more use of advance at this point causes the conceptual current pointer to fall off the end of the sequence:
banana, apple, lemon, mango
                                ^
                                |
                             current
Now there is no current item; the isCurrent operation will return false, and both the getCurrent and advance operations will cause an error. Similarly, when an empty sequence is first created, there is no current item.


TEST YOURSELF #1

Draw "before" and "after" pictures to illustrate the effect of the addBefore operation (a) on the sequence of numbers: 10, 20, 53, 2, in which 20 is the current item, and (b) on the same sequence of numbers in which there is no current item. In both cases, assume that the item being added to the sequence is the number 6.

solution


Now consider the "private" or "internal" part of the sequence ADT. In other words, how should we actually implement sequences in Java? First, note that Java classes are a good match with the idea of ADTs: the public and privates fields and methods of the class correspond to the public (external) and private (internal) parts of an ADT. So let's use a Java class for our sequence ADT.

Note that we could define different classes for different kinds of sequences (e.g., an IntSequence contains only integers, a StringSequence contains only strings, etc). Another alternative (which we will choose here) is to define the Sequence class as a sequence of Objects; that way, we only need one class definition, and we can use different instances of the class to store different kinds of items (we can even have a sequence that contains more than one kind of item, e.g., both Strings and Integers).

We will consider two ways to implement the Sequence class: using an array, and using a linked list (the former is covered in this set of notes; the latter in a later set of notes).

Array Implementation

Here's an outline of the Java definition for the Sequence class, implemented using an array to store the items in the sequence (the bodies of the methods are not filled in for now):

public class Sequence {
  //*** methods ***

  // constructor
    public Sequence() { ... }      

  // add items
    public void addBefore(Object ob) { ... }  
    public void addAfter(Object ob) { ... }   

  // remove items
    public void removeCurrent() { ... } throws NoCurrentException { ... }

  // iteration
    public void start() { ... }    
    public Object getCurrent() throws NoCurrentException { ... }  
    public void advance() throws NoCurrentException { ... }       
    public boolean isCurrent() { ... }        

  // other methods
    public int size() { ... }      
    public boolean lookup(Object ob) { ... }  

  // *** fields ***
    private Object[] items; // the items in the sequence
    private int current;    // the index of the current item
    private int numItems;   // the number of items in the sequence
}  

Notes:

  1. The public methods provide the "external" view of the sequence ADT. Looking only at the "signatures" of the public methods (the method names, return types, and parameters), plus the descriptions of what they do (e.g., as provided in the table above), a programmer should be able to write code that uses Sequences. It is not necessary for a client of the Sequence class to see how the Sequence methods are actually implemented.
  2. The private fields provide the "internal" view -- the actual implementation.
  3. Because the Sequence class is a public class, this code must be in a file named Sequence.java.
  4. Methods removeCurrent, getCurrent and advance are defined to throw an exception (NoCurrentException) if they are called when there is no current item. That exception should be defined (as a public class) in a file named NoCurrentException.java.

Implementing addAfter

Now let's think about how to implement some of the Sequence methods. First, consider the addAfter method. Recall that it adds a given item immediately after the current item if there is a current item; otherwise, it adds the item at the end of the sequence. In either case, the new item becomes the current item. Here is a conceptual picture of what addAfter should do when there is a current item:
      BEFORE:  item 1    item 2    item 3    ...    item n
                           ^
                           |
                          current


      AFTER:   item 1    item 2    NEW ITEM    item 3    ...    item n
                                      ^
                                      |
                                     current
and here's what it should do when there is no current item:
      BEFORE:  item 1    item 2    item 3    ...    item n


      AFTER:   item 1    item 2    item 3    ...    item n    NEW ITEM
                                                                 ^
                                                                 |
                                                                current

Now let's think about the actual code. We'll need two cases, depending on whether there is a current item. If there is, we'll need to move all of the items after the current item one place to the right in the array before inserting the new item. In both cases, the new item becomes the current item, and in both cases, we need to worry about whether the array is full. So here's an outline for the code:

public void addAfter( Object ob ) {
// if the array is full, expand it

// if there is a current item:
//    o move all items after the current item one place to the right
//    o increment current (so that it is the index of the space
//      in which the new item will be stored)

// if there is no current item:
//    o set current to be the index of the first free space at the
//      end of the array

// in both cases, store the new item in the space indexed by current
// and increment the count of the number of items in the sequence
}

Note that the addBefore method will also need code to expand the array and to move items to the right; therefore, it seems like a good idea to define (private) methods for those tasks. Here's the final code for addAfter, including calls to the new private methods:

    public void addAfter(Object ob) {
	// if the array is full, expand it
 	if (numItems == items.length) expandArray();

	if (isCurrent()) {
	  // move all items after the current item one place to the right
	  // and increment current
	    moveItemsRight(current+1);
	    current++;
	}
	else {
	  // set current to be the index of the first free space at the
	  // end of the array
	    current = numItems;
	}

	// store the new item in the space indexed by current
	// and increment the count of the number of items in the sequence
	items[current] = ob;
	numItems++;
    }

In general, when you write code it is a good idea to think about special cases. For example, does addAfter work when the sequence is empty? When the current item is the first one? When it is the last one? You should think through these cases (perhaps drawing pictures to illustrate what the sequence looks like before the call to addAfter, and how the call to addAfter affects the sequence). Decide if the code works as is, or if some modifications are needed.


TEST YOURSELF #2

Question 1.
Complete method expandArray using the following header. You should use the method System.arraycopy to copy the values from the current array to the new array. System.arraycopy has five parameters:

private void expandArray() {
// allocate an array of twice the size of the "items" array
// copy all values from "items" to the new array
// assign "items" to refer to the new array
}

Question 2.
Complete method moveItemsRight using the following header.

private void moveItemsRight(int start) {
// move all of the items in the sequence, starting with the item with
// index "start", one place to the right (i.e., move items[start] to
// items[start+1], move items[start+1] to items[start+2], etc

}

solution


Implementing removeCurrent

Now consider the removeCurrent method. Recall that it throws an exception if there is no current item. Otherwise, it removes the current item. If the removed item was the last item in the sequence, then after removeCurrent finishes, isCurrent() is false. Otherwise, the item after the one that was removed becomes the current item. Here's an outline for the code:
public void removeCurrent() throws NoCurrentException {
// error if there is no current item

// otherwise, move all of the items after the current item one place to
// the left and decrement the count of the number of items in the sequence


}

Note that moving the items after the current item to the left accomplishes two things:

  1. It overwrites the value of the current item, thus removing it from the sequence.
  2. It leaves the current field set to the correct value whether or not the removed item was the last one in the sequence!

Here's the final code for removeCurrent, using a new (private) method to move items to the left:

    public void removeCurrent() throws NoCurrentException {
	// error if there is no current item
	if (!isCurrent()) throw new NoCurrentException();
	else {
	  // otherwise, move all of the items after the current item one
	  // place to the left and decrement the count of the number of
	  // items in the sequence
	    moveItemsLeft(current+1);
	    numItems--;
	}
    }
Again, you should make sure that the code works in "boundary" cases (e.g., when the sequence only contains one item).

Implementing the constructor

Now let's think about the constructor function, which should initialize the fields so that the sequence is empty. Clearly, numItems should be set to zero. How about the other two fields? The items field could be set to null, but that would mean another special case in addAfter (and addBefore). A better idea would be to initialize items to refer to an array with some initial size, perhaps specified using a static final field, so that the initial size could be easily changed.

As for the current field, the constructor should initialize it so that there is no current item. In general, the current field will contain the index of the current item. What should it contain if there is no current item? Any value will do as long as we can tell the difference between a sequence with and without a current item (methods isCurrent, advance, getCurrent, and removeCurrent all need to make that distinction). A valid index is any value in the range 0 to numItems-1, so if current has a value outside that range, it means that there is no current item. So a reasonable value to use in the constructor is -1.

Below is the code for the constructor (including the declaration of the static field for the initial size). This code uses 10 as the initial size. In practice, the appropriate initial size will probably depend on the context in which the Sequence class is used. The advantage of a larger initial size is that more "add" operations can be performed before it is necessary to expand the array (which requires copying all items). The disadvantage is that if the initial array is never filled, then memory is wasted. The requirements for memory usage and runtime performance of the application that uses the Sequence class, as well as the expected sizes of the sequences that it uses should be used to determine the appropriate initial size.