Queues

Course links

Queues as an abstract data type

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:

Queues in Scheme

The implementation of queues in Scheme is somewhat trickier than the implementation of stacks. An implementation in which we simply store the queue's elements in a list, in order of arrival, is relatively inefficient when the queues grow long. Since each new item is added at the end of the list, we'd have to cdr down to the end of the list, add the element, and then cons all the previous elements back on again. This is markedly slower than the :push! and :pop! operations on stacks, which can both operate at the "free end" of the list (that is, its beginning).

In this implementation, we'll use two lists to store the elements of a single queue. One list, fore, will hold the part of the queue from which elements are dequeued; when we want the next element, we'll simply apply car to fore to get it. The other list, aft, will hold the part of the queue to which elements are enqueued; when we want to add an element to the queue, we'll simply apply cons to the new value and aft. The elements of aft will be in reverse order with respect to the queue -- the most recently added items will be near the beginning of the aft list.

Of course, if values are added only to aft and extracted only to fore, we'll encounter some cases in which fore is null when we want to dequeue the next item. Indeed, how do values ever get into the fore list?

To resolve this difficulty, we'll implement the :dequeue! and :front messages so that, when either of them is sent and fore is null, that null list is replaced by the reverse of the current aft list! At the same time, we'll replace the old aft list with a null list, so that we don't duplicate the queue's elements. Once the reversed aft list becomes the new fore list, we can proceed to dequeue a value from it or to inspect its first element, as appropriate.

If there are a lot of elements in the aft list, reversing it can take a long time in comparison to the other, simple operations here. However, if the queue is usually long, the reversal won't have to be done often. Since each queue element makes the transition from the aft list to the fore list once and only once, the probability (over a long series of queue operations) that a reversal will be needed on any given :dequeue or :front message is inversely proportional to the average length of the queue.

Another operation that could be slow if we implement it badly is :size. The size of a queue is the sum of the lengths of the fore and aft lists, but computing those lengths by traversing both lists isn't very efficient. Here we'll meet this difficulty by storing the number of elements in the queue in a third static variable, size, and maintaining the invariant that size is always equal to the sum of the length of fore and the length of aft. We can preserve this invariant by incrementing size at the beginning of each enqueuing operation and decrementing it at the beginning of each dequeuing.

Here is the constructor for queue objects:

;;; make-queue: construct and return a queue

;; Givens:
;;   None

;; Result:
;;   QUEUE, a procedure

;; Preconditions:
;;   None.

;; Postconditions:
;;   (1) QUEUE is initially empty.
;;   (2) When QUEUE is invoked with the argument :EMPTY?, it
;;       reports whether it is empty.
;;   (3) When QUEUE has been invoked with the first argument
;;       :ENQUEUE! and a second argument, say NEW-VALUE,
;;       NEW-VALUE is at the rear, and all other values in
;;       the queue are in front of it, in order by time of
;;       enqueuing.
;;   (4) When QUEUE has been invoked with the argument
;;       :DEQUEUE!, it returns the longest-enqueued value,
;;       the one at the front, and retains all other values,
;;       in order by time of enqueuing.
;;   (5) When QUEUE is invoked with the argument :FRONT, it
;;       returns the value at the front (available for
;;       dequeuing).
;;   (6) When QUEUE is invoked with the argument :SIZE, it
;;       returns the number of values in the queue.
;;   (7) It is an error to give QUEUE any other argument
;;       when invoking it.
;;   (8) When QUEUE is invoked with the first argument
;;       :EMPTY?, :DEQUEUE!, :FRONT, or :SIZE, it is an
;;       error to give it two or more arguments.
;;   (9) When QUEUE is invoked with the first argument
;;       :ENQUEUE!, it is an error to give it only one
;;       argument or three or more.
;;  (10) It is an error to invoke QUEUE with the argument
;;       :FRONT or the argument :DEQUEUE! when QUEUE is
;;       empty.

(define make-queue
  (lambda ()
    (let ((fore (make-vector 1 '()))
          (aft (make-vector 1 '()))
          (size (make-vector 1 0)))
      (lambda (message . arguments)
        (cond ((eq? message ':empty?)
               (if (null? arguments)
                   (zero? (vector-ref size 0))
                   (error (string-append
                            "queue:empty?: no "
                            "arguments are required"))))

              ((eq? message ':enqueue!)
               (cond ((null? arguments)
                      (error (string-append
                               "queue:enqueue!: an "
                               "argument is required")))
                     ((not (null? (cdr arguments)))
                      (error (string-append
                               "queue:enqueue!: no "
                               "more than one argument "
                               "is required")))
                     (else
                      (vector-set! size
                                   0
                                   (+ (vector-ref size 0)
                                      1))
                      (vector-set! aft
                                   0
                                   (cons (car arguments)
                                         (vector-ref aft
                                                     0))))))

              ((eq? message ':dequeue!)
               (cond ((not (null? arguments))
                      (error (string-append
                               "queue:dequeue!: no "
                               "argument is required")))
                     ((zero? (vector-ref size 0))
                      (error (string-append
                               "queue:dequeue!: "
                               "the queue is empty")))
                     (else
                      (vector-set! size
                                   0
                                   (- (vector-ref size 0)
                                      1))
                      (if (null? (vector-ref fore 0))
                          (begin
                            (vector-set! fore
                                         0
                                         (reverse
                                           (vector-ref aft
                                                       0)))
                            (vector-set! aft 0 '())))
                      (let ((dequeued (car (vector-ref fore
                                                       0))))
                        (vector-set! fore
                                     0
                                     (cdr (vector-ref fore
                                                      0)))
                        dequeued))))

              ((eq? message ':front)
               (cond ((not (null? arguments))
                      (error (string-append
                               "queue:front: no "
                               "argument is required")))
                     ((zero? (vector-ref size 0))
                      (error (string-append
                               "queue:front: the "
                               "queue is empty")))
                     (else
                      (if (null? (vector-ref fore 0))
                          (begin
                            (vector-set! fore
                                         0
                                         (reverse
                                           (vector-ref aft
                                                       0)))
                            (vector-set! aft 0 '())))
                      (car (vector-ref fore 0)))))

              ((eq? message ':size)
               (if (null? arguments)
                   (vector-ref size 0)
                   (error (string-append
                            "queue:size: "
                            "no argument is required"))))

              (else
               (error 'queue "unrecognized message")))))))

Acknowledgements

Professor Henry Walker wrote the original description of the queue abstract data type in this handout. The author cited below wrote the account of the fore-and-aft implementation of queues and the Scheme code.