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