CMPS 144 (Computer Science 2)
Dr. R. McCloskey
The (Positional) List ADT/Collection Class

The Concept
Applications
Implementation/Data Representation

The Concept

The list is a generalization of both stacks and queues. Stacks and queues are sequences of items from which we can remove items and into which we can insert items. But we are very restricted as to where these insertions and deletions may occur. In a stack, they occur at one end, called the top. In queues, insertions occur at one end, the rear, and deletions at the other, the front. We are also restricted as to which items are visible: in a stack, only the item at the top is visible; in a queue, only the item at the front (and possibly the one at the rear, depending upon who you ask) is visible.

If we lift these restrictions so as to allow insertion and deletion anywhere, plus the capability of navigation (i.e., "moving around" among the items), we get the concept of a list. Different varieties of lists are obtained by making different choices as to exactly which operations are included to provide these capabilities. The kind of list we present here represents only one such choice, which we refer to as the Positional List.

We can view a list as a sequence of "nodes", each of which contains a data item. Each node has a predecessor and a successor, with the exception of the first and last nodes. If we consider the first node to be the successor of the last, we arrive at the concept of a circular list or ring, as opposed to a grounded list.

Here is one possible depiction of a list of animals:

   +---+    +---+    +---+    +---+    +---+    +---+    +---+
   | C |    | D |    | B |    | A |    | O |    | Y |    | C |
   | A |----| O |----| U |----| N |----| W |----| A |----| O |----x
   | T |    | G |    | G |    | T |    | L |    | K |    | W |    ^
   +---+    +---+    +---+    +---+    +---+    +---+    +---+    |
     ^                 ^                                          |
     |                 |                                          |
     |                 |                                          |
   front            current                                      rear
                    position 

In lists, unlike stacks and queues, observation and mutation may occur not only at the two ends but also in the middle. Thus, we need some means of maneuvering or navigating through the structure. To achieve this, we have the concept of a current position. Several of the operations that we will define on lists have an effect that is, in some sense, local to the current position. In our abstraction, a position corresponds either to a node or to a special position, called the rear, which comes "after" the last node. The unique position that does not follow any node is called the front. That is, if a list has at least one node, the front is the position occupied by the first such node; if a list is empty, the front coincides with the rear. In our example, the current position is at the node containing BUG.

Among the operations on lists are ones that cause the current position to change. This is how navigation takes place. Also, the current position acts as our "viewing window", in the sense that the only node whose contents we can observe is the one at the current position. (If we want to obtain the contents of some other node, we must first move there.)

One might question why we should have a single current position. Why not generalize the idea and make use of the concept of a tab or cursor such that we can define and manipulate as many of them as wanted for any particular list? For some applications, it would be natural to use at least two cursors. (E.g., to determine whether a list is the same as its own reversal, we could have two cursors start at opposite ends of the list and work towards each other, verifying at each step that the two nodes contain equal items.)

The answer to this question is that allowing the use of multiple tabs/cursors

Thus, although it would be an interesting approach, we are going to keep matters simple by limiting each list to having a single cursor, which we refer to as the current position.

Specification: Here's one possible informal specification for a list class consistent with the discussion above:

Observers:

boolean isEmpty() : answers the question "Is list empty?"
int lengthOf()    : yields # of nodes in list
Object getObj()   : yields contents of node at current position
boolean atFront() : answers "Is current position the front of list?"
boolean atRear()  : answers "Is current position the rear of list?"
Remarks: As noted before, in an empty list (i.e., one having length zero), the front and rear positions coincide. The only pre-condition for any of these operations is that atRear()) must not hold when getObj() is invoked.

Navigators (Positional Mutators):

void toFront() : sets current position to front of list
void toRear()  : sets current position to rear of list
void toNext()  : moves current position one place closer to rear
void toPrev()  : moves current position one place closer to front
Remarks: The pre-condition of toNext() (respectively, toPrev()) is !atRear() (respectively, !atFront()).

Node Mutators:

void replace(x)      : replaces current node's contents with x
void insert(x)       : inserts new node containing x; it becomes the
                       predecessor of the current position
void insertFront(x)  : inserts new node containing x at front of list
void insertRear(X)   : inserts new node containing x at rear of list
void remove()        : removes the node at the current position; the
                       successor becomes the current position 

Remarks: The precondition of both replace() and remove() is !atRear().

Arrays vs. Lists:

Among the distinctions between lists and arrays is that, in an array, there is no notion of "deleting" or "inserting" an element. You can place an item into an array element (which overwrites what is already there), but you cannot "insert" a new element or delete an existing element.

Applications of Lists

Having sketched a (rather large, compared to the ADT's we've already seen) collection of operations, let's do a few list "applications".

