The Queue ADT/Collection Class

The Concept
Applications
Array-based Implementation/Data Representation
Reference-based Implementation/Data Representation

The Concept

In Great Britain (and some other parts of the English-speaking world), what we in the USA refer to as a waiting line (as in a bank or grocery store) is usually called a queue. The person at the front of the queue receives some kind of service and then departs. A person entering the queue does so at the rear. Thus, a queue exhibits a FIFO ("first-in-first-out") arrival/departure pattern. Viewing it slightly differently, we can say that a queue is a sequence of items such that insertions occur at one end (called the rear) and deletions occur at the other end (called the front). The insert and delete operations are usually called enqueue and dequeue, respectively (even though a good argument could be made for referring to them as insert and delete in order to maintain uniformity with other kinds of container structures).

Traditionally, the operations applicable to a queue are analogous to those that can be applied to a stack, there being

Here is a Java interface for the queue ADT:

/** An instance of a class that implements this interface represents a 
**  queue capable of holding items of the type specified by the client in 
**  instantiating the generic type parameter T.  A queue is a container 
**  having a FIFO ("first-in-first-out") arrival/departure pattern (which 
**  is achieved by having a queue be a list in which all insertions occur 
**  at one end (the "rear") and all removals occur at the other (the "front").
**
**  Author: R. McCloskey
**  Date: September 2020
*/
public interface Queue<T> {

   // observers
   // ---------

   /** Returns the number of items on this queue.
   */
   int sizeOf(); 

   /** Returns true iff there are no items on this queue.
   */
   boolean isEmpty(); 

   /** Returns true if this queue fails to have the capacity to store
   **  any more items (i.e., enqueue() would fail), false otherwise.
   */
   boolean isFull(); 

   /** Returns (a reference to) the item at the front of this queue.
   **  pre: !this.isEmpty()
   */
   T frontOf();


   // mutators
   // --------

   /* Removes all the items from this queue, leaving it empty.
   ** post: isEmpty()
   */
   void clear();

   /** Places the specified item onto the rear of this queue.
   **  pre:  !isFull()
   **  post: Letting elem(k) refer to the k-th item on a queue, counting
   **    from the front starting at zero, and letting q refer to this
   **    queue before enqueue() is applied:
   **      this.sizeOf() = q.sizeOf() + 1 &&
   **      this.elem(q.sizeOf()) = item &&
   **      for all k satisfying 0 <= k < q.sizeOf(), this.elem(k) = q.elem(k) 
   */
   void enqueue(T item);


   /** Removes the item at the front of this queue and returns (a reference to) it.
   **  pre: !isEmpty()
   **  post: Letting elem(k) refer to the k-th item on a queue, counting 
   **    from the front starting at zero, and letting q refer to this 
   **    queue before dequeue() is applied:
   **      this.sizeOf() = q.sizeOf() - 1 &&
   **      for all k satisfying 0 <= k < this.sizeOf(), this.elem(k) = s.elem(k+1) 
   */
   T dequeue();

}

Notice that dequeue() acts not only as a mutator but also an observer. This is strictly for the convenience of the client, as it is frequently the case that, when the item at the front of the queue is to be retrieved, it is also to be removed.


Applications

Use of queues by computer operating systems: A computer operating system manages the resources available to the processes running on the machine. Among these resources are the processor(s), space (memory), peripheral devices (e.g., printers, disk units). Often, a process will request a resource that is not currently available because it is already being used by some other process. When this happens, the request is recorded and, later, when the resource becomes available, the request is granted. Often (but not always), the order in which requests for a particular resource are granted corresponds to the order in which they were submitted. One way to achieve this is by associating with each resource a queue in which the as-yet-ungranted requests for it are stored.

The uniform-cost single-source shortest paths problem:

A somewhat more interesting application of a queue is in solving this problem: You are given as input a directed graph G and one of its vertices, v, called the source vertex. The goal is to determine, for every vertex in the graph, the cost of any minimum-cost path to it from the source vertex. The cost of a path is defined to be the sum of the costs of the edges that appear in it. Here, we address only the restricted form of the problem in which every edge is assumed to have cost one. (This explains the phrase "uniform-cost" in the title.) By making all edges have cost one, the problem reduces to that of determining the lengths of the shortest paths from the source vertex to all the vertices in the graph.

