CPSC 120 Lecture Notes, Wednesday, March 11, 1998

ASSIGNMENTS/ANNOUNCEMENTS

MOTIVATION FOR QUEUES

Often, you want to process things in a "first-in, first-out" (FIFO) manner. For instance, requests for some service are often handled this way (think about the check-out line at the grocery store). We talked last semester about queues of processes in an operating system waiting to get the CPU.

SPECIFYING THE QUEUE ADT

We can use the notion of an abstract state to specify a queue.
  1. A queue is modeled as a sequence of elements.
  2. Initially the state of the queue is the empty sequence.
  3. The effect of an enqueue(x) operation is to append x to the end of the sequence that represents the state of the queue. This operation returns nothing.
  4. The effect of a dequeue operation is to delete the first element of the sequence that represents the state of the queue. This operation returns the element that was deleted. If the queue is empty, it should return some kind of error indication.

As with a stack, one could instead specify a queue in terms of the allowable sequences of enqueue and dequeue operations (using some kind of "algebra").

For instance, consider the following sequences:

Other operations that you sometimes want to provide:

SOME APPLICATIONS OF QUEUES

The text discusses some applications of queues in operating systems:
  • Queues are also used in simulation programs. A simulation program is a program that mimics, or simulates, the behavior of some complicated real-world situation, such as

    These systems are typically too complicated to be modeled exactly mathematically, so instead, they are simulated: events take place in them according to some random number generator. For instance,

    Some of these situations are particularly well described using queues -- they are characterized by "things waiting in line". For instance,

    Part of the simulation of a queueing system will involve keeping queues of entities waiting for service.

    ANOTHER QUEUE APPLICATION: INFIX TO POSTFIX CONVERSION

    First, let's assume for simplicity that the infix expression is fully parenthesized, i.e., that there is a pair of parentheses around every operand, operator, operand triple.
    create an empty queue Q to hold the postfix expression
    create an empty stack S to hold the operators that have not yet
           been added to the postfix expression
    while there are more tokens do
        get the next token t
        if t is a number then enqueue t on Q
        else if t is an operator then push t on S
        else if t is ( then skip
        else if t is ) then pop S and enqueue the result on Q
    endwhile
    the final answer is in Q
    
    For example:
    ( ( ( 22 / 7) + 4 ) * ( 6 - 2 ) )
    (7 - ( ( ( 2 * 3 ) + 5 ) * ( 8 - ( 4 / 2 ) ) ) )

    However, it's more convenient not to have to put parentheses around everything. We have precedence conventions, that tell which operations to do first, in the absence of parentheses. For instance, 4 * 3 + 2 equals 12 + 2 = 14, not 4 * 5 = 20.

    We need to modify the above algorithm to handle precedence.

    As a "hack" in the algorithm, we will think of the left parenthesis as a "pseudo" operator, with the lowest precedence.

    create an empty queue Q to hold the postfix expression
    create an empty stack S to hold the operators that have not yet
           been added to the postfix expression
    while there are more tokens do
        get the next token t
        if t is a number then enqueue t on Q
        else if S is empty then push t on S
        else if t is ( (even if S is not empty) then push t on S
        else if t is ) then 
    	while top of S is not ( do
    	    pop S and enqueue the result on Q
    	endwhile
    	pop S (to get rid of the ( that ended the while)
        else (t is a real operator and S is not empty)
    	while precedence of t is <= precedence of top of S do
    	    pop S and enqueue the result on Q
    	endwhile
    	push t on S
        endif
    endwhile
    while S is not empty do
        pop S and enqueue the result on Q
    endwhile
    the final answer is in Q
    
    For example:
    (22 / 7 + 4 ) * ( 6 - 2 )
    7 - ( 2 * 3 + 5 ) * ( 8 - 4 / 2 )

    IMPLEMENTING A QUEUE WITH AN ARRAY

    The naive approach will quickly have you run out of room:

    Representation:

    Operations:
    * enqueue(x): (can experience overflow)
       tail++
       A[tail]:= x
    
    * dequeue(x): (can also check for empty queue)
        head++
        return A[head-1]
    
    * isEmpty:
       return (tail < head)
    
    * peek:
       return A[head]
    
    * size:
       return tail - head + 1
    
    The problem is you will march off the end of the array after very many operations, even if the size of the queue is small compared to the size of A.

    A better approach is to wrap around to reuse the vacated space at the beginning of the array, in a circular fashion. We can do this using the operator % (or, mod). Here's a first cut:

    * enqueue(x): 
       tail := (tail + 1) % A.length
       A[tail]:= x
    
    * dequeue(x):
        temp = A[head]
        head := (head + 1) % A.length
        return temp
    
    * isEmpty:
       return (tail < head) ???
    
    The problem is that tail can wrap around and be in front of head, when the queue is not empty. To get around this problem, add one more state component: Now, testing for empty is simply testing whether count = 0.

    You can also get around the limitations of the size of the array using the same idea as for the array implementation of a stack: Whenever the array is discovered to be full during an enqueue, allocate a new array that is twice the size of the old one, copy the old array to the new one and then enqueue.

    One complication with the queue, though, is that the contents of the queue might be in two sections: from head to the end of the array, and then from the beginning of the array to tail. When copying to the new array, you'll need to take this into account.

    Given the above discussion, you should be able to understand Program 6.22 in Standish. The only difference is that I've used slightly different terminology:

    You could also modify that code to throw an EmptyQueueException, along the lines of what I did for EmptyStackException.

    Performance of the circular array implementation of a queue: