Priority queues

Like a stack or a queue, a priority queue is a homogeneous data structure of variable size, initially empty, but allowing the insertion and extraction of elements during the execution of a program. But whereas in a stack or queue it is the nature of the data structure itself that determines which element is available for extraction at any given time, in a priority queue each element carries with it a value -- its priority -- by which the extraction order is determined: An element cannot leave a priority queue until after all the elements of greater priority have been extracted. In other words, an element that can be extracted from a priority queue must have a priority greater than or equal to that of any other element.

A priority queue is like a waiting line in which a high-priority element can ``pull rank'' on elements with lower priorities, cutting in line ahead of them. If priorities increase monotonically over time, the priority queue acts like a stack (because the last element in will have the highest priority and so be the first one out); if priorities decrease over time, it will act like a queue (first in, first out). But priority queues are generally used in cases where the priorities are independent of time. The print queues on the VAX, for example, are priority queues in which short jobs that will not tie up the printer for long are assigned higher priorities than long multi-page print jobs.

The interface for a priority queue type is straightforward and analogous to the interface for stacks and for queues. There is a constructor that allocates and returns an empty priority queue, a deallocator that recycles the storage for such a queue when one has finished with it, a test to determine whether a priority queue is empty, a procedure for adding an element to a priority queue, and a function for recovering the element of highest priority from the queue. Here's what it looks like:

type
  element = record
              priority: real;
              { and presumably other fields as well }
            end;

{ The empty_priority_queue function creates and returns an empty priority
  queue. }

function empty_priority_queue: priority_queue;

{ The is_empty_priority_queue function determines whether a given
  priority queue is empty. }

function is_empty_priority_queue (pq: priority_queue): Boolean;

{ The add_to_priority_queue procedure adds a given element to a given
  priority queue. }

procedure add_to_priority_queue (new_element: element;
  var pq: priority_queue);

{ The extract_top_from_priority_queue function removes from a given
  non-empty priority queue the element that has the greatest priority and
  returns that element. }

function extract_top_from_priority_queue (var pq: priority_queue):
  element;

{ The deallocate_priority_queue procedure recycles all the storage
  associated with a given priority queue, leaving its argument
  undefined. }

procedure deallocate_priority_queue (var pq: priority_queue);
As usual, there are various ways to implement priority queues. One common arrangement is to use linked lists, consistently keeping the elements in the list in order of descending priority (so that the highest-priority element is always at the front of the list, where it can be deleted efficiently). The implementation then looks like this:

type
  link = ^component;
  component = record
                datum: element;
                next: link
              end;
  priority_queue = link;

function empty_priority_queue: priority_queue;
begin
  empty_priority_queue := NIL
end;

function is_empty_priority_queue (pq: priority_queue): Boolean;
begin
  is_empty_priority_queue := (pq = NIL)
end;

procedure add_to_priority_queue (new_element: element;
  var pq: priority_queue);
label
  9;
    { bail out on encountering an element of lower priority than the
      one to be inserted }          
var
  new_link: link;
    { a pointer to the storage allocated for the new element }
  traverser, trailer: link;
    { pointers to successive components of the list; traverser advances
      through the list, while trailer stays one component behind
      traverser until the correct insertion point is found }
begin
  new (new_link);
  new_link^.datum := new_element;
  traverser := pq;
  trailer := NIL;
  while traverser <> NIL do begin
    if traverser^.datum.priority < new_element.priority then
      goto 9;
    trailer := traverser;
    traverser := traverser^.next
  end;
9:
  new_link^.next := traverser;
  if trailer = NIL then
    pq := new_link
  else
    trailer^.next := new_link
end;

function extract_top_from_priority_queue (var pq: priority_queue):
  element;
begin
  assert (pq <> NIL, EXTRACT_EXCEPTION, priority_queue_handler);
  extract_top_from_priority_queue := pq^.datum;
  pq := pq^.next
end;

procedure deallocate_priority_queue (var pq: priority_queue);
var
  traverser, trailer: link;
    { pointers to successive components of the list }
begin
  traverser := pq;
  while traverser <> NIL do begin
    trailer := traverser;
    traverser := traverser^.next;
    dispose (trailer)
  end;
  pq := NIL
end;
Unfortunately, insertion is not very efficient under this approach, since it involves a linear search down the linked list to find the correct place at which to add the element.

A faster approach is to store the data in binary trees, with an ordering property imposed to make sure that the highest-priority element is always easily accessible -- specifically, that it is at the root of the binary tree. The appropriate ordering property is that the element stored at any node of the binary tree should have a priority greater than or equal to that of any element stored in either of its subtrees. A binary tree that has this property is called a heap.

Ensuring that the heap property is restored after each insertion and after each deletion is a little tricky, but fast, since it turns out that we need to traverse only one branch of the binary tree in either case, and with a little care we can control the shape of the binary tree so that all of the branches remain as short as possible. More precisely, we'll require that it is always a complete binary tree -- one in which each new node is added as a leaf at the end of the shortest available branch (and among the branches that are equally short, to the branch that is as far to the left in the tree as possible).

In a complete binary tree, it is always possible to locate a node in the tree from its number, using the following numbering system: the root is node 1, its children are nodes 2 and 3, and in general the nodes in the left and right subtrees of node $n$ are numbered 2n and 2n + 1. The following function shows how to obtain a pointer to any node in a given binary tree, given its node number:

function find_by_number (bt: binary_tree; node_number: integer):
  binary_tree;
var
  parent: binary_tree;
    { a pointer to the parent of the desired node }