For convenience, we assume that the vertices of G are identified by the integers 0 through N-1, where N is the number of vertices in G. The output is an array dist[0..N-1], such that, for each z, 0≤z<N, dist[z] equals d(z), i.e., the distance from vertex v to vertex z, which we define to be the length of any shortest path from v to z. (If there is no path from v to z, dist[z] should end up containing, say, -1.)

One way to solve this problem is to perform a breadth-first search/traversal of the graph, starting with the source vertex. Sometimes this is called a "level-by-level" traversal, because the search implicitly builds a rooted tree whose root is v (the source, whose distance from itself is zero), whose children are the vertices at distance one from v, whose grandchildren are the vertices at distance two from v, etc., etc. This happens because the vertices are "visited/discovered" in ascending order by their distances from the source vertex v.

Such a traversal of the graph can be described as a sequence of explorations and discoveries of the vertices reachable from the source vertex, in which a queue is used for the purpose of properly scheduling those explorations.

In more detail, to explore a vertex simply means to examine each of its outgoing edges to see which vertices lie at the other ends. If a vertex at the other end of an outgoing edge is one that has not been encountered before, then it has just been discovered!

Immediately upon discovering a vertex, we place it at the rear of the queue so that, at some later time, it will be explored for the purpose of discovering more vertices reachable from the source. Indeed, the "next" vertex to be explored is the one at the front of the queue. By using a queue to hold the vertices waiting to be explored, we ensure that they are explored in the same order that they are discovered! This is crucial to the correct workings of the algorithm. In order to get things started, we initially place the source vertex into the queue.

The following is a Java method that implements the algorithm sketched above. Given a digraph and a "source" vertex v within that graph, it returns an array of int's indicating each vertex's distance from v. The method assumes the existence of a class DiGraph each instance of which represents a directed graph whose vertices are identified by the natural numbers in the range [0..N), where N is the number of vertices in the graph. It also assume that that class has the instance methods numVertices() and hasEdge(), the intended meanings of which should be obvious. The class QueueX can be any class that implements the Queue interface shown above.

/* Given a directed graph and the ID v of a vertex in that graph 
** (the so-called "source" vertex), returns an array indicating,
** for each vertex w in the graph, the distance from v to w.
*/
public static int[] distancesFrom( DiGraph graph, int v ) {

   final int N = graph.numVertices();
   int[] dist = new int[N];
   for (int i = 0; i != N; i++) { dist[i] = -1; }
   dist[v] = 0;
   
   Queue<Integer> q = new QueueX<Integer>();
   q.enqueue(new Integer(v)); // insert source vertex onto queue

   while (!q.isEmpty()) {
      // grab a vertex x off the queue 
      int x = q.dequeue(); 

      // now explore from that vertex
      for (int y = 0; y != N; y++) {
         if (dist[y] == -1  &&  graph.hasEdge(x,y)) { 
            q.enqueue(y);             // y is newly discovered!
            dist[y] = dist[x] + 1;    // distance to y from v 
         }                            // is one more than from
      }                               // v to x
   }
   return dist;
}

As an exercise, imagine that we call the method distancesFrom() and pass to it a Digraph object representing the graph described by the adjacency matrix shown below and the number 5 (indicating that vertex 5 is to play the role of the "source" vertex). The matrix is to be interpreted as follows: a 1 in location (i,j) (row i, column j) indicates that there is an edge in the graph directed from vertex i to vertex j. A zero indicates the lack of such an edge.

To the right of the adjacency matrix we show the tree that is implicitly described by the sequence of vertex explorations and discoveries during execution of the method. The root node of the tree is the source vertex and, with respect to every parent-child pair of nodes in the tree, the parent is the vertex that was being explored when the child was discovered. Thus, every path in the tree starting at the root node corresponds to a shortest path in the original graph.

