CPS222 Lecture: Queues and deques               - Last Revised 1/15/2013

Objectives:

1. To introduce the data structure queue.
2. To introduce STL support for queues
3. To show two ways of implementing the ADT queue - a circular array, and
   a linked list.
4. To introduce variants of queues - eg deque, priority queues.

Materials: 

1. Queue operations/axioms to project
2. My implementation of queues using arrays to project
3. My implementation of queues using linked lists to project

I. Introduction
-  ------------

   A. We have seen that one particularly useful form of sequential structure is
      the stack, which results from restricting our access to a sequential
      structure to those operating on the item most recently added.

   B. There is a second kind of restricted sequence that is also very
      useful.  This is the queue.  A queue is a linear structure in which we
      limit ourselves to the following basic operations:

      1. Create an empty structure
      2. Ask whether the structure is empty
      3. Insert a new item at the "rear" of the structure.  This operation
         goes by a variety of names in various books - our textbook author
         uses the term "enqueue"
      4. Examine the "front" item in the structure
      5. Remove the front item from the structure.  Again, this goes by
         a variety of names - our textbook author uses the term "dequeue".

   C. We can specify the operations of the ADT queue as follows:  (PROJECT)

      1. CREATE returns queue

         Preconditions - None
         Postcondition - Queue is empty

      2. EMPTY (queue) returns boolean

         Preconditions - None
         Postconditions - The result is true iff the queue is empty.

      3. ENQUEUE(item, queue) modifies the queue

         Preconditions - None
         Postcondition - Item is added to the rear of the queue

      4. DEQUEUE (queue) modifies the queue

         Precondition - Queue is not empty
         Postcondition - Front item is removed from the queue

      5. FRONT (queue) returns item

         Precondition - Queue is not empty
         Postconditions - The front item in the queue is returned,
                          but the queue is not affected.

      6. It is sometimes useful to also define an operation SIZE which
         gives the number of items in the queue

   D. The queue is of use whenever one has to model a waiting line. Indeed, the
      name queue is the British term for a waiting line.  At any time, the
      person at the front of the waiting line is the next to be served.  New
      arrivals are added at the end of the line.  

      1. Example: Consider the following series of operations

                CREATE
                ENQUEUE A
                ENQUEUE B
                ENQUEUE C
                DEQUEUE
                ENQUEUE D
                DEQUEUE
                What is the current value of FRONT?

        We can trace the behavior of these as follows:

                CREATE          (Empty)
                ENQUEUE A       A
                ENQUEUE B       A B
                ENQUEUE C       A B C
                DEQUEUE         B C
                ENQUEUE D       B C D
                DEQUEUE         C D
                So FRONT is C
 
      2. "Queue behavior" corresponds to the following axioms:

          For any item I and queue Q

          a. EMPTY(CREATE) ::= true
          b. EMPTY(ENQUEUE(I,Q)) ::= false
          c. FRONT(CREATE) ::= error
          d. FRONT(ENQUEUE(I,Q)) ::= if EMPTY(Q) then I 
                                     else FRONT(Q)
          e. DEQUEUE(CREATE) ::= error
          f. DEQUEUE(ENQUEUE(I,Q)) ::= if EMPTY(Q) then Q 
                                       else ENQUEUE(I,DEQUEUE(Q))
          g. SIZE(CREATE) ::= 0
          h. SIZE(ENQUEUE(I,Q)) ::= SIZE(Q) + 1
          i. SIZE(DEQUEUE(Q)) ::= SIZE(Q) - 1

          Example: the series of operations we just looked at could be
          analyzed axiomatically as follows:

          FRONT(DEQUEUE(ENQUEUE('D',DEQUEUE(ENQUEUE('C',
                ENQUEUE('B',ENQUEUE('A',CREATE)))))))

          We can apply axiom 'f' (non-empty variant) to the inner dequeue/
          enqueue sequence

          FRONT(DQUEUE(ENQUEUE('D',ENQUEUE('C',
                DEQUEUE(ENQUEUE('B',ENQUEUE('A',CREATE)))))))

          We can apply axiom 'f' twice - once to each dequeue/enqueue sequence. 
          Since in each case the queue being added to was not empty prior to 
          the enqueue, we use the not empty variant each time.  Thus, we have:

          FRONT(ENQUEUE('D',DEQUEUE(ENQUEUE('C',
                ENQUEUE('B',DEQUEUE(ENQUEUE('A',CREATE)))))))

          We can apply Axiom 'f' twice again.  In the outer case, we use
          the "not empty to begin with" variant, but in the inner case,
          we use the "empty to begin with variant"

          FRONT(ENQUEUE('D',ENQUEUE('C',DEQUEUE(ENQUEUE('B',CREATE)))))

          Now we apply Axiom 'f' - "empty to begin with" variant - again:

          FRONT(ENQUEUE('D',ENQUEUE('C',CREATE)))

          By Axiom 'd' - "not empty to begin with" variant  - we have:

          FRONT(ENQUEUE('C',CREATE))

          By axiom 'd' - "empty to begin with" variant - we end up with 'C' as 
          our result

   E. The following are some typical applications of queues:

      1. Queues are used extensively in multi-user operating systems for the 
         management of shared resources - eg peripheral devices such as 
         printers attached to a multi-user computer or a network can be 
         managed by a queue. When a user sends a file to the printer, it is 
         placed at the end of a queue.  When the printer finishes one file, it 
         goes to the queue to get the next file to print.  

      2. Queues are used extensively in computer simulations.  For example,
         suppose the state is trying to decide how to adjust the timing of
         traffic lights to optimize traffic flow through the center of a city.
         One approach is to try various patterns using a computer simulation in
         which car movements are modelled according to empirically determined
         probability distributions.  In such a simulation program, the line of
         cars waiting for a given light is modelled by a queue.

