Priority Queues

Abstract

The standard C++ library provides an adaptor to turn a container into a priority queue. However, this priority queue misses two important features, namely the possibility to change or remove an arbitrary element and logical inspectability. This component implements a collection of priority queues, all with different trade offs.

Synopsis

#include "boost/heap.hpp"
namespace boost
{
  template <typename T, typename Comp = std::less<T>, int d = 2>
  class d_heap;
  
  template <typename T, typename Comp = std::less<T> >
  class fibonacci_heap;

  template <typename T, typename Comp = std::less<T> >
  class lazy_fibonacci_heap;

  template <typename T, typename Comp = std::less<T> >
  class pairing_heap;
  
  template <typename T, typename Cont = std::vector<T>,
          typename Comp = std::less<typename Cont::value_type> >
  class priority_queue;

  template <typename T, typename Cont = std::deque<T> >
  class queue;

  template <typename T, typename traits>
  class radix_heap;

  template <typename T, typename Comp = std::less<T> >
  class splay_heap;

  template <typename T, typename Cont = std::vector<T> >
  class stack;
}
    

Description

The priority heaps provide efficient access to the "largest" element where the definition of what is considered to be the largest elements depends on the type of the priority queue:
  • For the class template boost::queue the largest elements is the the element which is stored the longest time in the queue (similar to boost::stack this is normally not considered a priority queue).
  • For the class template boost::stack the largest element is the most recently added element (it is kind of stretching the definition of "priority queue" to consider this a priority queue but this component is a reasonable home for this template...).
  • For the class template boost::radix_heap the element type is assumed to provide access to a non-negative integral value describing relative order of the element. This value is used to sort elements in ascending order.
  • For all other types, a template argument is used to specify a binary predicate which is used to determine an ordering on the elements of the heap.
All types of priority queues provide a common interface which is made up of a few operations and typedefs:
value_type
The type of the elements stored in the priority queue.
size_type
The type used to maintain the number of elements in the priority queue.
const_reference
The type returned from top().
const_iterator
The type of the iterator used to iterate over all elements in the priority queue (it is called const_iterator rather than iterator because the elements are immutable).
Default Constructor
Construct an empty priority queue (however, this constructor is only available if the comparator type provides a default constructor).
push()
Add an element to the priority queue.
top()
Provides access the current largest element.
pop()
Remove the current largest element from the priority queue.
size()
Returns the number of elements in the priority queue.
empty()
Returns whether there are elements in the priority queue.
begin()
Returns an iterator to the first element of the priority queue.
end()
Returns a past the end iterator for the priority queue.
Note, that all classes have this common interface. This is unfortunately not the case with the standard version of the adaptor std::queue which does not provide the function top(): Instead, this adaptor provides the functions front() and back() (which are, of course, also provided for the Boost version).

The description will always assume that there is efficient access to the "largest" element of a priority queue (this is in contrast to all literature I have which always uses the "smallest" element but consistent to the behavior of the default for the standard adaptor std::priority_queue). Of course, for all priority queue templates which take a comparator as template argument, this can be changed easily. For example to provide access to the smallest element just use std::greater<T> instead of std::less<T> (both templates are defined in the header <utility>) as the type of the comparator. For the templates boost::queue and boost::stack the largest element is determined by the insertion order (the first element added and the last element added, respectively). Only Radix heaps somewhat rely on the fact that efficient access is indeed to the smallest integer in the priority queue. To make this class consistent with the other types, an ugly hack is used (see the documentation of Radix heap for details).

In any case, if the documentation mentions the largest element, it is to be viewed with this intention in mind: Using the default parameters for the priority queue templates will provide efficient access to the largest element at least for the built-in types. For other types than built-in types, you have to provide an appropriate comparator (or define operator<()) which defines an order on the values. Note that I omitted the details on what kind of order: I'm currently not sure about the exact kind of order required for the various priority queues to work. It definitely works for total orders...

Several priority queue implementions use some sort of tree internally where a simple invariant is maintained: The data stored with a nodes is larger than the data of all child nodes. This way the root of every subtree always stores the largest element in this subtree. The different implementations either differ in the details how this invariant is maintained or what kind of accesses are allowed (the template boost::priority_queue basically implements a 2-heap but unlike the template boost::d_heap no methods for manipulation of elements different than the top element are provided). This basically applies to all implementations except for Radix heaps, although boost::queue and boost::stack maintain degenerate trees: In both cases it is just a sequence.

Properties of the Different Classes

This section is intended to give you an overview of all available priority queues to determine which is the best suitable for your situations. The table lists the supported operations, support for arbitrary comparators, and the efficiency of the implementation. The operations are put into groups:
Basic
Represents basic the operations push(), top(), pop(), size(), empty()
begin()
Represents support for logical inspectability, i.e. an iterator type and the methods begin(), end()
change_top()
Represents self, that is support for the method change_top()
change()
Represents the methods changing the priority of an arbitrary element, i.e. the methods change(), decrease(), increase()
The efficiency mentioned tries to describe the efficiency of the implementation. However, until now I have only made some very simple performance tests using only random data. For a really good estimate on the performance of the various implementations, it is necessary to measure the performance on real problem data.

Type Comp Basic begin() change_top() change() Efficiency
std::stack No Yes No No No fast
boost::stack No Yes Yes No No fast
std::queue No No No No No fast
boost::queue No Yes Yes No No fast
std::priority_queue Yes Yes No No No fast
boost::priority_queue Yes Yes Yes Yes No fast
boost::splay_heap Yes Yes Yes Yes Yes slow
boost::d_heap Yes Yes Yes Yes Yes slow
boost::radix_heap No Yes Yes Yes Yes slow
boost::fibonacci_heap Yes Yes Yes Yes Yes slow
boost::lazy_fibonacci_heap Yes Yes Yes Yes Yes slow
boost::pairing_heap Yes Yes Yes Yes Yes slow

The last six classes differ in their asymptotical behavior (which is important to theoreticians but probably completely pointless for the practioneer as they are all theoretical quite close). However, measuring different scenarios (using random data...) indicated that each of the classes has its strength depending on the number of elements and the mix of operations used. If your application normally uses a common mix of operations you might be able to enhance the performance by just testing which of the priority queues works best for your application.

Bugs

There are currently some known bugs which mainly stem from the fact that I want to get this stuff at least temporarily off my desk:
  • The implementation of the Radix heap is not yet complete. I'm pretty sure that it can be done but I haven't figured out some of the details yet and the literature I have is a little bit terse on some of the more intersting parts. However, it seems that this priority is relatively fast where it is applicable. Thus, it might be interesting to use it.
  • I haven't cared much about exception safety. I think it should be doable to add exception safety since most classes are "node based": after construction of an element, only built-in types are manipulated (notably boost::priority_queue seems to be the biggest problem with respect to exception safety). However, the compare function might throw exceptions and it is not always obvious how to restore the data structure if the comparison of objects is unreliable.
  • This documentation is probably full of typos, grammatical errors, and, hopefully to a much lesser degree, technical inconsistencies...

See Also

d_heap(3), f_heap(3), l_heap(3), p_heap(3), p_queue(3), queue(3), r_heap(3), s_heap(3) stack(3)
Copyright © 1999 Dietmar Kühl (dietmar.kuehl@claas-solutions.de)
Claas Solutions GmbH