Adjacency MatrixBreadth-first
Search Tree
13 | 1 0 0 0 0 0 1 0 0 0  0  0  0  0
12 | 0 0 0 0 0 0 0 0 0 0  0  1  0  0
11 | 0 0 0 0 0 0 0 0 0 0  0  0  1  0
10 | 0 0 0 0 0 0 0 0 0 0  0  0  0  0
 9 | 0 0 0 1 0 0 1 1 0 0  0  0  0  0
 8 | 1 1 1 0 1 1 0 0 0 0  0  0  0  0
 7 | 1 0 0 1 0 0 1 0 0 1  0  0  0  0
 6 | 0 0 0 0 0 0 0 1 0 1  0  0  0  1
 5 | 0 0 1 0 0 0 0 0 1 0  0  0  0  0
 4 | 0 0 0 1 0 0 0 0 1 0  0  0  0  0
 3 | 0 1 0 0 1 0 0 1 1 1  0  0  0  0
 2 | 0 1 0 0 0 1 0 0 1 0  0  0  0  0
 1 | 0 0 1 1 0 0 0 0 1 0  0  0  0  0
 0 | 0 0 0 0 0 0 0 1 1 0  0  0  0  1
   +--------------------------------
     0 1 2 3 4 5 6 7 8 9 10 11 12 13
        5
       / \
      /   \
     /     \
    /       \
   2         8
   |        / \
   |       /   \
   |      /     \
   1     0       4
   |    / \
   |   /   \
   3  7    13
   |  |
   |  |
   9  6

History of Queue and
Contents of dist[]
      +---+---+---+---+---+---+---+---+---+---+---+
queue | 5 | 2 | 8 | 1 | 0 | 4 | 3 | 7 |13 | 9 | 6 |
      +---+---+---+---+---+---+---+---+---+---+---+
          
enq.    1   3   4   6   8   9  11  13  14  17  19
deq.    2   5   7  10  12  15  16  18  20  21  22 


     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
dist | 2 | 2 | 1 | 3 | 2 | 0 | 4 | 3 | 1 | 4 |-1 |-1 |-1 | 3 |
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
       0   1   2   3   4   5   6   7   8   9  10  11   12  13

In the figure above, we show the history of the queue during execution of the method, as well as the array that would be returned by the method. What the figure shows is that the vertices were placed into the queue in this order: 5, 2, 8, 1, 0, etc., etc. But what it also shows is the order in which enqueue and dequeue operations were carried out. In the given graph, eleven vertices are reachable from the source vertex; thus, a total of 22 enqueues and dequeues were performed (because each reachable vertex was both placed onto the queue and later removed from it). The numbers in the rows marked enq. and deq. indicate the order in which the operations occurred. For example, the 6th operation to occur was to enqueue vertex 1, while the 10th was to dequeue that same vertex.

Somewhat redundantly, here is a high-level explanation of why the algorithm works as intended.

  1. Any vertex reachable from the source is eventually discovered and later explored.
  2. If vertices u and v are such that d(u) < d(v), then u will be discovered before v. Consequently, u will be placed on the queue before v is, and hence u will be explored before v is.
  3. If a vertex x has been discovered, then the value occupying dist[x] is equal to d(x) (x's distance from the source vertex).
  4. Discovery of a vertex y via exploration from a vertex x guarantees that d(y) = d(x) + 1. From the above two items, it follows that, upon discovering y, we are justified in storing dist[x] + 1 into dist[y]). (In fact, we must do so in order to maintain the truth of the previous item!)

The key to achieving these properties is the use of the queue to "schedule" the explorations of vertices. A more detailed proof of the algorithm's correctness can be found in the appendix, which is optional reading.


Implementation/Data Representation

Array-based

Naive Approach
Following an approach similar to that used in developing an array-based representation of a stack, let us propose the following array-based representation scheme for queues: for a queue containing k items, store (representations of) those items, in order from front to rear, in locations 0, 1, ..., k-1 of an array, which we will call items. (The values of array elements at locations k and beyond are irrelevant.) Rather than referring to the number of items as k, we shall use the variable numItems (just as in the Stack class).

For example, a queue containing the characters '$', 'd', 'z', '#', 'A', and '9' (written from front to rear) would be represented as follows:

        0   1   2   3   4   5   6   7               N-1
      +---+---+---+---+---+---+---+---+------------+---+