II. Implementing the ADT Queue
--  ------------ --- --- -----

   A. As was the case with stacks, the C++ STL includes a queue template,
      which can be instantiated to contain items of any desired type.

      1. #include <queue>

      2. Declare queue variables by:    queue < type > variable;

      3. Or declare a queue type by:     typedef queue < type > typename;

      4. The names of the operations are a bit unusual, though.  They include
         the following, where type is the type specified when the template
         is instantiated:

         a. Constructor

         b. bool empty()

         c. void push(type)     // Note the name!

         d. void pop()          // Note the name!

         e. type front()

       5. As was the case with stacks, it is useful to also look at how we
          could implement queues directly.

    B. When we first implemented the ADT stack, we used an array, then we looked
       at an alternate implementation using linked lists.  We will also do this
       with queues, and for the same reasons.

    C. Suppose we were to try to implement a queue using an array, in a way
       similar to the way we implemented stacks.  Clearly, we would not be able 
       to work with a single index, since while all stack operations are
       performed at one end (the top), queue operations are performed at both
       ends.  Thus, will need two indices: front and back.  

       1. Working with these, our queue implementation class would include:

                class queue
                {
                    ...

                    private:

                        int front;
                        int back;
                        sometype theArray [ somesize ];
                }
          
       2. Of course, we need to assign some interpretation to the indices front
          and back.  

          a. In the stack, top indicated the top item - i.e. the item which was 
             next to be popped.  Thus, it makes sense in the queue for front to 
             be the index of the the first item in the queue - i.e. the one 
             that is next to be removed.  

          b. Two interpretations are possible for back: it can either be the
             index of the LAST ITEM ADDED (as was true of top for the stack) or
             else the index of the NEXT SLOT TO BE FILLED.  (Notice that these
             two interpretations differ by one slot.)  Because the code is just
             a bit simpler, we will adopt the latter interpretation.

    D. Having assigned an interpretation to the indices, we can begin to
       develop our queue operations.

       1. To create an empty queue, we can initialize front to 0 and back to
          0.  (Note that there is not, of course, a front item as yet.  But
          the fact that back and front point to the same slot guarantees that
          the first item inserted will automatically become the front item,
          as desired.)

       2. To determine if the queue is empty, we need only ask if front == back.
          (I.e. if the slot where the front item is to be found is the next
          slot we are to fill, then we have no front item as yet, and the
          queue is empty.)

       3. To examine the front item in the queue Q, we look at theArray[front].
          (Provided, of course, the queue is not empty)

       4. To remove this item from the queue, we increment front.

       5. To insert an item in the queue, we put it in theArray[back], and then
          we increment back.  (Note the order!)

    E. Unfortunately, if we pursue this course of action long enough, we will 
       eventually have a situation in which back has gone past the end of 
       the array, but we may have free space between the beginning of the array
       and front which we can no longer use.  That is, the queue "migrates" 
       within the array - e.g. consider what happens as a result of the 
       following sequence of operations on the queue shown below:

        Initial:        A B C _ _ _     (3 free slots at end)
        Operations: Dequeue, Dequeue, Enqueue D, Enqueue E, Dequeue, Enqueue F, 
                    Dequeue, Dequeue
        Result:         _ _ _ _ _ F

       The array now contains 5 unused slots, but any attempt to insert a new
       item will fail because q.back has reached the maximum possible value.

    F. A partial solution to the problem of the migrating queue can be obtained
       by resetting both front and back pointers to their initial values
       whenever the queue becomes empty.  However, as the above example shows,
       this may not help.  A better approach is to use a circular
       implementation in which we conceive of the array we have allocated for
       storing the queue as wrapping around on itself:

                        _ _
                      _     _
                        _ _

       Thus, when we fill the last slot, the next slot we fill becomes 
       slot 0.

       1. Now, when we increment front or back, we use code like:

                front = (front + 1) % arraySize;
        or
                back  = (back  + 1) % arraySize;

       2. However, we have introduced a new problem!  In the straight array
          implementation, we had the possibility of front "catching up"
          with back.  In particular, front == back implied an empty
          queue.  But this is not necessarily the case with a circular queue.
          Not only can front catch up with back, but back can also catch
          up with front.  

          a. Consider a queue with 6 slots, as in the above examples.  If we 
             did six successive enqueues, back would again == front, but
             this time the meaning would not be that the queue is empty, but
             rather than it is FULL!

          b. back == front has two possible interpretations:
             
             - It could signal an empty queue: the front item in the queue
               is the next slot to fill - i.e. it hasn't been put there yet -
               i.e. there is no front item - i.e. the queue is empty.

             - It could signal a totally full queue: the next slot to fill
               is the front item - i.e. the next item to remove - i.e. an
               item must be removed to make room for the next item to be
               inserted - i.e. the queue is full.

       3. We can resolve the ambiguity in one of two ways:

          a. We can waste a slot in the queue, declaring the queue to be
             full whenever we would be inserting an item in the slot just
             before q.front.  This would prevent back from catching up
             with front.

          b. Or, we can use an additional field in our class.

             - We could use a bool field called (say) empty - true just when
               the queue is empty.

             - We could use an int field called (say) currentSize - which
               contains the number of elements in the queue

             Either solution would allow us to distinguish the two possible
             meanings of back == front.

   G. Having adopted the above conventions, we can implement the type queue
      using the following apprach:

      PROJECT EXAMPLE QUEUE IMPLEMENTATION - ARRAYS

   H. There are a several ways to implement queues using linked lists.

      1. When we represented a stack using a linked list, we used a single
         external pointer to the top item, with each item being linked to
         its successor - i.e. the one that is next to be popped..

      2. For much the same reason that we used two indices in our array
         implementation of queues, we could use two external pointers in a
         linked implementaiton - one to the front item and one to the rear- e.g.
         assume our queue contains Smith, Franklin, Jones, and Wilson in
         that order

        Rear
        o--------------------------------------------------
                __________      ____________    __________ \    __________
                | Smith  |      | Franklin |    | Jones  |  \-->| Wilson |
        Front   |        |      |          |    |        |      |        |
        o------>|      o-|----->|        o-|--->|      o-|----->|      o-|---
                ----------      ------------    ----------      ----------  |
                                                                          -----
                                                                           ---
         The code to insert a new node n becomes:

            if (front == NULL) 
                front = n;
            else
                back -> _link := n;
            back := n;

      3. We will not develop this method in detail, but instead will look at
         a more elegant solution that needs only one external pointer.

   I. Another way of implementing queues uses a circularly-linked list:

      1. In the linked lists we have considered thus far, a non-empty list has
         always had a "last" item, whose successor is NULL.  We can also
         conceive of a circular list, in which the last item contains a link
         back to the first.

      2. If we do this with a queue, we could have a single external pointer to 
         the rear item which would give us ready access to both ends of the
         queue.  Example:
                                        External Ptr -->
                                                         \
        --> [ Front ]--> [ Second ] --> [ Third ] --> ... --> [ Rear ] -->
        |_________________________________________________________________|

         a. Insertions are made between the rear item and the front item,
            and the external pointer is then reset to the newly inserted item -
            e.g. - the above after inserting a new node:

                                                External Ptr -->
                                                      Former    \
