CS62, Lecture 12

  1. Queues
    1. Interface
    2. Linked List Implementation
    3. Clever array implementation
    4. Queue applications -- event queues
    5. Stequeues and dequeues

Queues

Queues are FIFO (first in-first out) structures. Applications include:

Interface

public interface Queue {
	/**
	 * Returns the number of elements in the queue.
	 * 
	 * @return number of elements in the queue.
	 */
	public int size();

	/**
	 * Returns whether the queue is empty.
	 * 
	 * @return true if the queue is empty, false otherwise.
	 */
	public boolean isEmpty();

	/**
	 * Inspects the element at the front of the queue.
	 * 
	 * @return element at the front of the queue.
	 * @exception EmptyQueueException
	 *                if the queue is empty.
	 */
	public E front() throws EmptyQueueException;

	/**
	 * Inserts an element at the rear of the queue.
	 * 
	 * @param element
	 *            new element to be inserted.
	 */
	public void enqueue(E element);

	/**
	 * Removes the element at the front of the queue.
	 * 
	 * @return element removed.
	 * @exception EmptyQueueException
	 *                if the queue is empty.
	 */
	public E dequeue() throws EmptyQueueException;
}

Linked List Implementation

We can easily hold the queue as a linked list where we keep pointers to the front and rear of the queue.

Which way should pointers go? From front to rear or vice-versa?
If the pointers point from the rear to the front, it will be quite difficult to remove the front element. As a result, we orient them to point from the front to the rear.

All of the operations are O(1).

The text and structures package implementation originally used a singly-linked list. This was a bad decision since it makes add an O(n) operation. The doubly-linked list is an alternative mentioned in the text, but that involves wasted space. Another alternative would have been a circular list. The best alternative would have been a singly-linked list with references to both the front and rear.

Clever array implementation

Also have array implementation, but bit trickier!

Suppose we can set an upper bound on the maximum size of the queue.

How can we solve the problem of the queue "walking" off one end of the array?

Instead we try a 'Circular' Array Implementation w/ "references" (subscripts) referring to the head and tail of the list.

We increase the subscripts by one when we add or remove an element from the queue. In particular, add 1 to front when you remove an element and add 1 to rear when you add an element. If nothing else is done, you soon bump up against the end of the array, even if there is lots of space at the beginning (which used to hold elements which have now been removed from the queue).

To avoid this, we become more clever. When you walk off one end of the array, we go back to beginning. Use

    index = (index + 1) mod MaxQueue 
to walk forward. This avoids the problem with falling off of the end of the array.

Exercise: Before reading futher, see if you can figure out the representation of a full queue and empty queue and how you can tell them apart.

Notice that the representation of a full queue and an empty queue can have identical values for front and rear.

