Notes on Queues

(related reading: Main & Savitch, Chapter Eight)

Do you like to wait in line?

Neither do I. Next time you are stuck waiting in line, observe the obvious: In summary, lines, (a.k.a. queues) aren't that complicated.

Queues

A queue is an ordered collection of items for which we can only add an item at the back or remove an item from the front. The queue is yet another container class, much like a list, but with a much more limited set of operations, reminiscent of the stack:

Queues are "first in, first out" (FIFO) structures.

We are interested in designing a queue class template so that we may place all sorts of things on our queues. Our interface will look something like:

template <class Item>
class Queue {
public:
  bool isEmpty() const;
  size_t size() const;
  Item dequeue();
  void enqueue(const Item& it);
  ...
private:
  ...
};

A simple queue application: Palindromes

A palindrome is a sequence of symbols that reads the same backward or forward. Examples include:

(Sometimes we ignore case. Sometimes we ignore spaces. Sometimes we ignore non-alphabetic characters.)

How can we tell if a sequence of characters is a palindrome? We can use a stack and a queue. For each character in the sequence, we push it on the stack and place it on the back of the queue. When we have finished reading the sequence, we pop the stack and remove the front item from the queue, checking to see if the characters match. If so, we repeat until the stack and queue are empty (in which case the sequence is a palindrome) or until the popped and dequeued characters differ (in which case the sequence is not a palindrome). For a simple implementation, look here. (For a more detailed discussion see Main & Savitch pp. 350-352.)

As an example, consider the word "radar":

|r|<-- top of stack
|a|
|d|                 -----        
|a|                 radar
|r|                 -----
---                 ^   ^
                 back   front

Implementing Queues

List implementation

Queues are very much likes lists, minus the ability to manipulate items on the lists that are not at one of the ends. A logical choice for the representation of queues, is using a list. An implementation of a Queue class template using our List class template can be seen here. The idea is to keep the cursor of the underlying list at the head of the list and thus at the front of the queue. Dequeue calls reset (to guarantee the cursor is at the front), current (to retrieve the item at the front), and remove (to take the item off the queue). Enqueue calls append to place the new item on the back of the queue. Otherwise this implementation is almost identical to the Stack class template using the List class template discussed in the notes on stacks.

Array implementation

Implementing stacks with arrays was easy and efficient - we might try the same basic implementation for queues:

template <class Item, size_t cap>
class Queue
{
public:
  ...
  void enqueue(const Item& it);
  Item dequeue();
  size_t size() const;
  bool isEmpty() const;
  bool isFull() const;
private:
  Item data[cap];
  size_t count;
};

We can treat the first position (index 0) in the array as the front of the stack and treat the position indicated by index count as the back of the stack. isEmpty, isFull, size are then as expected (just like for stacks). Enqueue remains simple:

template <class Item, size_t cap>
void Queue<Item,cap>::enqueue(const Item& it)
{
  assert(!isFull());

  data[count] = it;
  count++;
}
But what happens when we implement dequeue? The obvious approach is like the remove member function for lists implemented with arrays - we must "slide" each item one position toward the front:
template<class Item, size_t cap>
Item Queue<Item,cap>::dequeue()
{
  assert(!isEmpty());

  Item result = data[0];
  count--;
  for(size_t i=0; i<count; i++)
    data[i] = data[i+1];
  return result;
}
What's wrong with this implementation? It's running time is linear (O(n)) in terms of the number of items on the queue. Our linked list representation only requires constant-time (O(1)). pop, the corresponding operation for stacks can be also be implemented to run in constant time using arrays. So what does this say about implementing queues? Either we should stick with the linked list representation or we should find a more clever way to do it with arrays.

A more clever array implementation

We can avoid the above inefficiency by treating the array as if it were a circular structure. We let both the back and the front of the queue move forward through the array until the end of the array is hit at which point they return to the beginning of the array. This can be accomplished by adding two private member variables:

size_t front;
size_t back;
Member variable front indicates the index of the array representing the front of the queue. Member variable back indicates the index of the array representing the back of the queue. If front < back then the items on the queue run from data[front] to data[back-1]. If front = back then the queue is either full or empty (depending on count). If front > back then the items are in data[front] to the end of the array and from the beginning of the array up to but not including data[back].

Enqueue now becomes:

template<class Item, size_t cap>
void Queue<Item, cap>::enqueue(const Item& it)
// Places it on the back of the queue.
{
  assert(!isFull());

  data[back] = it;
  back = (back + 1) % cap;
  count++;
}

The line

 back = (back + 1) % cap;
indicates the back should be incremented modulo cap. That is, it is incremented unless it hits the end of the array, in which case it should be reset to zero.

A similar operation will be needed on front in dequeue. To avoid redundancy and to provide clarity, we write a private helper function to compute the next index in the circular array:

template<class Item, size_t cap>
size_t Queue<Item,cap>::next_index(size_t index) const
{
  return ((index + 1) % cap);
}

Now, the class template definition looks like:

template <class Item, size_t cap>
class Queue
{
public:
  ...
  void enqueue(const Item& it);
  Item dequeue();
  size_t size() const;
  bool isEmpty() const;
  bool isFull() const;
private:
  Item data[cap];
  size_t count;
  size_t front;
  size_t back;
  size_t next_index(size_t index) const; // helper function
};

enqueue now looks like:

template<class Item, size_t cap>
void Queue::enqueue(const Item& it)
{
  assert(!isFull());

  data[back] = it;
  back = next_index(back);
  count++;
}

Finally, dequeue looks like:

template<class Item, size_t cap>
Item Queue<Item, cap>::dequeue()
{
  assert(!isEmpty());

  Item result = data[front];
  front = next_index(front);
  count--;
  return result;
}

Now we have all our basic queue operations running in constant time regardless of whether or not we use linked lists or arrays. As mentioned with the array implementation of stacks, we need copy constructors and assignment operators. They are virtually identical to the stack version. (These take linear time in the number of items in the queue.) The array implementation of queues can be viewed here.

Queue applications

Queues, like stacks, have a great many applications. Below we discuss how they might be used in a data structure known as a priority queue. Later, we'll use queues when performing various graph algorithms.