CMPS 144
Array-based Implementation of Positional Lists with Cursors

The Naive Approach

A naive approach for using an array to implement a positional list is to store the list items in locations [0..numItems) of the array, where numItems stores the length of the list. An associated cursor would be represented by an int value.

To illustrate, here is our familiar list of animals, with two associated cursors, and a naive array-based representation thereof:

List of AnimalsNaive Array-based
Representation
+---+  +---+  +---+  +---+  +---+  +---+  +---+
| C |  | D |  | B |  | A |  | O |  | Y |  | C |
| A |--| O |--| U |--| N |--| W |--| A |--| O |--*
| T |  | G |  | G |  | T |  | L |  | K |  | W |  ^
+---+  +---+  +---+  +---+  +---+  +---+  +---+  |
  ^      ^                    ^                  | 
  |      |                    |                  |
  |      |                    |                  |
front    |                    |                rear 
       cursor               cursor
         #1                   #2
   +-----+
 0 | CAT |
   +-----+     +-----+
 1 | DOG |     |  7  |
   +-----+     +-----+
 2 | BUG |     numItems
   +-----+
 3 | ANT |
   +-----+     +---+
 4 | OWL |     | 1 |
   +-----+     +---+
 5 | YAK |     loc (#1)
   +-----+
 6 | COW |
   +-----+     +---+
 7 |  -  |     | 4 |
   +-----+     +---+
 8 |  -  |     loc (#2)
   +-----+
 9 |  -  |
   +-----+
  contents 

Hence, the class headings and instance variable declarations of classes implementing the interfaces for positional lists with cursors and the cursors themselves might look like this:

public class PositionalListWithCursorsViaAry<T> implements PositionalListWithCursors<T> {

   protected int numItems;    // # of items in the list
   protected T[] contents;    // list items are stored in contents[0..numItems)

   ...
   ...
}

public class PositionalListViaAryCursor<T> implements PositionalListCursor<T> {

   // reference to the associated positional list
   private PositionalListWithCursorsViaAry<T> list;

   // location in array corresponding to cursor's position
   private int loc; 

   ...
   ...
} 

Note that a cursor being at the rear of a list corresponds to its loc value being equal to the list's numItems value.

To explore this representation scheme, let's try to implement several of the observer operations that we included in our PositionalListCursor interface.

public boolean atFront() { return loc == 0; }
public boolean atRear()  { return loc == list.numItems; }
public T getItem() { return list.contents[loc]; } 

Recall that getItem() has as its precondition !atRear(). If we had chosen to implement getItem() in accord with the defensive programming style (in which a method assumes responsibility for verifying that its precondition is met), we could have written it as follows:

public T getItem() {
   if (atRear()) 
      { throw an exception }
   else
      { return list.contents[loc]; }
} 

So we see that this scheme gives us simple and efficient (indeed, constant-time) ways of implementing the observer operations. Let's turn to navigation operations:

public void toFront() { loc = 0; }
public void toRear() { loc = list.numItems; }
public void toNext() { loc = loc + 1; }
public void toPrev() { loc = loc - 1; }

Employing the defensive style in implementing toNext(), we get

public void toNext() {
   if (atRear())
      { throw an exception }
   else
      { loc = loc + 1; }
} 

The defensive version of toPrev() is similar.

Regarding mutators that change the list itself, replace() is simple:

public void replace(T x) 
   { list.contents[loc] = x; } 

We leave it to the reader to devise the defensive version.

So far, everything looks fine. But it remains to implement remove() and insert(), which, being the two operations that change the list's structure, are apt to be the most difficult to implement:

public void remove() { 
   // shift list.contents[loc+1..numItems) one place to the left
   for (int i = loc; i != list.numItems-1;  i = i+1) { 
      list.contents[i] = list.contents[i+1];
   }
   list.numItems = list.numItems - 1;
}

public void insert(T x) {
   // shift contents[loc..numItems) one place to the right
   for (int i = numItems; i != loc;  i = i-1) { 
      list.contents[i] = list.contents[i-1]; }
   list.numItems = list.numItems + 1;
   // place new value into position indicated by cursor
   list.contents[loc] = x;  
} 

In remove(), the values in the segment contents[loc+1..numItems) are shifted "to the left" one place in order to "plug" the hole left by the item being removed from location loc. On average, then, we expect that about half of the items in the list will be shifted, and, in the worst case, all of them will be shifted (except for the item being removed). It follows that this is a linear time (i.e., O(n)) operation. A similar observation holds for insert(), except that the reason for shifting (this time "to the right") is to open up space (at location loc) for an item to be inserted.

Given the constraints of the representation scheme that we have chosen (namely, that the list items be held, in their logical order, in contents[0..numItems)), there seems to be no way to avoid having the insert() and remove() operations perform this wholesale element-shifting, which necessarily results in a linear-time computation. This leads us to wonder whether there exists a different array-based representation scheme that allows for more efficient versions of these operations.


A Better Approach

Recall that, in devising an efficient array-based representation for queues, the key idea was to allow the element at the front of the queue to be stored at any of the locations in the array, followed by all the others (in a "wrap-around" fashion). Taking a similar approach here will help. But we need to go a step further. Because insertions and deletions occur only at the ends of a queue, keeping a queue's items stored (in order from front to rear) in a contiguous array segment (possibly wrapped around from the end to the beginning) never requires that the elements in a sub-segment be shifted in order to plug a hole caused by a removal or to create a hole needed for an insertion. Such is not the case for lists, in which insertion and removal may occur anywhere. To make array segment shifting unnecessary, we relax the condition that the order in which the items occur in the list necessarily corresponds to the order in which they are stored in the array. Indeed, we allow ourselves the freedom to store the data describing a list node at any location of the array, except that we reserve element 0 for storing information regarding the rear position.

But then how do we keep track of which node is first, which is second, and so on? The answer is that, for each node, we also store the locations of its predecessor and successor.

To illustrate such a scheme, consider the following figure, which shows again our list of animals (on the left), viewed from a high level of abstraction, and one possible representation thereof, which uses three arrays, contents[], pred[] (for "predecessor"), and succ[] (for "successor"), as well as int variable frontLoc.

+---+  +---+  +---+  +---+  +---+  +---+  +---+
| C |  | D |  | B |  | A |  | O |  | Y |  | C |
| A |--| O |--| U |--| N |--| W |--| A |--| O |--x
| T |  | G |  | G |  | T |  | L |  | K |  | W |  ^
+---+  +---+  +---+  +---+  +---+  +---+  +---+  |
  ^                                              |
  |                                              |
 front                                         rear 
    pred      contents     succ 
   +----+    +--------+   +----+
 0 |  8 |    |  null  |   | -1 |
   +----+    +--------+   +----+
 1 |  5 |    |  DOG   |   | 10 |
   +----+    +--------+   +----+
 2 |  3 |    |  OWL   |   |  7 |
   +----+    +--------+   +----+
 3 | 10 |    |  ANT   |   |  2 |
   +----+    +--------+   +----+
 4 |    |    |        |   |    |
   +----+    +--------+   +----+
 5 | -1 |    |  CAT   |   |  1 |
   +----+    +--------+   +----+
 6 |    |    |        |   |    |
   +----+    +--------+   +----+
 7 |  2 |    |  YAK   |   |  8 |
   +----+    +--------+   +----+
 8 |  7 |    |  COW   |   |  0 |
   +----+    +--------+   +----+
 9 |    |    |        |   |    |
   +----+    +--------+   +----+
10 |  1 |    |  BUG   |   |  3 |
   +----+    +--------+   +----+
11 |    |    |        |   |    |
   +----+    +--------+   +----+
12 |    |    |        |   |    |
   +----+    +--------+   +----+

            +---+
   frontLoc | 5 |
            +---+

Notice that, for each item in the list, (a reference to) it is contained in one of the elements of contents[] and the corresponding elements of pred[] and succ[] "point to" that item's predecessor and successor, respectively. (We use -1 to denote a null pointer.) The values stored in array elements whose contents are not shown are irrelevant. The value of frontLoc tells us where data about the front position (the first node, if the list is not empty) is located. (Note that we don't need a variable rearLoc, because we decided to reserve the zero-th element of each array to store information regarding the rear of the list.)

Notice that it doesn't matter in which element of contents[] a particular list node is stored, as long as the corresponding elements of pred[] and succ[] correctly indicate the locations of that node's predecessor and successor, respectively, and as long as frontLoc points to the right place. Indeed, within this scheme, the same list has many different possible representations (12!/6!, in fact), of which the above is only one example.

For a hint as to why this representation scheme is superior to the naive one that was explored earlier, consider the modifications needing to be made in order to effect a remove(). Suppose that this operation is applied to a cursor that is positioned at the DOG node (which is to say that its loc value is 1). After performing the removal, the list looks like this:

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

With respect to our concrete representation, then, the element of pred[] corresponding to BUG (which had been DOG's successor but is now CAT's) should be changed to point to CAT. Similarly, the element of succ[] corresponding to CAT should point to BUG. If we agree that the cursor through which the remove() was carried out should be adjusted to be positioned at the removed node's successor (which seems to be the most reasonable choice), we should change its loc value to point to BUG (and therefore have loc value 10). And that is all! No loop is needed! That is, under this representation scheme, removing an item from a list is accomplished simply by modifying three int variables, which can be done in constant time, as opposed to an amount of time that depends upon the length of the list (or the length of the array being used to represent it)!

Note: If the removed node were at the front of the list, we would have changed frontLoc rather than the element of succ[] corresponding to that node's (non-existent) predecessor. End of Note.

The original and updated representations, side-by-side, are as follows, with the modified values underlined and in red:

OriginalUpdated
    pred      contents     succ 
   +----+    +--------+   +----+
 0 |  8 |    |  null  |   | -1 |
   +----+    +--------+   +----+
 1 |  5 |    |  DOG   |   | 10 |
   +----+    +--------+   +----+
 2 |  3 |    |  OWL   |   |  7 |
   +----+    +--------+   +----+
 3 | 10 |    |  ANT   |   |  2 |
   +----+    +--------+   +----+
 4 |    |    |        |   |    |
   +----+    +--------+   +----+
 5 | -1 |    |  CAT   |   |  1 |
   +----+    +--------+   +----+
 6 |    |    |        |   |    |
   +----+    +--------+   +----+
 7 |  2 |    |  YAK   |   |  8 |
   +----+    +--------+   +----+
 8 |  7 |    |  COW   |   |  0 |
   +----+    +--------+   +----+
 9 |    |    |        |   |    |
   +----+    +--------+   +----+
10 |  1 |    |  BUG   |   |  3 |
   +----+    +--------+   +----+
11 |    |    |        |   |    |
   +----+    +--------+   +----+
12 |    |    |        |   |    |
   +----+    +--------+   +----+

            +---+
   frontLoc | 5 |
            +---+
    pred      contents     succ 
   +----+    +--------+   +----+ 
 0 |  8 |    |  null  |   | -1 |
   +----+    +--------+   +----+
 1 |  - |    |    -   |   |  - |
   +----+    +--------+   +----+
 2 |  3 |    |  OWL   |   |  7 |
   +----+    +--------+   +----+
 3 | 10 |    |  ANT   |   |  2 |
   +----+    +--------+   +----+
 4 |    |    |        |   |    |
   +----+    +--------+   +----+
 5 | -1 |    |  CAT   |   | 10 |
   +----+    +--------+   +----+
 6 |    |    |        |   |    |
   +----+    +--------+   +----+
 7 |  2 |    |  YAK   |   |  8 |
   +----+    +--------+   +----+
 8 |  7 |    |  COW   |   |  0 |
   +----+    +--------+   +----+
 9 |    |    |        |   |    |
   +----+    +--------+   +----+
10 |  5 |    |  BUG   |   |  3 |
   +----+    +--------+   +----+
11 |    |    |        |   |    |
   +----+    +--------+   +----+
12 |    |    |        |   |    |
   +----+    +--------+   +----+

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

A list class based upon this representation scheme might begin like this:

public class PositionalListWithCursorsViaAry<T> implements PostionalListWithCursors<T> {

   // instance variables
   // ------------------
   protected T[] contents;   // array in which list items are stored
   protected int[] pred;     // predecessor pointers
   protected int[] succ;     // successor pointers
   protected int frontLoc;   // points to array elements holding first node
   ...
   ...
   ...
}

A cursor class could look like the following, including some of the methods implementing observer and navigation operations:

class PositionalListViaAryCursor<T> implements PositionalListCursor<T> {

   // instance variable
   // -----------------
   private PositionalListWithCursorsViaAry<T> list;
   private int loc; // location within list's three arrays describing
                    // the node at which this cursor is positioned

   // observers
   // ---------
   public boolean isEmpty() { return list.frontLoc == 0; }
   public boolean atRear() { return loc == 0; }
   public boolean atFront() { return loc == list.frontLoc; }
   public void toNext() { loc = list.succ[loc]; }
   public void toPrev() { loc = list.pred[loc]; }
   public void toFront() { loc = list.frontLoc; }
   public void toRear() { loc = 0; }
   public T getItem() { return list.contents[loc]; }
   ...
   ...
} 

isEmpty(): Our implementation of isEmpty() is based upon the observation that a list is empty if and only if its front and rear positions coincide. As we always store information about the rear at location zero, it suffices to compare frontLoc to zero. The reader should have no trouble comprehending why the other method bodies shown above will work correctly. (Note that ToPrev() has as its precondition !atFront() and that each of toNext() and getItem() has as its precondition !atRear().)

lengthOf(): We run into a problem when we try to provide the body of lengthOf(), however. The most obvious way to compute the length of a list is to traverse it by going to the front and following the succ pointers until arriving at the rear, and counting along the way. But this would take time proportional to the length of the list! This is exactly what we are trying to avoid!

A different approach for calculating the length of the list is to examine each element of contents[], or one of the other two arrays, to determine whether it is "occupied", as opposed to "empty". (By "occupied" we mean that the element is being used to describe either a node in the list or the rear position.) Is there some way to make this distinction? Well, yes. Recall that, in Java, all elements of an array holding numbers of a primitive type (e.g., int, double) are automatically initialized to zero. But any occupied element of pred[] will contain a non-zero value, because no position has the rear (which corresponds to location zero) as its predecessor! Hence, pred[i] == 0 corresponds to the proposition "Element i is empty.". (If we were working in some language in which such an automatic initialization were not done, we could do the initialization ourselves at the time the array was created.) However, this approach, which entails examining every element of pred[], requires time proportional to pred.length, which is at least as large as the length of the list and, in some circumstances, may be significantly larger! So this approach is even worse.

If the representation scheme we have in mind does not admit a constant-time algorithm for lengthOf(), perhaps we can adjust the scheme so that it does! What if we simply introduce a new instance variable of type int, called numItems, to the PositionalListWithCursorsViaAry class? As suggested by its name, the purpose of this variable is to store the number of items in the list, i.e., its length. This gives us a very simple (and constant-time) algorithm for lengthOf():

public int lengthOf() { return numItems; }

We could also rewrite isEmpty() to utilize the new variable, as follows:

public boolean isEmpty() { return numItems == 0; }

Thus, we have solved the problem, right? Well, not entirely. By introducing a new instance variable with the associated invariant numItems = length of list, we have imposed an extra computational burden upon all the operations that cause a list's length to change. It is conceivable that, in order to maintain this invariant, some operation that otherwise could have been carried out in constant time will now require greater-than-constant time. A few moment's reflection, however, dispels this concern. The only operations that change the length of a list are remove() and insert(). In each case, the modification needing to be made to numItems (a decrement and an increment, respectively) is trivial.

remove(): Now let's consider the remove() operation. As noted above, it requires changes to only a handful of instance variables:

public void remove() {
   if (atRear()) 
      { throw an exception }
   else {
      int predLoc = list.pred[loc];   // location of predecessor of node to remove
      int succLoc = list.succ[loc];   // location of successor of node to remove

      if (predLoc == -1)                   // If node to remove is at front,
         { frontLoc = succLoc; }           // its successor becomes the front.
      else                                 // Otherwise, 
         { list.succ[predLoc] = succLoc; } // connect predecessor to successor.

      list.pred[succLoc] = predLoc;      // connect successor to predecessor

      loc = succLoc;                     // successor becomes cursor's position
      list.numItems = list.numItems - 1; // list has one fewer item than before
   }
} 

Notice that the above takes constant time, as there are no loops. The figure above showed a removal of the DOG node from the example list.

insert(): What about insertion? To perform one, it suffices to

  1. Find the location k of an "unoccupied" array element.
  2. Fill location k of each array with values appropriate for describing the new node:
    1. Place into contents[k] (a reference to) the item belonging in the new node.
    2. Place into pred[k] a pointer to the location holding the new node's predecessor (or −1 if the new node is being inserted at the front of the list).
    3. Place into succ[k] a pointer to the location holding the new node's successor.
  3. If the new node is being inserted at the front of the list, place k into frontLoc. Otherwise, place k (which points to the new node) into succ[predLoc], where predLoc points to the node that is to become the new node's predecessor.
  4. Place k (which points to the new node) into pred[succLoc], where succLoc points to the new node's successor.

Except for the first step, all of them are easy to code:

  1. k = emptyLoc(); // it remains to implement emptyLoc()
    1. list.contents[k] = newItem;
    2. list.pred[k] = list.pred[loc];
    3. list.succ[k] = loc;
  2. if (atFront()) { list.frontLoc = k; }
    else { list.succ[list.pred[k]] = k; }
  3. list.pred[list.succ[k]] = k;

OriginalUpdated
    pred      contents     succ 
   +----+    +--------+   +----+
 0 |  8 |    |  null  |   | -1 |
   +----+    +--------+   +----+
 1 |  5 |    |  DOG   |   | 10 |
   +----+    +--------+   +----+
 2 |  3 |    |  OWL   |   |  7 |
   +----+    +--------+   +----+
 3 | 10 |    |  ANT   |   |  2 |
   +----+    +--------+   +----+
 4 |    |    |        |   |    |
   +----+    +--------+   +----+
 5 | -1 |    |  CAT   |   |  1 |
   +----+    +--------+   +----+
 6 |    |    |        |   |    |
   +----+    +--------+   +----+
 7 |  2 |    |  YAK   |   |  8 |
   +----+    +--------+   +----+
 8 |  7 |    |  COW   |   |  0 |
   +----+    +--------+   +----+
 9 |    |    |        |   |    |
   +----+    +--------+   +----+
10 |  1 |    |  BUG   |   |  3 |
   +----+    +--------+   +----+
11 |    |    |        |   |    |
   +----+    +--------+   +----+
12 |    |    |        |   |    |
   +----+    +--------+   +----+

            +---+
   frontLoc | 5 |
            +---+
    pred      contents     succ 
   +----+    +--------+   +----+
 0 |  8 |    |  null  |   | -1 |
   +----+    +--------+   +----+
 1 |  5 |    |  DOG   |   | 10 |
   +----+    +--------+   +----+
 2 | 11 |    |  OWL   |   |  7 |  (step 4)
   +----+    +--------+   +----+
 3 | 10 |    |  ANT   |   | 11 |  (step 3)
   +----+    +--------+   +----+
 4 |    |    |        |   |    |
   +----+    +--------+   +----+
 5 | -1 |    |  CAT   |   |  1 |
   +----+    +--------+   +----+
 6 |    |    |        |   |    |
   +----+    +--------+   +----+
 7 |  2 |    |  YAK   |   |  8 |
   +----+    +--------+   +----+
 8 |  7 |    |  COW   |   |  0 |
   +----+    +--------+   +----+
 9 |    |    |        |   |    |
   +----+    +--------+   +----+
10 |  1 |    |  BUG   |   |  3 |
   +----+    +--------+   +----+
11 |  3 |    |  PIG   |   |  2 |  (step 2)
   +----+    +--------+   +----+
12 |    |    |        |   |    |
   +----+    +--------+   +----+

            +---+
   frontLoc | 5 |
            +---+
+---+  +---+  +---+  +---+  +---+  +---+  +---+
| C |  | D |  | B |  | A |  | O |  | Y |  | C |
| A |--| O |--| U |--| N |--| W |--| A |--| O |--x
| T |  | G |  | G |  | T |  | L |  | K |  | W |  ^
+---+  +---+  +---+  +---+  +---+  +---+  +---+  |
  ^                                              |
  |                                              |
 front                                         rear 
+---+  +---+  +---+  +---+  +---+  +---+  +---+
| C |  | B |  | A |  | P |  | O |  | Y |  | C |
| A |--| U |--| N |--| I |--| W |--| A |--| O |--x
| T |  | G |  | T |  | G |  | L |  | K |  | W |  ^
+---+  +---+  +---+  +---+  +---+  +---+  +---+  |
  ^                   new                        |
  |                                              |
front                                          rear

The above shows how things would change if a node containing PIG were inserted using a cursor positioned at the OWL node.

emptyLoc(): As for emptyLoc(), let's exploit the fact (introduced in our earlier discussion of how to implement lengthOf()) that pred[i] == 0 if and only if element i is empty.

/* Returns i>0 such that pred[i] = 0.
** precondition: lengthOf() + 1 < pred.length 
*/
private int emptyLoc() {
   int i=1;
   while (list.pred[i] != 0) { i++; }
   return i;
} 

But this method, in the worst case, requires time proportional to the length of contents[], as an "empty" location might not be found until looking at the very last candidate. Which means that its caller, insert(), takes at least that much time. Can we speed it up by having i go through its possible values in a different order? No, because we have no way of predicting which locations are occupied and which are empty.

The fundamental problem is that our representation scheme (so far as we have developed it) fails to include any information that is helpful in quickly locating an unoccupied array element. Luckily, this can be repaired!

Solution 1: Use an "Avail list" (of empty elements)

One effective approach for quickly finding an "empty" array element is to use the empty elements of succ[] to form a chain. To accomplish this, introduce an instance variable avail to the PositionalListWithCursorsViaAry class whose purpose is to point to the "first" empty array element. For each empty location i, succ[i] points to the "next" one (or has the value −1 if there is none). When an unoccupied element is needed (for an insertion into the list), avail provides the location of one in constant time. To update avail, make it point to the "second" unoccupied element, which is given by succ[avail]. When a list item is removed, the location i of the vacated element is placed at the beginning of the avail chain by setting succ[i] to avail and setting avail to i.

To illustrate, below is the concrete representation of our list of animals, augmented to include an Avail list:

    pred      contents     succ      frontLoc     
   +----+    +--------+   +----+   +----------+
 0 |  8 |    |  null  |   | -1 |   |     5    |
   +----+    +--------+   +----+   +----------+
 1 |  5 |    |  DOG   |   | 10 |
   +----+    +--------+   +----+
 2 |  3 |    |  OWL   |   |  7 |       avail
   +----+    +--------+   +----+      +-----+
 3 | 10 |    |  ANT   |   |  2 |      |  6  |
   +----+    +--------+   +----+      +-----+
 4 |    |    |        |   | 12 |
   +----+    +--------+   +----+
 5 |  0 |    |  CAT   |   |  1 |
   +----+    +--------+   +----+
 6 |    |    |        |   | 11 |
   +----+    +--------+   +----+
 7 |  2 |    |  YAK   |   |  8 |
   +----+    +--------+   +----+
 8 |  7 |    |  COW   |   |  0 |
   +----+    +--------+   +----+
 9 |    |    |        |   | -1 |
   +----+    +--------+   +----+
10 |  1 |    |  BUG   |   |  3 |
   +----+    +--------+   +----+
11 |    |    |        |   |  4 |
   +----+    +--------+   +----+
12 |    |    |        |   |  9 |
   +----+    +--------+   +----+ 

The avail list includes, in order, the locations 6, 11, 4, 12, and 9. Insertion of a new node would result in location 6 becoming occupied (with data describing the new node) and location 11 going to the head of the avail list. Removal of the DOG node, for example, would result in location 1 becoming empty and going to the head of the avail list. (In effect, the avail list is a stack insofar as items are inserted and removed from it at the same end!)

Here are implementations of insert() and remove():

public void insert(T newItem) {
   int k = allocateEmptyLocation();  // see below
   list.contents[k] = newItem;
   list.pred[k] = list.pred[loc];
   list.succ[k] = loc;
   if (list.frontLoc == loc)
      { list.frontLoc = k; }
   else
      { list.succ[list.pred[k]] = k; }
   list.pred[list.succ[k]] = k;
   list.numItems++;
}

/* Returns location of empty element and removes it from the avail list.
** pre: lengthOf() + 1 < succ.length
*/
private int allocateEmptyLocation() {
   int result = list.avail;
   list.avail = list.succ[list.avail];
   return result;
}

public void remove() {     // non-defensive version
   int predLoc = list.pred[loc];
   int succLoc = list.succ[loc];
   if (predLoc == -1) {  
      { list.frontLoc = succLoc; }
   else
      { list.succ[predLoc] = succLoc; }
   list.pred[succLoc] = predLoc;
   dispose(loc);   // see below
   loc = succLoc;
   list.numItems--;
}

/* Place k onto the avail list.
*/
private void dispose(int k) {
   list.succ[k] = list.avail;
   list.avail = k;
} 

What is troubling about this approach is that construction of a brand new empty list takes time proportional to contents.length, as the initial avail chain must include every location, and thus each element of succ[] must be initialized to point to some other element. (One way to achieve this is to set avail to contents.length - 1 and, for each i in the index range, succ[i] = i-1.)

It seems rather ironic that our representation scheme is such that its most expensive operation is the construction of an empty list!

One slight advantage that the avail chain approach has over the earlier one is that, under it, remove() is much simpler.

Solution 2: keep unoccupied elements at end of arrays

One solution is to maintain the following representation invariant: Elements 0 through numItems of the three arrays are occupied and the rest are not. (Recall that element zero is used for storing the rear, so we need numItems + 1 locations in total.) Thus, the first unoccupied element is always at location numItems+1. This makes finding an unoccupied element very easy, which makes step (1) in the informal insertion algorithm simple. But it complicates the implementation of remove(), because that method must be modified so as to leave the arrays in a state satisfying the new representation invariant.

For example, suppose that an application of remove() is to have the effect of removing, from a 57-node list (occupying locations 0 through 57), the node that happens to be stored at location 24 of the arrays. Upon completion, locations 0 through 56 of the arrays should be occupied and the rest unoccupied. But the straightforward implementation of remove() given above would leave location 57 occupied and location 24 unoccupied. How can it be fixed?

One way is to transfer, in each of the three arrays, the value in location 57 to location 24. So now the values in location 24 of the arrays describe the node that had been described in location 57. That means that all pointers to that node (i.e., all pointers having value 57) must be changed to have value 24. These would include pred[succ[57]] and either frontLoc (in case frontLoc = 57 happened to be true) or succ[pred[57]]. But these aren't all, because there could be any number of cursors (i.e., instances of PositionalListViaAryCursor) pointing to the node that we moved from location 57 to 24! (Of course, in each of these objects the instance variable loc would have value 57.)

How in the world can we possibly "find" all these cursor objects? To do so would seem to require that, for each array location holding data describing a node, we have an associated list (or stack, or queue, or set, or some kind of container) holding (references to) all the cursor objects that point to that node!

For the moment, let's ignore this issue and develop the remaining code for remove():

public void remove() {
   if (atRear())
      { throw an exception }
   else {
      int predLoc = list.pred[loc];   // loc of predecessor of item to be removed
      int succLoc = list.succ[loc];   // loc of successor of item to be removed

      if (predLoc == -1)              // if item being removed is first,
         { list.frontLoc = succLoc; } // its successor becomes the front
      else
         { list.succ[predLoc] = succLoc; } //connect predecessor to successor

      list.pred[succLoc] = predLoc;      // connect successor to predecessor

      int vacantLoc = loc;
      loc = succLoc;               // cursor moves to successor 


      if (vacantLoc == list.numItems) {
         // no action needed
      }
      else {
         // now adjust arrays by moving contents of last occupied element 
         // into the element that was just vacated 
         list.contents[vacantLoc] = list.contents[numItems];
         list.pred[vacantLoc] = list.pred[numItems];
         list.succ[vacantLoc] = list.succ[numItems];

         // now make all pointers to the last occupied element point to
         // the element that had been vacant
         if (list.pred[vacantLoc] == -1)
            { list.frontLoc = vacantLoc; }
         else
            { list.succ[list.pred[vacantLoc]] = vacantLoc; }

         list.pred[list.succ[vacantLoc]] = vacantLoc; 

         if (loc == list.numItems)   // cursor may need to be adjusted to
             { loc = vacantLoc; }    // point to formerly vacant location

         // Here would go code to update all cursors pointing to the
         // moved node, changing their loc value from numItems to vacantLoc.
       }
       list.numItems = list.numItems - 1;
   }
}

A Hybrid Approach Makes all Operations run in constant time

In order to get the advantages of both approaches, a hybrid approach is possible. Here, an avail chain is maintained, but it includes only unoccupied array elements that had been occupied at some point in the past. In doing an insertion, if the avail chain is empty, the first never-occupied array elements (which will be the ones at location numItems + 1) are used for storing the new item.