items |'$'|'d'|'z'|'#'|'A'|'9'|   |   |   ...      |   |
      +---+---+---+---+---+---+---+---+------------+---+
         +---+
numItems | 6 |
         +---+ 

Having proposed a representation scheme, let's determine whether it admits easy and efficient implementations of the standard queue operations. To implement frontOf() is easy: we simply return the value items[0]. As for enqueue(), it, too, is simple: we simply store the value to be inserted into items[numItems] and then increment numItems. (This assumes that numItems < N. Otherwise, before inserting the new value into the queue, we would have to create a new, longer array, copy the contents of items[] into it, and then, via assignment statement, make items refer to the new array.)

The dequeue() operation, however, does not work out so nicely. In order to delete the item at the front of the queue, while at the same time remaining faithful to our proposed representation scheme, we must shift all the elements in items[1..numItems-1] one location to the "left" and then decrement numItems. Shifting the elements could be accomplished as follows:

     for (int i = 0; i != numItems-1; i = i+1)
        { items[i] = items[i+1]; }  
Notice that execution of this requires time proportional to the number of items on the queue. In other words, it has linear running time. Intuition tells us that we ought to be able to do better.

What would happen if we stored the queue items in the array in order from rear to front, rather than front to rear? That is, suppose that we kept the rear item at location zero, the one preceding it (on the queue) at location one, etc., etc., and the front item at location numItems-1. Then to perform a dequeue() would be easy: simply decrement numItems. But now enqueue() would require that we shift items[0..numItems-1] to the "right" one position so as to make room (at location zero) for the value being inserted. This requires linear time, of course. So this variation doesn't help. For similar reasons, storing the items at the "end" of the array, rather than at the beginning, is no better.

WrapAround Approach
It appears that the decision to keep all the items in the queue stored in the "leftmost" (or "rightmost") segment of items[] may need to be relaxed in order to allow us to achieve a sub-linear running time for both enqueue() and dequeue(). So we propose the following: Let the segment of items[] holding the items on the queue "float" through the array, and use int variables frontLoc and rearLoc to indicate the boundaries of that segment. Suppose that frontLoc points directly to the element holding the item at the front of the queue and that rearLoc points to the element following the one holding the item at the rear. Taking the example from above, one possible representation would be

        0    ....  44  45  46  47  48  49  50  51    ....  N-1
      +---+-------+---+---+---+---+---+---+---+---+-------+---+
items |   |  .... |   |'$'|'d'|'z'|'#'|'A'|'9'|   |  .... |   |
      +---+-------+---+---+---+---+---+---+---+---+-------+---+
         +---+
frontLoc | 45|
         +---+
         +---+
 rearLoc | 51|
         +---+ 

With this representation, an enqueue() requires only that the new item be placed into items[rearLoc] and that then rearLoc be incremented. To perform dequeue() requires only that frontLoc be incremented. Thus, we have achieved simple and constant-time solutions for both of them. How about the other operations? Well, frontOf() is implemented simply by returning the value in items[frontLoc]. As for isEmpty(), it should be clear that the condition of a queue being empty corresponds, in our new representation scheme, to frontLoc == rearLoc.

We are not quite finished, however, because an interesting question arises: what happens when rearLoc is N and an enqueue() occurs? (This will occur the (N+1)-st time that enqueue() is invoked.) One possible answer would be to extend the length of items[]. But, except in the unlikely event that frontLoc == 0, this is rather wasteful, because the array segment items[0..frontLoc) is, logically speaking, empty. In order to make use of it, we may imagine that location 0 comes immediately after location N-1. In other words, we may view the array as being circular in layout, rather than linear. Mathematically, it means that our index calculations should be carried out "modulo N" —meaning that, when incrementing frontLoc or rearLoc (in performing a dequeue or enqueue, respectively), we should increment and then take the remainder of division by N. In other words, if incrementing frontLoc (or rearLoc) results in its having value N, its value should be set to zero!

To illustrate this "wrap-around" scheme, the following is another possible representation of the queue from above, under the assumption that N = 75.

        0   1   2      ....    70  71  72  73  74
      +---+---+---+-----------+---+---+---+---+---+
