Notes on Priority Queues

(related reading: Main & Savitch, pp. 379-383)

Stacks and queues are simple, practical abstract data types that can be implemented very efficiently - their basic operations can be implemented to run in worst-case constant time. Here we will consider a new data type, very similar to a queue, also simple and also very practical, but in which efficient implementation is not so easy.

A priority queue is a data structure for maintaining a collection of items each with an associated key (or priority). A priority queue supports the following operations:

as well as the now standard isEmpty, isFull, and size operations.

If each item's priority is taken to be time at which it is inserted, then we get a first-in-first-out structure (i.e. a queue), thus the name priority queue.

What happens if two different items have the same priority? There are several options. As stated so far, there is no rule - so which ever item is extracted first is arbitrary. Alternatively, we can, as Main & Savitch do, treat the item that arrives first as if it had higher priority. In any case, this should be documented in specification of the priority queue.

(Note: in class, our first implementation (with arrays), makes no guarantees about the order of which items will be extracted if they have the same priority. Our second implementation (the bounded priority queue) takes the approach that if two items have the same priority the first one on the queue will be the first one off. Using this approach we can see that if all items on the queue have the same priority, the priority queue behaves just as a queue - perhaps, another reason for the name of this ADT.)

Interface

The interface for a priority queue class template might look like:

template <class Item, class Key>
class PQueue {
public:
  ...
  Item maximum() const;
  void insert(const Item& it, const Key& key);
  Item extractMax();
  ...
private:
  ...
};
Notice that we make the class template over two class parameters, so that we can store items of most any types (that have copy constructors and assignment operators) and have priorities (keys) of a range of types that support comparison.

Priority Queue Applications

Implementations

There are many different ways to implement priority queues. The choice of which implementation to use is largely based on the resulting run-time complexity of the three basic priority queue operations (insert, maximum, extractMax).

Array Implementation

A straightforward array implementation of priority queues can be found on-line here. As pointed out by several students in class, this implementation does not preserve the order of arrival of elements of equal priority.

The private part of the class looks like:

template <class Item, class Key>
class PQueue {
public:
  static const size_t CAPACITY = ...
  ...
private:
  struct IKPair {
    Item val;
    Key k;
  };
  IKPair data[CAPACITY];  // an array of item/key pairs
  size_t count;           // how many items on the PQ
  size_t max;             // index of highest priority
};
struct IKPair is an example of a nested class - a class defined within another class. Since it has been declared in the private section, only PQueue (or its friends, if it has any) can use IKPair. The keyword struct indicates a class whose member are public by default.

The invariant for this implementation is that data[max] always contains the Item/Key pair of highest priority.

It's easy to see that insert and maximum run in constant time - they each only do a handful of simple operations without loops. However, extractMax takes linear time (O(n)) since we must walk through the remaining item/key pairs in the array to find the pair with the next highest priority. This can be too slow for application that need to make frequent use of the extractMax function.

A slightly more complicated array implementation of priority queues can be found on-line here. This implementation uses circular arrays in such a way that the order of arrival of elements of equal priority is preserved.

Bounded Priority Queues

We can use an array of queues to implement a priority queue. One queue is used for each priority level. This restricts our keys to be of a integral type (so they may be indices into the array). Furthermore, there is a maximum number of different priority which must be fixed in advance. An implementation of a bounded priority queue template class can be found here.

In the implementation we reuse a Queue class template. Recall, that our queue interface provided no way to examine the front of the queue other than by calling dequeue which removes the item at the front. Thus, it would be messy to implement the maximum function for priority queues with this implementation. However, not all priority queues need a separate maximum function (separate from extractMax) and for simplicity, we define our bounded priority queue without it. The interface is:

template <class Item, size_t priCap>
class BPQueue {
public:
  ...
  insert
  extractMax
  ...
private:
  Queue<Item> qs[priCap]; // array of queues
  size_t count;    // total number of item in PQ
  size_t highest;  // index of highest priority level 
                   // with a non-empty queue

As with our array implementation of priority queues, insert runs in constant time. How fast is extractMax for bounded priority queues? In the worst case, after removing an item, our priority queue will be empty and we will need to iterate through all the queues in the array to find the next highest priority item. This is O(k) where k is the size of the the array. Notice that k is essentially fixed (i.e. a constant) in terms of the number of possible items placed in the queue. So for small k compared to n (number of items in the queue) we can say that extractMax is O(1). So bounded priority queues provided are efficient. However, they do limit the number and kind of priorities. So it may be not be ideal for certain applications (for instance if each item may have a specific time, other than its arrival time, associated with it).