Sometimes we want a data structure that provides access to its elements on ``first-in, first-out'' basis, rather than the ``last-in, first-out'' constraint that a stack imposes. (For example, it might be prudent to treat that pile of unpaid bills a little differently, adding new elements at the bottom of the pile rather than the top, Paying off the most recent bill first, as in a stack, can make one's other, older creditors a little testy.)
Such a structure is called a queue. Like a line of people waiting for some service, a queue acquires new elements at one end (the rear of the queue) and releases old elements at the other (the front). Here is the abstract data type definition for queues, with the conventional names for the operations:
create
Create a new, empty queue object.
empty
Determine whether the queue is empty; return true if it is and
false if it is not.
enqueue
Add a new element at the rear of a queue.
dequeue
Remove an element from the front of the queue and return it. (This
operation cannot be performed if the queue is empty.)
front
Return the element at the front of the queue (without removing it from the
queue). (Again, this operation cannot be performed if the queue is empty.)
size
Return the number of elements currently in the queue.
The implementation of queues in Scheme is somewhat trickier than the
implementation of stacks. Again, we'll keep the elements of the queue in a
list. However, it turns out that the enqueue operation can be
slightly faster if we represent an empty queue by a list containing one
element, a ``dummy header,'' and store the actual queue elements after this
header, oldest first. The dummy header is not inserted by enqueue
and cannot be removed by the dequeue. It is not there to provide a
value, but just to keep the list from becoming null, so that one can always
apply the set-cdr! procedure to it without first testing to
see whether it is null. The fact that the underlying list never becomes
completely null is an invariant of this implementation of queues.
The other novel feature of this implementation is that we'll actually be
accessing the list through three different fields, fore,
aft, and size. The fore field
always contains the entire list structure, beginning with the dummy header;
(cdr fore) is the list of the actual elements of the queue,
and (cadr fore) is the first real element of the queue (when
it is not empty). The aft field, on the other hand, is
always a one-element list; it contains the last element of the queue,
except when the queue is empty, in which case the aft field
contains the dummy header. The size field keeps track of the
number of elements in the queue, not including the dummy header. Again, it
is an invariant of this implementation that size is one less
than the length of the underlying list.
The following box-and-pointer diagram shows a queue into which the symbols
a, b, and c have been enqueued, in
that order:
Here is the constructor for queue objects:
(define make-queue
(lambda ()
(let* ((fore (list 'dummy-header))
(aft fore)
(size 0))
(lambda (message . arguments)
(cond ((eq? message ':empty?)
(zero? size))
((eq? message ':enqueue!)
(if (null? arguments)
(error "queue:enqueue!: an argument is required")
(begin
; Attach a new cons cell behind the current aft
; element.
(set-cdr! aft (list (car arguments)))
; Advance AFT so that it is once more a list
; containing only the last element.
(set! aft (cdr aft))
; Increment SIZE.
(set! size (+ size 1)))))
((eq? message ':dequeue!)
(if (null? (cdr fore))
(error "queue:dequeue!: the queue is empty")
; Recover the first element of the queue (not including
; the dummy header).
(let ((removed (cadr fore)))
; Splice out the element to be dequeued.
(set-cdr! fore (cddr fore))
; If we just spliced out the last element of the
; queue, reset AFT so that it holds the dummy
; header.
(if (null? (cdr fore))
(set! aft fore))
; Decrement SIZE.
(set! size (- size 1))
removed)))
((eq? message ':front)
(if (null? (cdr fore))
(error "queue:front: the queue is empty")
(cadr fore)))
((eq? message ':size)
size)
(else (error 'queue "unrecognized message")))))))
Add to the queue an additional operation, print, activated by the
':print message, that displays each of the elements of the
queue on a separate line (without actually removing any of them from the
queue). Make sure not to print the dummy header.
Define and test a procedure named merge-queues that takes as
arguments two queues of real numbers, both of which must be in ascending
numerical order from front to rear, and returns a single queue containing
the elements of both of the given queues, again in ascending numerical
order.
Using deep recursion, write a procedure that creates an empty queue, then traverses a tree of symbols and puts each symbol that it encounters into the queue, and finally uses the print operation added in the previous exercise to display the contents of the queue.
This document is available on the World Wide Web as
http://www.cs.grinnell.edu/~stone/courses/scheme/queues.xhtml
created April 28, 1997
last revised May 1, 2000
Henry Walker (walker@cs.grinnell.edu) and John David Stone (stone@cs.grinnell.edu)