Problem #0: Develop a method that finds the length of (i.e., the number of nodes in) a list. (Ignore the fact that we stipulated the existence of a lengthOf() operation!)
Solution:

   public static int length( PositionalList list )   {
 
      int cntr = 0;
      list.toFront();

      // loop invariant: cntr = # of nodes preceding current position
      while ( !list.atRear() )  {
         cntr = cntr + 1;
         list.toNext();
      }
      return cntr;
   } 

Problem #1: Develop a method that finds the sum of the values in a list of items from the wrapper class Integer.
Solution:

   public static int sumOf( PositionalList<Integer> list )   {
 
      int sum = 0;

      list.toFront();
      while ( !list.atRear() )  {
         sum = sum + list.getObj().intValue();
         list.toNext();
      }
      return sum;
   } 

Here we see a design pattern that is quite common: traversing the list, examining each node, and, in doing so, performing some operation that helps in the accumulation of some result.

Note that, using the lengthOf() method, we could have written the loop as a for loop, with the loop control variable varying from, say, 0 to list.lengthOf()-1.

Trivial modifications to the program yield solutions to similar problems such as, say, counting the number of nodes containing positive values. Here, we would replace the assignment inside the loop with an if statement

       if (list.getObj().intValue() > 0)
          { cntr = cntr + 1; } 
where cntr is a local variable whose value would, after termination of the loop, be returned as the method's result.

Problem #2: For each node in a given list of items from the (wrapper) class Integer, add one to its contents if it contains a positive value, subtract one if it contains a negative value, and don't change it if it contains zero.
Solution:

   public static void change( PositionalList<Integer> list )  {

      list.toFront();
      while ( !list.atRear() )
         int y = list.getObj().intValue();
         if (y < 0)  
            { list.replace(new Integer(y-1)); }
         else if (y > 0) 
            { list.replace(new Integer(y+1)); }
         else  // y = 0
            {  }

         list.toNext();
      }
   } 

Problem #3: Remove from a list any node containing the same value (as determined by the equals() predicate) as its predecessor. That is, the final state of the list should be such that it contains the same sequence of values as originally, but with no consecutive duplicates.
Solution:

   public static void removeAdjacentDuplicates(PositionalList list)  {

      if (list.isEmpty())  
         {  }
      else  {
         int prevItem, crrntItem;
         list.toFront();
         prevItem = list.getObj();
         list.toNext();

         /* loop invariant: there are no adjacent duplicates in that 
         *  portion of list preceding the current position &&
         *  prevItem equals the value in the predecessor node
         */
         while ( !list.atRear() )   {
            crrntItem = list.getObj();
            if ( prevItem.equals(crrntItem) )
               { list.remove(); }
            else { 
               prevItem = crrntItem;
               list.toNext();
            }
         }
      }
   } 

Problem #4: Assuming that the items in list are in ascending order with respect to the Comparator c, insert a new item into its rightful place.
Solution:

   /* pre:  list is in ascending order (with respect to c)
   ** post: list is still in ascending order, and is the same as before,
   **       except that a new node containing k appears in it
   */
   public static void orderedInsert(PositionalList list, 
                                    Comparator c,
                                    Object obj)  { 

      list.toFront();
      /* loop invariant:
         All nodes preceding current position contain values less than obj
      */
      while (!list.atRear()  &&  c.compare(obj, list.getObj()) > 0)
         { list.toNext(); }

      // assertion: either
      // (1) list.atRear() and obj is larger than every item in list, or
      // (2) !list.atRear() and the current node is first one containing
      //     a value greater than or equal to obj
      list.insert( obj );
   }  

Problem #5: Sorting. Given an array of Objects and a comparator c that defines a total ordering upon them, construct an ascending-ordered list containing the objects in the array.
Solution:

   public static PositionalList sort(Object[] a, Comparator c)  { 
      PositionalList list = new PositionalListViaX();  

      for (int i=0; i != a.length; i = i+1) {
         orderedInsert(list, a[i]);    // from previous problem
      }
      return list;
   } 

Note that if the objects in the array are in "random" order, the running time of the method above would be expected to be proportional to the square of the length of the resulting list. Letting n be the number of items ending up in the list, we use the notation O(n2) to describe this running time.

Problem #6: Partitioning. Given a list of Objects, a Comparator, and a "pivot" object, rearrange the items in the list so that, upon completion, all those less than the pivot precede the current position and all those not less than the pivot follow it.
Solution:

   public static void partition(PositionalList list,
                                Comparator c,
                                Object pivot)  { 

      list.toFront();
      // loop invariant:
      //    list.length() is unchanging and
      //    all items preceding current position are less than pivot and
      //    # items less than pivot following current position is at most i
      int len = list.length;
      for (int i = len; i > 0; i = i-1) {
         Object item = list.getObj();
         if (c.compare(item, pivot) < 0)
            { list.toNext(); }
         else {
            list.remove();
            list.insertAtRear(item);
         }
      } 

Implementation/Data Representation

Click
here for an array-based representation.