items |'A'|'9'|   |    ....   |   |'$'|'d'|'z'|'#'|
      +---+---+---+-----------+---+---+---+---+---+
         +---+
frontLoc | 71|
         +---+
         +---+
 rearLoc | 2 |
         +---+ 

Under this scheme, what do we do upon an attempt to insert (i.e., enqueue) if the array items is "full"? For that matter, how do we determine whether or not it is full? It would seem that the condition frontLoc == rearLoc corresponds to the array being full, because that would indicate that the array segment items[frontLoc..N) contains the items on the initial part of the queue and that items[0..frontLoc) contains the remaining items. But earlier —when we considered how to tell whether or not the queue was empty— we claimed that the same condition indicated an empty queue! So, which is it?! Well, it could be either one! That is, the condition frontLoc == rearLoc indicates that either the queue is empty or that the array holding its elements is full. In order to determine which it is, more information is needed!

A good way to provide the extra information is to introduce another instance variable, say numItems, whose value indicates the number of items currently occupying the queue. To maintain its value, we simply increment it each time an item is enqueued and decrement it each time an item is dequeued. In order to determine whether the queue is empty, it suffices to compare numItems against zero. Similarly, to determine whether items[] is full, it suffices to compare numItems against items.length. Employing this approach, one never need test for the condition frontLoc == rearLoc, as it will hold if and only if either numItems == 0 or numItems == items.length.

Note: An alternative approach for storing the extra information is to have an instance variable whose purpose is "to remember" which of the two mutation operations was applied most recently. If frontLoc == rearLoc and enqueue (respectively, dequeue) was most recently applied, the array is full (respectively, the queue is empty). End of note.

Having introduced numItems, we consider the relationship that exists between it, frontLoc, and rearLoc. Clearly, the value of rearLoc should always be precisely numItems positions "to the right" (using wraparound when necessary) of frontLoc. That is, an invariant of our representation scheme is

rearLoc = (frontLoc + numItems) % items.length

In other words, the value of rearLoc is calculable from the values of the other two. It follows that we don't need the variable rearLoc! In its place, we may use the right-hand side of the above equation.

Here is the complete implementation:

/** QueueViaArray.java
**  An instance of this class represents a queue capable of holding items
**  of the type specified by the client in instantiating the generic type
**  parameter T.  The implementation is based upon storing the queue items 
**  in an array.
**
**  Author: R. McCloskey
**  Date: September 2020
*/

public class QueueViaArray implements Queue {

   // symbolic constant
   // -----------------

   private static final int INIT_CAPACITY_DEFAULT  = 8;


   // instance variables
   // ------------------

   protected int numItems;  // # items occupying the queue
   protected T[] items;     // holds (references to) the items on the queue
   protected int frontLoc;  // location in items[] where front item is


   // constructors
   // ------------

   /* Establishes this queue to be empty.
   */
   public QueueViaArray(int initCapacity) {
      numItems = 0;
      items = (T[])(new Object[initCapacity]);
      frontLoc = 0;
   }

   public QueueViaArray() { this( INIT_CAPACITY_DEFAULT); }


   // observers
   // ---------

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

   public boolean isFull() { return false; }

   public int sizeOf() { return numItems; }

   public T frontOf() { return items[frontLoc]; }

   /* Returns a list of the (images of the) items in the queue,
   ** going from front to rear, separated by spaces and enclosed
   ** in square brackets.
   ** Example:  "[ cat in the hat ]"
   */
   public String toString() {
      StringBuilder result = new StringBuilder("[ ");
      for (int i=0; i != numItems; i++) {
         result.append(items[locOf(i)] + " ");
      }
      return result.append(']').toString();
   }


   /* Returns the location, within items[], at which the k-th element
   ** on the queue (counting from zero starting at the front) is stored.
   ** pre: 0 <= k < sizeOf()
   */
   protected int locOf(int k) { return (frontLoc + k) % items.length; }


   // mutators
   // --------

   public void clear() {
      // This loop is not necessary, but it could aid the garbage collector.
      for (int i=0; i != numItems; i++) {
         items[locOf(i)] = null;
      }
      numItems = 0;
      frontLoc = 0;   // Not necessary
   }

