Queues

Line up, people. No cuts.

The Basics

A queue is a FIFO sequence. Addition takes place only at the tail, and removal takes place only at the head.

queues.png

The basic operations are:

Queues are seen often in computer science. A tiny fraction of their known uses:

An Interface

Yes, it’s true: there's already a Queue interface in the Java Core API and a whole bunch of implementing subclasses (AbstractQueue, ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedList, PriorityBlockingQueue, PriorityQueue, SynchronousQueue), but we're going to write some queues from scratch because:

Here is a Java Interface:

Queue.java
/**
 * A small queue interface. You can query the size of the queue and ask whether
 * it is empty, add and remove items, and peek at the front item.
 */
public interface Queue {

    /**
     * Adds the given item to the rear of the queue.
     */
    void enqueue(Object item);

    /**
     * Removes the front item from the queue and returns it.
     * 
     * @exception java.util.NoSuchElementException if the queue is empty.
     */
    Object dequeue();

    /**
     * Returns the front item from the queue without popping it.
     * 
     * @exception java.util.NoSuchElementException if the queue is empty.
     */
    Object peek();

    /**
     * Returns the number of items currently in the queue.
     */
    int size();

    /**
     * Returns whether the queue is empty or not.
     */
    default boolean isEmpty() {
        return size() == 0;
    }
}

Representations

As usual, we have many choices.

Linked Queues

Let’s start with a linked representation. Since we are adding at the tail and removing from the head, we need direct access to both ends. Now we could store direct head and tail links in the object:

simple-linked-queue.png

You are encouraged to implement the interface with a structure like this, and it is certainly doable, but there are a lot of special cases. When you are learning to program with linked structures, it’s a great exercise. In practice, however, we get a slightly simpler implementation if we store only a link to the tail node and link the tail to the head, like so:

circular-linked-queue.png

Here is an implementation using the circular queue:

CircularLinkedQueue.java
import java.util.NoSuchElementException;

/**
 * An implementation of a queue using a ring of singly linked nodes. The queue
 * object contains only a single link to its tail node (null for the empty queue
 * of course). The tail node's successor is the head of the queue, ensuring O(1)
 * time for both enqueue and dequeue.
 */
public class CircularLinkedQueue implements Queue {

    // We cannot use a record because we have to mutate these nodes
    private static class Node {
        Object data;
        Node next;

        Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
    }

    private Node tail = null;
    private int size = 0;

    public void enqueue(Object item) {
        if (isEmpty()) {
            tail = new Node(item, null);
            tail.next = tail;
        } else {
            tail = tail.next = new Node(item, tail.next);
        }
        size++;
    }

    public Object dequeue() {
        var item = peek();
        if (tail.next == tail) {
            // Last node deleted
            tail = null;
        } else {
            // Just "point around" the old head you are deleting
            tail.next = tail.next.next;
        }
        size--;
        return item;
    }

    public Object peek() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return tail.next.data;
    }

    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return tail == null;
    }
}

A Bounded Array Queue

Now this is going to get really interesting!

We’re going to get a feel for how this works in pictures.

bounded-queue.png

Here’s the code:

BoundedQueue.java
import java.util.NoSuchElementException;

/**
 * An implementation of a queue using a fixed, non-expandable array whose
 * capacity is set in its constructor.
 */
public class BoundedQueue implements Queue {
    private Object[] elements;
    private int size = 0;
    private int head = 0; // index of the current front item, if one exists
    private int tail = 0; // index of next item to be added

    public BoundedQueue(int capacity) {
        elements = new Object[capacity];
    }

    public void enqueue(Object item) {
        if (isFull()) {
            throw new IllegalStateException("Cannot add to full queue");
        }
        elements[tail] = item;
        tail = (tail + 1) % elements.length;
        size++;
    }

    public Object dequeue() {
        var item = peek();
        elements[head] = null;
        head = (head + 1) % elements.length;
        size--;
        return item;
    }

    public Object peek() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return elements[head];
    }

    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    // This is a bonus method that is not part of the interface
    public boolean isFull() {
        return size == elements.length;
    }
}

Things to note here:

An Expandable Array Queue

You can do a little “mixing” of the array and linked approaches. Start with an array of reasonable size, then, when it fills up, reallocate a new array (of twice the size), copy the contents over, and add the new element. If the array ever gets too small, allocate a smaller array for the queue and copy into it.

Exercise: This will be homework.

Unit Testing

Here are some things we need to test:

We have multiple implementations to test, so it makes sense to factor out all the common tests into a base test class:

QueueTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.NoSuchElementException;

/**
 * Base test for any class that implements the Queue interface.
 */
public abstract class QueueTest {

    /**
     * The queue to use in all the tests: set this in subclasses.
     */
    protected Queue q;

    @Test
    public void testNewQueueIsEmpty() {
        assertTrue(q.isEmpty());
        assertEquals(q.size(), 0);
    }

    @Test
    public void testInsertsToEmptyQueue() {
        int numberOfInserts = 8;
        for (var i = 0; i < numberOfInserts; i++) {
            q.enqueue("zzz");
        }
        assertTrue(!q.isEmpty());
        assertEquals(q.size(), numberOfInserts);
    }

    @Test
    public void testEnqueueThenDequeue() {
        var message = "hello";
        q.enqueue(message);
        assertEquals(q.dequeue(), message);
    }

    @Test
    public void testEnqueueThenPeek() {
        var message = "hello";
        q.enqueue(message);
        var size = q.size();
        assertEquals(q.peek(), message);
        assertEquals(q.size(), size);
    }

    @Test
    public void testFiftyInThenFiftyOut() {
        for (int i = 0; i < 50; i++) {
            q.enqueue(i);
        }
        for (int i = 0; i < 50; i++) {
            assertEquals(((Integer) q.dequeue()).intValue(), i);
        }
    }

    @Test
    public void testRemovingDownToEmpty() {
        int numberOfRemoves = (int) (Math.random() * 20 + 1);
        for (int i = 0; i < numberOfRemoves; i++) {
            q.enqueue("zzz");
        }
        for (int i = 0; i < numberOfRemoves; i++) {
            q.dequeue();
        }
        assertTrue(q.isEmpty());
        assertEquals(q.size(), 0);
    }

    @Test
    public void testRemoveFromEmptyQueueThrows() {
        assertTrue(q.isEmpty());
        assertThrows(NoSuchElementException.class, () -> q.dequeue());
    }

    @Test
    public void testPeekIntoEmptyQueueThrows() {
        assertTrue(q.isEmpty());
        assertThrows(NoSuchElementException.class, () -> q.peek());
    }

}

The concrete test classes instantiate the specific kind of queue. The linked tester needs nothing other than the creation of the test queue:

LinkedQueueTest.java
import org.junit.jupiter.api.BeforeEach;

public class LinkedQueueTest extends QueueTest {

    @BeforeEach
    public void makeLinkedQueue() {
        q = new LinkedQueue();
    }
}

For the bounded queue, we need to test that insertion into a full queue is an error:

BoundedQueueTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class BoundedQueueTest extends QueueTest {
    private static int CAPACITY = 60;

    @BeforeEach
    public void makeBoundedQueue() {
        q = new BoundedQueue(CAPACITY);
    }

    @Test
    public void testEnqueueToFullQueue() {
        for (int i = 0; i < CAPACITY; i++) {
            q.enqueue("abc");
        }
        assertThrows(IllegalStateException.class, () -> q.enqueue("abc"));
    }
}

An Application of Queues

TODO

Summary

We’ve covered:

  • Basic information about queues
  • An interface
  • Two different implementations
  • Unit testing
  • An application