Link

Priority Queues and Heaps

We’ve already studied 5 abstract data types: List, Stack, Queue, Set, and Map. There’s one more frequently-used ADT, called the Priority Queue.

Priority Queue
An abstract data type characterized by two operations: removing the maximum item and adding new items.

A possible Java implementation might look like:

public interface MaxPQ<Item> {

    /** Adds the item to the priority queue. */
    public void add(Item x);

    /** Returns the item with the highest priority. */
    public Item max();

    /** Removes and returns the item with the highest priority. */
    public Item removeMax();

    /** Returns the number of items in the priority queue. */
    public int size();
}

Imagine we’re developing an emergency room patient management system. We’d like to know, at any given time, which patient requires the most urgent care. This is a perfect application for a priority queue: the patient requiring the most urgent care has the maximum priority in our priority queue. When we remove this patient from the priority queue, we’d like to know which patient has the next-highest priority. However, we don’t necessarily need to maintain a perfect sorted order for all the patients; all we need to know is the patient with the current highest priority.

Priority Queue Remove

Similarly, when a new patient arrives in the emergency room we want to ensure that they’re registered with our patient management system (ie, added to our priority queue) and possibly moved to the front if they’re the highest-priority … even if they just arrived.

One way of viewing our “classic Queue” ADT is as a Priority Queue whose priority is “duration in the queue”: the oldest item is removed first, then the next-oldest, until the queue is emptied. In contrast, in a Priority Queue a newly-added item may the next one removed if its priority is higher than anything currently in the queue.

Priority Queue Add

When we compared the List ADT with the Queue ADT, we found that restricting an ADT’s functionality (List to Queue) gives the implementer more flexibility to design faster data structures (ArrayList to ArrayQueue). We will use the same approach here: first, we will attempt to use existing data structures (such as linked nodes) to implement a priority queue, and then see if they generate any insights that might help us implement a faster data structure. Let’s compare some of these implementations now.

Describe how we could implement the MaxPQ interface with an unordered linked list.

In an unordered linked list, we can put our items anywhere in the list. This will make insertion fast because we can add the item to the front of the list in constant time, but finding the maximum slow since we need to scan across the entire linked list.

Describe how we could implement the MaxPQ interface with an ordered linked list.

In an ordered linked list, the opposite is true. We can find the maximum quickly since we can maintain a reference to it (either the front or back of the list, our choice), allowing us to remove and return the maximum priority item in constant time. However, this slows down the insertion process since we need to find the exact, ordered position for the item in the list.

Describe how we could implement the MaxPQ interface with a balanced search tree.

A balanced search tree maintains the order of items in the structure of the tree. The largest key is the very rightmost node in the tree. We can keep a reference to the rightmost node in the tree and update it as we add and remove items from the priority queue. However, every time we remove the rightmost item we will have to rebalance our tree, which may take O(log N) time.

This yields the following table comparing the asymptotic running time for each implementation in Big-O notation, where N is the size of the priority queue.

OperationUnordered Linked ListOrdered Linked ListBalanced Search Tree
addO(1)O(N)O(log N)
maxO(N)O(1)O(1)
removeMaxO(N)O(1)O(log N)

While the balanced search tree implementation is appealing, the more we call removeMax the more we find that the tree is harder to rebalance. That is, our initial removeMax calls will only take a 1-2 rotations to rebalance the tree, but later calls might require rotations go all the way to the root. In class, we’ll discuss a related data structure called a heap whose runtime is technically O(log N) but in practice is closer to O(1).