   public void enqueue( T item ) {
      if (numItems == items.length) {
         // items[] is full, so double its length.
         adjustArrayLength(2 * items.length);
      } 
      items[locOf(numItems)] = item;
      numItems = numItems + 1;
   }


   public T dequeue() {
      T result = items[frontLoc];
      items[frontLoc] = null;       // to aid garbage collection
      numItems = numItems - 1;
      frontLoc = (frontLoc + 1) % items.length;

      if (items.length >= 2 * INIT_CAPACITY_DEFAULT && 
          items.length >= 4 * numItems)
      {
         // The length of items[] is at least twice the default initial 
         // capacity and at least four times the queue's size, so cut its
         // length in half.
         adjustArrayLength(items.length / 2);
      }
      return result;
   }


   // private  utility
   // ----------------

   /* Transfers the queue elements into a new array of the specified
   ** length, adjusting the values of the instance variables 'items'
   ** and 'frontLoc' appropriately.
   ** pre: numItems <= newLength 
   */
   private void adjustArrayLength(int newLength) {
      T[] newItems = (T[])(new Object[newLength]);
      for (int i = 0; i != numItems; i++) {
         newItems[i] = items[locOf(i)];
      }
      items = newItems;
      frontLoc = 0;
   }

} 

Reference-Based Implementation/Data Representation

One of the less attractive features of using an array as the basis upon which to represent a queue is that, when the queue's size becomes "incompatible" with the size of the array (i.e., when either the queue has become too large to fit into the array or it has become so small that a significant portion of the array is unused), it becomes necessary/wise to create a new, differently-sized array and to copy all the relevant data into it from the old array. In order to ensure that this "size-change" operation does not dominate the time required to process queue operations, the new array's size should be significantly different from the old one. (In our implementation, the new array is made to be either double or half the size of the old one.)

Hence, although the abstract queue structure is growing and shrinking in small increments, the underlying structure used to represent it is growing and shrinking (occasionally) in large increments.

A concrete representation that makes use of references, rather than an array, can conveniently grow and shrink incrementally, just like the abstract structure that it represents.

The idea is to make use of a (generic) class that provides one-directional linking capabilities. We shall call this class Link1<T>. An object of this class can be depicted as

  +---+---+
  | * | *-+----> points to a Link1<T> object
  +-+-+---+
    |
    |
    v
points to an object of type T

That is, a Link1<T> object contains a reference to an object (of type T) and a reference to another object of type Link1<T>. An implementation of this class is as follows:

/** An instance of this class contains a reference to an object of the
**  specified type T (the generic type parameter) and a reference to
**  an object of the same kind (i.e., Link1).  The idea is that objects
**  of this class can be used as building blocks to form one-directional 
**  linked structures (i.e., one-way lists).
*/
public class Link1<T> { 

   // instance variables 
   // ------------------

   public T item;
   public Link1<T> next;

 
   // constructors
   // ------------

   public Link1(T item, Link1<T> next) { 
      this.item = item; this.next = next;
   }

   public Link1(T item) { this(item, null); }

   public Link1() { this(null, null); }

}

Using Link1 as a basis, we can represent the queue containing COW, CAT, DOG, BUG, and ANT objects as follows:

+-----+---+     +-----+---+     +-----+---+     +-----+---+     +-----+---+
| COW | x-+---->| CAT | x-+---->| DOG | x-+---->| BUG | x-+---->| ANT | x-+--!
+-----+---+     +-----+---+     +-----+---+     +-----+---+     +-----+---+
     ^                                                              ^
     |                                                              |
     |                                                              |
   +-+-+                                                          +-+-+
   | x |                                                          | x |
   +---+                                                          +---+
   front                                                          rear

For simplicity, we have simply written each Link1 object's animal name inside (the box representing) its first field. In reality, each such field is a reference (i.e., pointer) to the corresponding animal object.

Following this approach, here is the queue class that we derive:

/** An instance of this class represents a queue capable of holding items
**  of the specified type T (the generic type parameter).
**  The underlying implementation makes use of a linked structure of
**  objects arising from the Link1 class.
**
**  Author: R. McCloskey
**  Date: January 2020
*/
public class QueueViaLink1<T> implements Queue<T> {

   // instance variables
   // ------------------

   protected Link1<T> front; // reference to object holding front item on queue 
   protected Link1<T> rear;  // reference to object holding rear item on queue
   private int numItems;    // # of items on the queue


   // constructors
   // ------------

   /* Establishes this queue as being empty.
   */
   public QueueViaLink1() { clear(); }


   // observers
   // ---------

   public int sizeOf() { return numItems; }

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

   public boolean isFull() { return false; }

   public T frontOf() { return front.item; }

   /* Returns a string, beginning and ending with square brackets, containing
   ** the (images of the) items on this queue, going from front to rear,
   ** separated by spaces.
   */
   @Override
   public String toString() {
      Link1 pntr = front;
      StringBuilder result = new StringBuilder("[ ");
      for (int i=0; i != numItems; i++) {
         result.append(pntr.item + " ");
         pntr = pntr.next;
      }
      return result.append(']').toString();
   }


   // mutators
   // --------

   public void clear() { front = null; rear = null; numItems = 0; }

   public void enqueue( T item ) { 
      Link1<T> newRear = new Link1(item, null);
      if (isEmpty())
         { front = newRear; }
      else
         { rear.next = newRear; }
      rear = newRear;
      numItems = numItems + 1;
   }

   public T dequeue() { 
      T result = front.item;
      front = front.next; 
      numItems = numItems - 1;
      return result;
   }

}

A slightly different approach, which is more clever but not really any better, is to use a circular chain of Link1 objects. Here, the class implementing queues needs only a single variable, which is a reference to the Link1 object corresponding to the rear of the queue. As the chain is circular, this object includes a reference to the front of the queue, so there is no need for the queue class to include an instance variable pointing to the front of the queue. (Assuming that the instance variable that refers to the rear is called rear, a reference to the front of the queue is obtained using the expression rear.next.) Here is a picture depicting the situation:

     +---------<--------------------------<---------------------<-------+
     |                                                                  |
     v                                                                  |
+-----+---+     +-----+---+     +-----+---+     +-----+---+     +-----+-+-+
| COW | *-+---->| CAT | *-+---->| DOG | *-+---->| BUG | *-+---->| ANT | * |
+-----+---+     +-----+---+     +-----+---+     +-----+---+     +-----+---+
                                                                    ^
                                                                    |
                                                                    |
                                                                  +-+-+
                                                                  | * |
                                                                  +---+
                                                                  rear

We leave it as an exercise for the reader to modify the QueueViaLink1 class above to make use of this idea.


Appendix: Proof of Correctness of the Algorithm for the Uniform-cost Single Source Shortest Paths Problem

First we prove a few graph-theoretic results that will come in handy in the proof of the algorithm's correctness. We assume that G is a directed graph whose set of edges is E. Recall that d(x) is the distance from the source vertex, v, to vertex x, meaning the length of any shortest path starting at v and ending at x.

Lemma 1: Let (x,y) be in E and suppose that there is a path in G from v to x. Then d(y)d(x) + 1.

Proof: Take any shortest path from v to x (which, by definition of d, must have length d(x)) and extend it by the edge (x,y). The resulting path from v to y has length d(x) + 1; hence, the shortest path from v to y is of that length or less, which is to say that d(y)d(x) + 1.