begin
  if node_number = 1 then
    find_by_number := bt
  else begin
    parent := find_by_number (bt, node_number div 2);
    if odd (node_number) then
      find_by_number := parent^.right
    else
      find_by_number := parent^.left
  end
end;
To add a new element to a heap, one attaches the leaf at the appropriate position and then performs an ``upheaping'' operation, shifting elements of lower priority downwards along the branch to make room for the new element to be inserted. Thus one moves along the branch from the leaf towards the root; to make this possible, each node includes not only pointers to its left and right subtrees, but also a pointer to its parent node:

type
  binary_tree = ^node;
  node = record
           datum: element;
           left, right, parent: binary_tree
         end;
The upheaping operation then looks like this:

procedure upheap (bt: binary_tree; new_element: element);
begin
  if bt^.parent = NIL then
    bt^.datum := new_element
  else if new_element.priority <= bt^.parent^.datum.priority then
    bt^.datum := new_element
  else begin
    bt^.datum := bt^.parent^.datum;
    upheap (bt^.parent, new_element)
  end
end;
In order to keep track of the correct insertion point for a new element in the heap, then, we only need to know the number of nodes in the tree; the next node to be added will go into the position that is one greater than the current size. Here, then, is the appropriate type definition for a priority queue:

type
  priority_queue = ^header;
  header = record
             size: integer;
             bt: binary_tree
           end;
And here is the insertion procedure:

procedure add_to_priority_queue (new_element: element;
  var pq: priority_queue);
var
  new_bt: binary_tree;
    { a singleton binary tree -- the leaf to be attached }
  insertion_point: binary_tree;
    { the subtree to which a new leaf is to be added to accommodate the
      extra element }
begin 
  new (new_bt);
  new_bt^.left := NIL;
  new_bt^.right := NIL;

  pq^.size := pq^.size + 1;
  if pq^.bt = NIL then begin
    new_bt^.parent := NIL;
    new_bt^.datum := new_element;
    pq^.bt := new_bt
  end
  else begin
    insertion_point := find_by_number (pq^.bt, pq^.size div 2);
    new_bt^.parent := insertion_point;
    if odd (pq^.size) then
      insertion_point^.right := new_bt
    else
      insertion_point^.left := new_bt;
    upheap (new_bt, new_element);
  end
end;
To perform a deletion, one first makes a copy of the element stored at the root of the tree, then finds the most recently added leaf, recovers the element stored there, and performs a ``downheaping'' operation in which a branch of the tree is traversed, starting from the root, and at each step the datum stored at a node is filled either by the recovered element or by the datum at the root of one of the subtrees, whichever of the three has the highest priority. The traversal continues as long as data from subtrees are ``promoted'' in this way; it stops when the recovered element has been reinserted.

Here is the procedure that performs the downheaping operation:

procedure downheap (bt: binary_tree; new_element: element);
var
  advancer: binary_tree;
    { a subtree of bt, containing a datum whose priority might
      entitle it to be promoted into bt itself }
begin
  if bt^.left = NIL then
    bt^.datum := new_element
  else begin
    advancer := bt^.left;
    if bt^.right <> NIL then begin
      if bt^.left^.datum.priority < bt^.right^.datum.priority then
        advancer := bt^.right;
    end;
    if advancer^.datum.priority <= new_element.priority then
      bt^.datum := new_element
    else begin
      bt^.datum := advancer^.datum;
      downheap (advancer, new_element)
    end
  end
end;
Thus the following function extracts the highest-priority element from the binary tree inside the priority queue and then restores the heap property:

function extract_top_from_priority_queue (var pq: priority_queue):
  element;
var
  deletion_point: binary_tree;
    { the parent of a leaf to be removed from the binary tree }
  temp: element;
    { temporary storage for the element in that leaf, which must be
      re-inserted through downheaping }
begin
  assert (pq^.bt <> NIL, EXTRACT_EXCEPTION, priority_queue_handler);
  extract_top_from_priority_queue := pq^.bt^.datum;
  if pq^.size = 1 then begin
    dispose (pq^.bt);
    pq^.bt := NIL
  end
  else begin
    deletion_point := find_by_number (pq^.bt, pq^.size div 2);
    if odd (pq^.size) then begin
      temp := deletion_point^.right^.datum;
      dispose (deletion_point^.right);
      deletion_point^.right := NIL
    end
    else begin
      temp := deletion_point^.left^.datum;
      dispose (deletion_point^.left);
      deletion_point^.left := NIL
    end;
    downheap (pq^.bt, temp)
  end;
  pq^.size := pq^.size - 1
end;
The other priority-queue operations are straightforward:

function empty_priority_queue: priority_queue;
var
  result: priority_queue;
begin
  new (result);
  result^.size := 0;
  result^.bt := NIL;
  empty_priority_queue := result
end;

function is_empty_priority_queue (pq: priority_queue): Boolean;
begin
  is_empty_priority_queue := (pq^.bt = NIL)
end;

procedure deallocate_priority_queue (var pq: priority_queue);

  procedure deallocate_binary_tree (var bt: binary_tree);
  begin
    if bt <> NIL then begin
      deallocate_binary_tree (bt^.left);
      deallocate_binary_tree (bt^.right);
      dispose (bt)
    end
  end;

begin { procedure deallocate_priority_queue }
  deallocate_binary_tree (pq^.bt);
  dispose (pq);
  pq := NIL
end;

This document is available on the World Wide Web as

http://www.math.grin.edu/~stone/courses/fundamentals/priority-queues.html

created April 30, 1996
last revised April 30, 1996