[Python-Dev] Re: heaps
David Eppstein
eppstein@ics.uci.edu
Thu, 24 Apr 2003 16:20:50 -0700
In article <001901c30aab$bf31c060$b6b8958d@oemcomputer>,
"Raymond Hettinger" <python@rcn.com> wrote:
> > I'd very much like to see the current heapq replaced with a different
> > interface in time for 2.3. I believe that an opaque object is better,
> > since it allows more flexibility later.
>
> I'm quite pleased with the version already in CVS. It is a small
> masterpiece of exposition, sophistication, simplicity, and speed.
> A class based interface is not necessary for every algorithm.
It has some elegance, but omits basic operations that are necessary for
many heap-based algorithms and are not provided by this interface.
Specifically, the three algorithms that use heaps in my upper-division
undergraduate algorithms classes are heapsort (for which heapq works
fine, but you would generally want to use L.sort() instead), Dijkstra's
algorithm (and its relatives such as A* and Prim), which needs the
ability to decrease keys, and event-queue-based plane sweep algorithms
(e.g. for finding all crossing pairs in a set of line segments) which
need the ability to delete items from other than the top.
To see how important the lack of these operations is, I decided to
compare two implementations of Dijkstra's algorithm. The priority-dict
implementation from
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/119466 takes as
input a graph, coded as nested dicts {vertex: {neighbor: edge length}}.
This is a variation of a graph coding suggested in one of Guido's essays
that, as Raymond suggests, avoids using a separate class based interface.
Here's a simplification of my dictionary-based Dijkstra implementation:
def Dijkstra(G,start,end=None):
D = {} # dictionary of final distances
P = {} # dictionary of predecessors
Q = priorityDictionary() # est.dist. of non-final vert.
Q[start] = 0
for v in Q:
D[v] = Q[v]
for w in G[v]:
vwLength = D[v] + G[v][w]
if w not in D and (w not in Q or vwLength < Q[w]):
Q[w] = vwLength
P[w] = v
return (D,P)
Here's a translation of the same implementation to heapq (untested
since I'm not running 2.3). Since there is no decrease in heapq, nor
any way to find and remove old keys, I changed the algorithm to add new
tuples for each new key, leaving the old tuples in place until they
bubble up to the top of the heap.
def Dijkstra(G,start,end=None):
D = {} # dictionary of final distances
P = {} # dictionary of predecessors
Q = [(0,None,start)] # heap of (est.dist., pred., vert.)
while Q:
dist,pred,v = heappop(Q)
if v in D:
continue # tuple outdated by decrease-key, ignore
D[v] = dist
P[v] = pred
for w in G[v]:
heappush(Q, (D[v] + G[v][w], v, w))
return (D,P)
My analysis of the differences between the two implementations:
- The heapq version is slightly complicated (the two lines
if...continue) by the need to explicitly ignore tuples with outdated
priorities. This need for inserting low-level data structure
maintenance code into higher-level algorithms is intrinsic to using
heapq, since its data is not structured in a way that can support
efficient decrease key operations.
- Since the heap version had no way to determine when a new key was
smaller than an old one, the heapq implementation needed two separate
data structures to maintain predecessors (middle elements of tuples for
items in queue, dictionary P for items already removed from queue). In
the dictionary implementation, both types of items stored their
predecessors in P, so there was no need to transfer this information
from one structure to another.
- The dictionary version is slightly complicated by the need to look up
old heap keys and compare them with the new ones instead of just
blasting new tuples onto the heap. So despite the more-flexible heap
structure of the dictionary implementation, the overall code complexity
of both implementations ends up being about the same.
- Heapq forced me to build tuples of keys and items, while the
dictionary based heap did not have the same object-creation overhead
(unless it's hidden inside the creation of dictionary entries). On the
other hand, since I was already building tuples, it was convenient to
also store predecessors in them instead of in some other structure.
- The heapq version uses significantly more storage than the dictionary:
proportional to the number of edges instead of the number of vertices.
- The changes I made to Dijkstra's algorithm in order to use heapq might
not have been obvious to a non-expert; more generally I think this lack
of flexibility would make it more difficult to use heapq for
cookbook-type implementation of textbook algorithms.
- In Dijkstra's algorithm, it was easy to identify and ignore outdated
heap entries, sidestepping the inability to decrease keys. I'm not
convinced that this would be as easy in other applications of heaps.
- One of the reasons to separate data structures from the algorithms
that use them is that the data structures can be replaced by ones with
equivalent behavior, without changing any of the algorithm code. The
heapq Dijkstra implementation is forced to include code based on the
internal details of heapq (specifically, the line initializing the heap
to be a one element list), making it less flexible for some uses. The
usual reason one might want to replace a data structure is for
efficiency, but there are others: for instance, I teach various
algorithms classes and might want to use an implementation of Dijkstra's
algorithm as a testbed for learning about different priority queue data
structures. I could do that with the dictionary-based implementation
(since it shows nothing of the heap details) but not the heapq one.
Overall, while heapq was usable for implementing Dijkstra, I think it
has significant shortcomings that could be avoided by a more
well-thought-out interface that provided a little more functionality and
a little clearer separation between interface and implementation.
--
David Eppstein http://www.ics.uci.edu/~eppstein/
Univ. of California, Irvine, School of Information & Computer Science