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 interfaces for stacks and queues:

create
Inputs: none.
Output: result, a priority queue.
Preconditions: none.
Postcondition: result is an empty priority queue.

insert
Inputs: insertend, a value having a priority, and base, a priority queue of which the elements are of the same type as insertend.
Outputs: none.
Preconditions: none.
Postcondition: base at output contains all of the elements that were in base at input and also contains insertend.

extract-foremost
Input: base, a priority queue.
Output: extrahend, a value of the type of the elements of base.
Precondition: base is not empty at input.
Postconditions: No element of base at input has a higher priority than that of extrahend. base at output does not contain extrahend, but does contain all of the other elements that were in base at input.

empty
Input: operand, a priority queue.
Output: result, a Boolean.
Preconditions: none.
Postcondition: Either result is true and operand is empty, or result is false and operand is not empty.

When this interface is implemented in HP Pascal, one should add a DeallocatePriorityQueue procedure to make it possible to recycle dynamic storage correctly.

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
  PriorityQueue = List { with cursor };

function CreatePriorityQueue: PriorityQueue;
begin
  CreatePriorityQueue := MakeEmptyList
end;

procedure InsertInPriorityQueue (Insertend: Element; var Base: PriorityQueue);
var
  Continue: Boolean;
    { indicates whether the search for the correct insertion point can and
      should continue }
  Current: Element;
    { one element at a time from the priority queue }
begin
  CursorToStartOfList (Base);
  Continue := not NullCursorInList (Base);
  while Continue do begin
    Current := ElementAtCursorInList (Base);
    if Current.Priority < Insertend.Priority then
      Continue := False
    else begin
      AdvanceCursorAlongList (Base);
      Continue := not NullCursorInList (Base)
  end;
  InsertAtCursorInList (Base, Insertend)
end;

function ExtractForemostFromPriorityQueue (var Base: PriorityQueue): Element;
begin
  Assert (Base <> nil, ExtractForemostFromPriorityQueueException,
          PriorityQueueExceptionHandler);
  ExtractForemostFromPriorityQueue := FirstOfList (Base);
  DeleteFirstOfList (Base)
end;

function EmptyPriorityQueue (Operand: PriorityQueue): Boolean;
begin
  EmptyPriorityQueue := EmptyList (Operand)
end;

procedure DeallocatePriorityQueue (var Operand: PriorityQueue);
begin
  DeallocateList (Operand)
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 FindByNumber (BT: BinaryTree; NodeNumber: Integer): BinaryTree;
var
  Parent: BinaryTree;
    { a pointer to the parent of the desired node }
begin
  if NodeNumber = 1 then
    FindByNumber := BT
  else begin
    Parent := FindByNumber (BT, NodeNumber div 2);
    if Odd (NodeNumber) then
      FindByNumber := Parent^.Right
    else
      FindByNumber := 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
  BinaryTree = ^Node;
  Node = record
           Datum: Element;
           Left, Right, Parent: BinaryTree
         end;
The upheaping operation then looks like this:

procedure UpHeap (BT: BinaryTree; NewElement: Element);
begin
  if BT^.Parent = nil then { at the root of BT }
    BT^.Datum := NewElement
  else if NewElement.Priority <= BT^.Parent^.Datum.Priority then
    BT^.Datum := NewElement
  else begin
    BT^.Datum := BT^.Parent^.Datum;
    UpHeap (BT^.Parent, NewElement)
  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
  PriorityQueue = ^Header;
  Header = record
             Size: Integer;
             BT: BinaryTree
           end;
And here is the insertion procedure:

procedure InsertInPriorityQueue (Insertend: Element;
  var Base: PriorityQueue);
var
  NewBT: BinaryTree;
    { a singleton binary tree -- the leaf to be attached }
  InsertionPoint: BinaryTree;
    { the subtree to which a new leaf is to be added to accommodate the
      extra Element }
begin 
  New (NewBT);
  NewBT^.Left := nil;
  NewBT^.Right := nil;
  Base^.Size := Base^.Size + 1;
  if Base^.BT = nil then begin
    NewBT^.Parent := nil;
    NewBT^.Datum := Insertend;
    Base^.BT := NewBT
  end
  else begin
    InsertionPoint := FindByNumber (Base^.BT, Base^.Size div 2);
    NewBT^.Parent := InsertionPoint;
    if Odd (Base^.Size) then
      InsertionPoint^.Right := NewBT
    else
      InsertionPoint^.Left := NewBT;
    UpHeap (NewBT, Insertend);
  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. Downheaping consists of traversing one branch of the tree, starting from the root, and choosing the datum to be stored at each node to be either the recovered element or 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: BinaryTree; NewElement: Element);
var
  Advancer: BinaryTree;
    { 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 := NewElement
  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 <= NewElement.Priority then
      BT^.Datum := NewElement
    else begin
      BT^.Datum := Advancer^.Datum;
      DownHeap (Advancer, NewElement)
    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 ExtractForemostFromPriorityQueue (var Base: PriorityQueue): Element;
var
  DeletionPoint: BinaryTree;
    { the Parent of a leaf to be removed from the binary tree }
  Temporary: Element;
    { Temporary storage for the element in that leaf, which must be
      re-inserted through downheaping }
begin
  Assert (Base^.BT <> nil, ExtractForemostFromPriorityQueueException,
          PriorityQueueExceptionHandler);
  ExtractForemostFromPriorityQueue := Base^.BT^.Datum;
  if Base^.Size = 1 then begin
    Dispose (Base^.BT);
    Base^.BT := nil
  end
  else begin
    DeletionPoint := FindByNumber (Base^.BT, Base^.Size div 2);
    if Odd (Base^.Size) then begin
      Temporary := DeletionPoint^.Right^.Datum;
      Dispose (DeletionPoint^.Right);
      DeletionPoint^.Right := nil
    end
    else begin
      Temporary := DeletionPoint^.Left^.Datum;
      Dispose (DeletionPoint^.Left);
      DeletionPoint^.Left := nil
    end;
    DownHeap (Base^.BT, Temporary)
  end;
  Base^.Size := Base^.Size - 1
end;
The other priority-queue operations are straightforward:

function CreatePriorityQueue: PriorityQueue;
var
  Result: PriorityQueue;
begin
  New (Result);
  Result^.Size := 0;
  Result^.BT := nil;
  CreatePriorityQueue := Result
end;

function EmptyPriorityQueue (Operand: PriorityQueue): Boolean;
begin
  EmptyPriorityQueue := (Operand^.BT = nil)
end;

procedure DeallocatePriorityQueue (var Operand: PriorityQueue);

  procedure DeallocateBinaryTree (var BT: BinaryTree);
  begin
    if BT <> nil then begin
      DeallocateBinaryTree (BT^.Left);
      DeallocateBinaryTree (BT^.Right);
      Dispose (BT)
    end
  end;

begin { procedure DeallocatePriorityQueue }
  DeallocateBinaryTree (Operand^.BT);
  Dispose (Operand);
  Operand := 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 November 21, 1996