Still Under Construction
The Concept
In Great Britain, 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
data types).
Often, observers of a queue are limited to detecting whether or not it is
empty (or perhaps even its size) and, if not, to seeing the item at the front.
Queues are analogous to stacks in that there are two observer operations
(one to determine if the queue is empty and the other to examine the item at
the front) and two mutators (one for insertion and one for deletion).
Here is a Java interface for the queue ADT:
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.
(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 length of a shortest path from vertex v to vertex
z.
(If there is no path from v to z, dist[z]
should end up containing, say, -1.)
Here, the length of a path is defined to be the number of edges appearing
in it.
(The term "uniform cost" refers to the fact that we assume that
traversing one edge costs the same as traversing any other.
If edges are allowed to have different costs associated with them,
the problem becomes more difficult to solve.)
The following is a Java method that solves the problem.
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 having the
instance methods that it invokes.
The class QueueX can be any class that implements the Queue
interface shown above.
As an exercise, apply the above algorithm to the graph given by the
following adjacency matrix and using vertex 5 as the source:
The main concepts underlying the algorithm are those of
exploring from a vertex and discovering a vertex.
In doing the former, we sometimes achieve the latter.
To explore a vertex, we examine its outgoing edges.
If the edge being examined goes to an undiscovered vertex,
that vertex has just been discovered!
Any vertex that gets discovered is later explored. The key to why
the algorithm works correctly is that
The remainder of this section is optional reading.
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.
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.
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
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.
Naive Approach
For example, a queue containing the characters '$', 'd', 'z', '#', 'A',
and '9' (written from front to rear) would be represented as follows:
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
contents[0]. As for enqueue(), it, too, is simple:
we simply store the value to be inserted into contents[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 contents into it, and then, via assignment
statement, make contents 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 contents[1..numItems-1] one location to
the "left" and then decrement numItems. Shifting the elements
could be accomplished as follows:
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 contents[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
With this representation, an enqueue() requires only that
the new item be placed into contents[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 contents[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 contents[]. But, except in the unlikely
event that frontLoc == 0, this is rather wasteful, because
the array segment contents[0..frontLoc-1] 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.
Under this scheme, what do we do upon an attempt to insert (i.e., enqueue)
if the array contents 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
contents[frontLoc..N-1] contains the items on the initial part of
the queue and that contents[0..frontLoc-1] 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 items 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 contents[] is full, it suffices
to compare numItems against contents.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 = contents.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
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:
Hence, although the abstract queue structure is growing and shrinking
in small increments, the underlying structure used to represent it is
growing and shrinking 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
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:
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. Here is a picture depicting the situation:
Applications
Array-based
Implementation/Data Representation
Reference-based
Implementation/Data Representation
The Concept
public interface Queue<T> {
/* <<<<< o b s e r v e r s >>>>> */
/* pre: none
post: returns true if the queue is empty, false otherwise
*/
public boolean isEmpty();
/* pre: !this.isEmpty()
post: returns item at front of the queue
*/
public T frontOf();
/* <<<<< m u t a t o r s >>>>> */
/* pre: none
post: item has been placed at the rear of the queue
*/
public void enqueue( T item );
/* pre: !this.isEmpty()
post: item at front of the queue has been removed
*/
public void dequeue();
}
Applications
public static int[] shortestDistances( 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 the queue
while (!q.isEmpty()) {
/* grab a vertex off the queue */
int x = q.frontOf(); // Integer/int conversion is automatic in Java5.0
q.dequeue();
/* now explore from that vertex */
for (int y = 0; y != N; y++) {
if (dist[y] == -1 && graph.hasEdge(x,y)) {
q.enqueue(new Integer(y)); // y is newly discovered!
dist[y] = dist[x] + 1; // distance to y is one more than to x
}
}
}
return dist;
}
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
The key to achieving these properties is the use of the queue to
"schedule" the explorations of vertices.
(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.
Implementation/Data Representation
Array-based
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
contents. (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).
0 1 2 3 4 5 6 7 N-1
+---+---+---+---+---+---+---+---+------------+---+
contents |'$'|'d'|'z'|'#'|'A'|'9'| | | ... | |
+---+---+---+---+---+---+---+---+------------+---+
+---+
numItems | 6 |
+---+
for (int i = 0; i != numItems-1; i = i+1)
{ contents[i] = contents[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.
It appears that the decision to keep all the items in the queue stored
in the "leftmost" (or "rightmost") segment of contents[] 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 contents[] 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
+---+-------+---+---+---+---+---+---+---+---+-------+---+
contents | | .... | |'$'|'d'|'z'|'#'|'A'|'9'| | .... | |
+---+-------+---+---+---+---+---+---+---+---+-------+---+
+---+
frontLoc | 45|
+---+
+---+
rearLoc | 51|
+---+
0 1 2 .... 70 71 72 73 74
+---+---+---+-----------+---+---+---+---+---+
contents |'A'|'9'| | .... | |'$'|'d'|'z'|'#'|
+---+---+---+-----------+---+---+---+---+---+
+---+
frontLoc | 71|
+---+
+---+
rearLoc | 2 |
+---+
public class QueueViaArray<T> implements Queue<T> {
/* P R I V A T E */
private int frontLoc;
private int numItems;
private T[] contents;
private static final int DEFAULT_INIT_LEN = 8;
/* <<<<< c o n s t r u t o r s >>>>> */
public QueueViaArray() {
numItems = 0; frontLoc = 0;;
contents = (T[])(new Object[ DEFAULT_INIT_LEN ]);
}
/* <<<<< o b s e r v e r s >>>>> */
/* pre: none
post: returns true if the queue is empty, false otherwise
*/
public boolean isEmpty() { return numItems == 0; }
/* pre: !this.isEmpty()
post: returns item at front of the queue
*/
public T frontOf() { return contents[frontLoc]; }
/* <<<<< m u t a t o r s >>>>> */
/* pre: none
post: queue has been modified by placing item at its rear
*/
public void enqueue( T item ) {
// if conents[] is full, double its length
if (numItems == contents.length) {
int newLength = 2 * contents.length;
T[] temp = (T[])(new Object[newLength]);
for (int i=0; i != numItems; i = i+1)
{ temp[i] = contents[(frontLoc + i) % contents.length]; }
contents = temp;
frontLoc = 0;
}
contents[(frontLoc + numItems) % contents.length] = item;
numItems = numItems + 1;
}
/* pre: !this.isEmpty()
post: item at front of the queue has been removed
*/
public void dequeue() {
frontLoc = (frontLoc + 1) % contents.length;
numItems = numItems - 1;
if (contents.length >= 2 * DEFAULT_INIT_LEN) {
if (numItems < contents.length / 4) {
int newLength = contents.length / 2;
T[] temp = (T[])(new Object[newLength]);
for (int i=0; i != numItems; i = i+1)
{ temp[i] = contents[(frontLoc + i) % contents.length]; }
contents = temp;
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.)
+---+---+
| x | x-+----> 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:
public class Link1<T> {
protected T item;
protected Link1<T> next;
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); }
public T getItem() { return this.item; }
public Link1<T> getNext() { return this.next; }
public void setItem(T newItem) { this.item = newItem; }
public void setNext(T newNext) { this.next = newNext; }
}
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
/*
* Java class for queues; data representation is based upon use of the
* Link1 class. A queue is represented by references front and rear,
* which point to the first and last, respectively, "nodes" in a chain
* of Link1 objects.
*/
import Link1;
class QueueViaLink1<T> implements Queue<T> {
Link1<T> front; // points to front of queue
Link1<T> rear; // points to rear of queue
/* c o n s t r u c t o r */
/* pre: none
post: isEmpty()
*/
public QueueViaLink1() {
front = null;
rear = null;
}
/* o b s e r v e r s */
/* pre: none
post: returns true iff the queue is empty
*/
public boolean isEmpty() { return front == null; }
/* pre: !isEmpty()
post: object returned is that at front of the queue
*/
public T frontOf() { return front.getItem(); }
/* m u t a t o r s */
/* pre: !isEmpty()
post: queue has been modified so that item at front has been removed
*/
public void dequeue() { front = front.getNext(); }
/* pre: none
post: item will have been placed at the rear of 'this'
*/
public void enqueue(T item) {
Link1<T> newRear = new Link1<T>(item, null);
if (isEmpty())
{ front = newRear; }
else
{ rear.setNext(newRear); }
rear = newRear;
}
+---------<--------------------------<---------------------<-------+
| |
v |
+-----+---+ +-----+---+ +-----+---+ +-----+---+ +-----+-+-+
| COW | x-+---->| CAT | x-+---->| DOG | x-+---->| BUG | x-+---->| ANT | x |
+-----+---+ +-----+---+ +-----+---+ +-----+---+ +-----+---+
^
|
|
+-+-+
| x |
+---+
rear
Translating this approach to Java, we get
/*
Java class for queues; data representation is based upon use of the
Link1 class. A queue is represented by a circular chain of Link1
objects. A single pointer to rear node is sufficient to do every
operation in constant time.
*/
import Link1;
class QueueViaLink1Circ<T> implements Queue<T> {
Link1<T> rear; // points to rear of queue
/* c o n s t r u c t o r */
/* pre: none
post: isEmpty()
*/
public QueueViaLink1Circ() { rear = null; }
/* o b s e r v e r s */
/* pre: none
post: returns true iff queue is empty
*/
public boolean isEmpty() { return rear == null; }
/* pre: !isEmpty()
post: object returned is that at front of the queue
*/
public T frontOf() { return (rear.getNext()).getItem(); }
/* m u t a t o r s */
/* pre: !isEmpty()
post: queue has been modified so that item at front has been removed
*/
public void dequeue() {
Link1<T> front = rear.getNext();
if (rear == front) // queue has only one item
{ rear = null; }
else // queue has two or more items
{ rear.setNext(front.getNext()); }
}
/* pre: none
post: item will have been placed at the rear of the queue
*/
public void enqueue(Object item) {
Link1<T> newNode = new Link1<T>(item, null);
if ( isEmpty() ) {
newNode.setNext(newNode);
}
else {
newNode.setNext( rear.getNext() );
rear.setNext(newNode);
}
rear = newNode;
}
}