The only way we can keep track of whether it is full or empty is to keep track of the number of elements in the queue. (But notice we now don't have to keep track of the rear of the queue, since it is count-1 after front.)

There is an alternative way of keeping track of the front and rear which allow you to determine whether it is full or empty without keeping track of the number of elements in the queue. Always keep the rear pointer pointing to the slot where the next element added will be put. Thus an empty queue will have front = rear. We will say that a queue is full if front = rear + 1 (mod queue_length). When this is true, there will still be one empty slot in the queue, but we will sacrifice this empty slot in order to be able to determine whether the queue is empty or full.

public class QueueArray implements Queue
{
    protected E[] data;    // array of the data
    protected int head;         // next dequeue-able value
    protected int count;        // # elts in queue

    public QueueArray(int size)
    // post: create a queue capable of holding at most size 
    //          values.
    {
        data = (E[]) new Object[size];
        head = 0;
        count = 0;
    }

    // pre: The queue is not full
    // post: the value is added to the tail of the structure
    public void enqueue(E value)
    {
        int tail = (head + count) % data.length;
        data[tail] = value;
        count++;
    }

    public E dequeue()
    // pre: the queue is not empty
    // post: the element at the head of the queue is removed 
    //          and returned
    {
        E value = data[head];
        head = (head + 1) % data.length;
        count--;
        return value;
    }

    // pre: the queue is not empty
    // post: the element at the head of the queue is returned
    public E front() throws EmptyQueueException
    {
       if (count == 0) throw new EmptyQueueException();
        return data[head];
    }

    ...

    public int size()
    // post: returns the number of elements in the queue.
    {
        return count;
    }

    public void clear()
    // post: removes all elements from the queue.
    {
        // we could remove all the elements from the queue.
        count = 0;
        head = 0;
    }
    
    public boolean isFull()
    // post: returns true if the queue is at the capacity.
    {
        return count == data.length;
    }

    public boolean isEmpty()
    // post: returns true iff the queue is empty
    {
        return count == 0;
    }
}

The complexity of operations for the array implementation of the queue is the same as for the linked list implementation.

There are the same trade-offs between the two implementations in terms of space and time as with stacks above. Notice that we do not bother to set array entry for dequeued element to null. Similarly with clear. Thus the garbage collector would not be able to sweep up removed elements even if not in use elsewhere. It would probably make sense to be more consistent with Vector and clean up behind ourselves.

Applications of Queues

The text talks about several applications including round-robin polling. We will talk about use of the event queue in Java. Java programs are executed with multiple threads: The initial threads run the main method and all code that follows from that. In applets, these threads take care of start and init methods (and begin in objectdraw). The event dispatch thread responds to user events, including explicit acts like clicking on a button and implicit acts like uncovering part of a window, which requires that it be redrawn. The worker threads are typically created explicitly by the programmer to handle time-consuming computations. We will only be concerned with the event-dispatch thread here.

While events can be triggered by any thread, all responses to events are handled by the event-dispatch thread. When a thread (including the OS thread) "posts" an event, it is added to the Java event queue. When an event is removed from the queue by the event-dispatch thread, all objects that have registered to be notified about that event are sent a message with a parameter providing information about the event.

The event-dispatch thread removes items from that queue one at a time and sends the appropriate message to any registered listeners.

In Java, objects can register to be listeners for particular kinds of events on objects that generate events. A simple example arises with a JButton. JButton's generate action events when the user presses on the button. Objects can register to listen for action events on a particular JButton so that they can be notified when the button is pressed. The following example is from the class GraphicsMaze.

	protected JButton startButton; // Button to start solving maze
	protected JButton clearMaze; // Button to clear maze solution
	...
	public void begin() {
		startButton = new JButton("Solve maze");
		startButton.addActionListener(new StartListener(this));
        ...

The actual listening class is declared as follows:

	public class StartListener implements ActionListener {
		protected GraphicsMaze gMaze;
		public StartListener(GraphicsMaze gMaze) {
			this.gMaze = gMaze;
		}
		
		/**
		 * Post: If clicked start and maze successfully loaded, then attempted to
		 * solve maze. If selected new maze, then loaded and displayed. If
		 * clicked clear, then old path removed and maze redisplayed
		 */
		public void actionPerformed(ActionEvent e) {
			if (success) { // start solving maze
				gMaze.showStatus("Running maze");
				...
			}
		}
	}

Notice that the class StartListener implements interface ActionListener, which has only one method: actionPerformed. All listeners to action events must satisfy that interface, because the event-dispatch thread counts on the presence of that method in the object.

Here is what happens when the user clicks on the "start" button:

  1. The system posts the action event, e (an object of type ActionEvent), into the event queue.

  2. When e reaches the front of the queue, the event-dispatch queue sends the message actionPerformed with parameter e to all objects that are registered to listen to the button. These are executed sequentially. When all finish, the event-dispatch thread fetches the next event from the event queue.

A couple of relevent points:

  1. Methods responding to events in the listener classes should complete as quickly as possible as otherwise they block the system from responding to the other events on the queue. Spawn a new thread if the response will take time.

  2. The programmer has a lot of flexibility in determining which objects listen for events. In CS 51 at Pomona, it was always the class extending WindowController (the applet). In this program, it is an "inner class", which is much more typical of Java programs.

    Inner classes are classes nested inside other classes. In the case of the example above, class StartListener is nested inside class GraphicsMaze. There are two advantage to using nested classes over using classes at the top level. The first is that nested classes are not visible to other classes, so they keep the name space less cluttered. Second, they have access to the instance variables of the enclosing class. Both of these help with information hiding and modularity of programs. [Note that non-static inner classes cannot contain static variables or methods because they are considered to be contained within objects rather than classes.]

In the calculator example next week, you will see that we use different kinds of classes as listeners to different kinds of buttons.

Stequeues and Dequeues

A dequeue is a variant of a queue in which items may be added or removed from both ends. It can be efficiently implemented as a doubly-linked list (see the text for details). A stequeue is a data structure in which elements may be added or deleted on one end (like a stack), while only added on the other (like the back of a queue). It can be implemented by a singly linked list with references to the front and rear. You should be able to work out the implementation on your own.

¥