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 from which we can remove items and into which we can insert items. But we are very restricted as to where these insertions and removals may occur. In a stack, they occur at one end, called the top. In queues, insertions occur at one end, the rear, and removals at the other end, the front. In the most restrictive versions of stacks and queues, we are also limited 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) 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 with Cursors.

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 (grounded) 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                                                         rear

In (most versions of) 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 mechanism for maneuvering (or navigating) through the structure and for viewing what's there. For this purpose, we introduce the concept of a "movable" cursor, which can be positioned at any of a list's nodes or at the special rear position that follows the list's last node. (Note that the front position corresponds to a list's first node, unless the list is empty (i.e., has no nodes), in which case the front and rear positions coincide.)

Most of the operations that we define on positional lists are done with respect to a cursor. For example, to access the contents of a node, we position a cursor at that node and then invoke the cursor's getItem() operation (which, as the name suggests, retrieves the data in the node at which the cursor is positioned). Similarly, to remove a node, we position a cursor there and invoke its remove() operation.

Hence, to express our specification for Positional List with Cursors in Java, we use two interfaces, one for cursors and one for positional lists.

Specification:

public interface PositionalListWithCursors {

   int lengthOf();                       // returns # nodes in the list
   PositionalListCursor getCursor();  // returns a cursor into the list
}

public interface PositionalListCursor {

   /**  observers  **/

   boolean atFront();   // is cursor at front of list?
   boolean atRear();    // is cursor at rear of list?
   T getItem();         // returns item at cursor's position

   // returns the list to which the cursor is associated
   PositionalListWithCursors listOf() 

   // Note: precondition of getItem() is !atRear()

   /**  navigation  **/

   void toFront();    // positions cursor at front of list
   void toRear();     // positions cursor at rear of list
   void toNext();     // moves cursor one position closer to rear
   void toPrev();     // moves cursor one position closer to front

   // positions cursor at same position as c
   void setTo(PositionalListCursor c); 

   // Note : precondition of toNext() is !atRear()
   //        precondition of toPrev() is !atFront()
   

   /**  list mutation  **/

   T remove();              // removes node at which cursor is positioned
   void replace(T newItem); // replace item at cursor's position
   void insert(T newItem);  // inserts new node, containing the specified item,
                            //  as predecessor of cursor's position

   // Note : precondition of remove() and replace() is !atRear()
} 

Arrays vs. Lists:

Some authors present a version of the list concept that is really a hybrid between the list and the array. But one should not confuse the two. Among the distinctions between lists and arrays is that, in an array, there is no notion of "removing" or "inserting" an element. You can place an item into a specified array element (overwriting what is already there), but you cannot insert a new element or remove an existing element. With lists, this restriction does not apply. On the other hand, in a list, retrieving (or replacing) the k-th element, for a specified value of k, is not a "primitive" operation in the sense that locating the specified element is, generally, not a trivial task.

Java's java.util.ArrayList class is a good example of a hybrid structure combining the traditional capabilities of arrays and lists. Suppose, for example, that al is a reference to an ArrayList object. Then the call al.get(4) retrieves the 4-th element of al and the call al.set(4,x) replaces the 4th element by the value of x. These operations are analogous to retrieving a value from, and replacing a value in, an array, respectively, where the element is specified via an index value. But with the ArrayList, one could also make the calls al.add(4,y) and al.remove(4), the former of which inserts the value of x into position 4 (and bumps the elements formerly at positions 4, 5, 6, etc., to positions 5, 6, 7, etc.) and the latter of which removes the value at position 4 (and bumps those at positions 5, 6, 7, etc., into positions 4, 5, 6, etc.).

A natural question to ask is why we should ever choose to use an array (or a list) when we could choose to use a more versatile structure, such as an ArrayList. The overarching, vague answer is: because you can't get something for nothing! Somewhat more precisely, there is a tradeoff between performance and versatility: given two interfaces A and B, where A includes all the operations provided by B, plus more, the likelihood is that an implementation of B will exhibit superior performance (in terms of running times and/or memory usage) to an (equally good) implementation of A. Regarding the ArrayList, as compared to the array, the ability to insert and remove elements imposes a performance penalty upon the task of retrieving them via index value. Regarding the ArrayList, as compared to the list, the ability to retrieve elements via index value imposes a performance penalty upon the task of inserting and removing them.

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 int lengthOf( PositionalListWithCursors list )   {
 
      int cntr = 0;
      PositionalListCursor c = list.getCursor();
      c.toFront();

      // loop invariant: cntr = # of nodes preceding c's position
      while ( !c.atRear() )  {
         cntr = cntr + 1;
         c.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 int sumOf( PositionalListWithCursors<Integer> list )   {
 
      int sum = 0;
      PositionalListCursor<Integer> c = list.getCursor();
      c.toFront();

      while ( !c.atRear() )  {
         sum = sum + c.getItem().intValue();
         c.toNext();
      }
      return sum;
   } 

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

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 (c.getItem().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 void change( PositionalList<Integer> list )  {

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

         c.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 void removeAdjacentDuplicates(PositionalListWithCursors list)  {

      if (list.lengthOf() < 2)  
         {  }
      else  {
         PositionalListCursor pred, crrnt;
         pred = list.getCursor();
         crrnt = list.getCursor();
         pred.toFront();  pred.toNext()
         crrnt.toFront();

         /* loop invariant: there are no adjacent duplicates in the
         *  portion of list preceding crrnt &&
         *  pred and crrnt are at adjacent positions, the latter being
         *  the successor of the former
         */
         while ( !crrnt.atRear() )   {
            if (pred.getItem().equals(crrnt.getItem()))
               { crrnt.remove(); }
            else { 
               pred.toNext();    // or, equivalently, pred.setTo(crrnt);
               crrnt.toNext();
            }
         }
      }
   } 

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

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

      PositionalListCursor<T> cur = list.getCursor();
      cur.toFront();
      /* loop invariant:
      ** All nodes preceding cur contain values less than obj
      */
      while (!cur.atRear()  &&  comp.compare(obj, cur.getItem()) > 0)
         { cur.toNext(); }

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

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

   public PositionalListWithCursors<T> sort(T[] a, Comparator<T> comp)  { 
      PositionalListWithCursors<T> list = new PositionalListWithCursorsViaX<T>();  

      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 by which to compare them, and a "pivot" object, rearrange the items in the list so that, upon completion, all those less than the pivot precede those not less than the pivot. The returned value corresponds to the position at which the not-less-than-the pivot objects begin. Note that this solution requires that the equals() method on cursors yield true iff two cursors are at the same position (within the same list!) and thus does not necessarily correspond to ==.


Solution:

   public void partition(PositionalListWithCursors<T> list,
                         Comparator<T> comp,
                         T pivot)  { 

      int compareResult;
      PositionalListWithCursors<T> cLeft = list.getCursor();
      PositionalListWithCursors<T> cRight = list.getCursor();
      cLeft.toFront();
      cRight.toRear();
      
      /* loop invariant: The set of nodes in list is fixed  &&
      **    all nodes preceding cLeft are less than the pivot  &&
      **    all nodes at or following cRight are not less than the pivot.
      */
      while (!cLeft.equals(cRight)) {
         compareResult = comp.compare(cLeft.getItem(), pivot); 
         if (compareResult >= 0) {
            cRight.insert(cLeft.getItem());
            cRight.toPrev();
            cLeft.remove();
         }
         else {
            cLeft.toNext();
         }
      }
      return cLeft;
   } 

Implementation/Data Representation

Click
here for an array-based representation.