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