--> [ Front ]--> [ Second ] --> [ Third ] --> ... --> [ Rear ] --> [ Rear ] -->
|_____________________________________________________________________________|

         b. Removals are done by removing the item after the item pointed to
            by the external pointer, without altering the external pointer - 
            e.g. the original example after a remove:

                                External Ptr -->
                   Now           Now             \
             --> [ First ] --> [ Second ] --> ... --> [ Rear ] -->
            |_____________________________________________________|


      3. We have to recognize some special cases, though

         a. An empty queue is represented by a NULL  external pointer.

         b. A one-item queue is represented by an external pointer to a node
            pointing to itself.

         c. The enqueue algorithm needs to test for an empty queue - in which
            case it constructs a one-item queue instead of using the more
            general algorithm.

         d. The dequeue algorithm needs to test for a one item queue - in
            which case, it resets the external pointer to nil.

      PROJECT EXAMPLE QUEUE IMPLEMENTATION - LISTS

      (Note: the approach I am developing here includes the list
       implementation directly, rather than embedding a list in a wrapper
       that handles only the actual stack operations as in the book)

III. Variants of the Queue
---  -------- -- --- -----

   A. A deque - double ended queue - is a variant of the queue in which we
      allow all operations at both ends - i.e. we have:

                        CREATE
                        EMPTY
        INSERTF - add to front          INSERTR - add to rear
        REMOVEF - remove from front     REMOVER - remove from rear
        FRONT - examine the front item  REAR - examine the rear item

      1. Deques are of little direct interest, though one might be of
         use, say, in a supermarket simulation where the possibility of the
         rear customer giving up and leaving the line is considered.

      2. However, if one has an implementation for a deque he can implement
         both the stack operations and the queue operations as deque
         operations, by letting the front of the deque be the front of the
         queue and the rear of the dequeue be the rear of the queue and the
         top of the stack - i.e.

        stack operation         dequeue operation

        CREATE                  CREATED
        EMPTY                   EMPTY
        PUSH                    INSERTR
        POP                     REMOVER
        TOP                     REAR

        queue operation

        CREATE                  CREATE
        EMPTY                 EMPTY
        ENQUEUE                 INSERTR
        DEQUEUE                 REMOVEF
        FRONT                   FRONT

      3. This is, in fact, what the STL does - it implements a deque template
         (using a linked list of nodes, each of which can contain several
         items!) 

   B. A priority queue is a queue structure in which each item has a priority
      value.  When an item is inserted in such a queue, it is not inserted
      at the rear, but rather in front of all items having lower priority, and
      after all items of greater or equal priority.  (I.e. the queue is
      maintained as an ordered list in priority order.)  Such queues are very
      important in operating systems.

      1. This can be done by using a single queue with insertions possibly
         being done in the middle - in which case a linked implementation is
         much to be preferred over an array.  (We will discuss this later.)

      2. Alternately, if the priorities are discrete values, one can have
         multiple queues - one for each possible priority value:

         a. The ENQUEUE operation adds a new item to the rear of the queue for
            its priority.

         b. The FRONT and DEQUEUE operations choose the highest-priority non
            empty queue.

      3. Or, one can use a tree-like structure called a heap, which we will
         come to later in the course