Lemma 2: Let d(y) = k, where k > 0.
(a) Then there exists a vertex x for which d(x) = k-1 and (x,y) is in E.
(b) There is no vertex x' for which d(x') < k-1 and (x',y) is in E.

Proof: Part (b) follows from Lemma 1. As for Part (a), let P = w0, w1, ..., wk-1, wk (with w0 = v and wk = y) be a shortest path in G from v to y. Letting x = wk-1, we have that (x,y) is in E. It remains to show that d(x) = k-1. The prefix of P of length k-1 is a path from v to x; hence d(x) ≤ k-1. It remains only to show that d(x) > k-2. Suppose, to the contrary, that d(x) = j, where j ≤ k-2. But then, by Lemma 1, we would have d(y) ≤ j+1 ≤ k-1, contradicting the assumption d(y) = k.

Here, once again, is the algorithm:

/* Given a directed graph and the ID v of a vertex in that graph 
** (the so-called "source" vertex), returns an array indicating,
** for each vertex w in the graph, the distance from v to w.
*/
public static int[] distancesFrom( DiGraph graph, int v ) {

   final int N = graph.numVertices();
   int[] dist = new int[N];
   for (int i = 0; i != N; i++) { dist[i] = -1; }
   dist[v] = 0;
   
   Queue<Integer> q = new QueueX<Integer>();
   q.enqueue(new Integer(v)); // insert source vertex onto queue

   while (!q.isEmpty()) {
      // grab a vertex x off the queue 
      int x = q.dequeue(); 

      // now explore from that vertex
      for (int y = 0; y != N; y++) {
         if (dist[y] == -1  &&  graph.hasEdge(x,y)) { 
            q.enqueue(y);             // y is newly discovered!
            dist[y] = dist[x] + 1;    // distance to y from v 
         }                            // is one more than from
      }                               // v to x
   }
   return dist;
}

Lemma 3: Following initialization of every element of dist[] to −1, every assignment to an element of that array changes its value from −1 to some natural number.

Proof: The proof is by induction on the number of assignments made to elements in dist[] following initialization. The first one clearly changes dist[v] from −1 to 0. Every subsequent assignment to an element of dist[] occurs inside the loop. By inspection of the code, it is clear that assignment to an element cannot occur unless its value is −1. Also, as the value it is given is one more than that of another array element (whose value, the induction hypothesis tells us, must be either −1 or a natural number, depending upon whether it was ever changed), that value must be a natural number.

Corollary 3.1: Following initialization, no element of dist[] is the target of more than one assignment.

Corollary 3.2: No vertex in G is discovered (i.e., placed on the queue) more than once.

Proof: Inspection of the algorithm indicates that each discovery of a vertex is immediately followed by an assignment to the corresponding element of dist[]. If a vertex were discovered more than once, the corresponding array element would be the target of more than one assignment, contradicting Corollary 3.1.

Theorem: Let y be a vertex reachable from v, and let d(y) = k. Then

  1. y will be discovered (i.e., placed on the queue) before any vertex at distance greater than k is discovered, and
  2. Upon discovery of y, the value placed into dist[y] is d(y) (i.e., k), and, thereafter, dist[y] will never change.

Proof: The proof is by mathematical induction on k.

Basis: k = 0. As the only vertex at distance zero from v is v itself, y must be v. As v is the first vertex placed on the queue, (1) clearly holds. Also, zero is placed into dist[v] prior to the loop, which, applying Corollary 3.1, gives us (2).

Induction Step: Let k > 0 and assume, as an induction hypothesis, that the theorem holds for all k' < k. According to Lemma 2, among the vertices possessing an edge directed to y, none is at distance less than k-1 from v, but there is at least one at distance k-1 from v. Let x be the first such vertex to be discovered. By (2), k-1 will be placed in dist[x] when x is discovered and will remain there until termination of the program. As vertices are explored in the same order as they are discovered (because the algorithm employs a queue for "scheduling" these events), every vertex explored before x (according to (1) of the induction hypothesis) is either at distance less than k-1 from v or at distance k-1 but having no edge to y. By Lemma 2, none of the vertices at distance less than k-1 have edges to y. Hence, y will be discovered during exploration of x. Because (by (2) of the induction hypothesis) the value of dist[x] is k-1 at the time that x is explored, from inspection of the program it follows that k-1 + 1 (i.e., k) is placed into dist[y], satisfying the first part of (2). The second part of (2) follows from Corollary 3.1.

As for (1), suppose, to the contrary, that some vertex z satisfying d(z) > k were discovered before y. By Lemma 2, such a discovery could only happen during exploration of a vertex u satisfying d(u) ≥ k. For z to be discovered before y would require that u be explored before y, which, by the use of the queue for scheduling explorations, means that z would have been discovered before y. But this contradicts (1) of the induction hypothesis.