Priority Queues: 1 N 1 N 1 N 1 N
Priority Queues: 1 N 1 N 1 N 1 N
Priority Queues: 1 N 1 N 1 N 1 N
Priority Queues
Anyone who has ever waited impatiently in line, for example at the ticket counter
of a crowded train station, is all too familiar with the notion of a queue. Standard
queues exhibit First-In-First-Out (FIFO) behavior, where those entering the queue
first are also the first to leave. There are some situations, however, where FIFO
behavior is not desired. For instance, in the emergency room of a hospital, patients
waiting for treatment are typically considered in order according to the urgency of
their condition. This is known as a priority queue, where elements enter the queue
with associated priorities, and the element with the highest priority is always the
first to leave. The priority queue is one of the most fundamental types of data
structures, one you are likely to encounter often in practice. In this chapter we
discuss several ways to implement a priority queue, focusing in particular on those
that are the most popular, the most efficient, and the most illustrative of elegant
principles in data structure design.
To be considered a priority queue, a data structure must at the very least support
two basic operations:
Every element stored in a priority queue has an associated key, and by convention
elements with lower keys are usually taken to have higher priority, although if
desired, it is just as easy to adopt the opposite convention. Several other operations
are commonly supported by priority queues:
Due to redundancies among these operations, we may not need to implement all of
them:
Often, a priority queue maintains only pointers to data records, rather than the
records themselves. This can improve efficiency, especially if our data records are
large, since we need to shuffle less memory around. If our data records are already
stored in some other data structure (as is often the case), we can view our priority
queue as a lightweight indexing structure built on top of this existing structure,
giving it the added functionality of a priority queue. Most important, however, is
that not unlike your digestive system, once an element is inserted into a priority
queue, it is generally not easy to access until it re-emerges from the other side, in
response to a call to remove-min. If we need to access elements from outside the
priority queue (say, if we need to call decrease-key or delete), we need to maintain
pointers to them from the outside, since priority queues do not support an efficient
means of finding elements (by contrast, dictionary data structures, discussed exten-
sively in the next two chapters, are specifically designed to efficiently find elements
based on their keys). Therefore, if e is an element stored inside a priority queue
representing data record d stored outside the structure, we typically maintain a
pointer from d to e so that we can still access e if needed. Having said all of this,
for simplicity we henceforth ignore this issue and pretend that we are storing just
a set of keys in our priority queue.
Figure 5.1: Running times for the operations of several basic priority queue
data structures. If we are using a sorted linked list rather than a sorted array,
then delete takes O(1) rather than O(n) time.
A priority queue is stable if it acts like a FIFO queue for equal elements, causing
them to exit in the same order they enter. When used to sort, stable priority
queues give us stable sorting algorithms. We can make any priority queue stable
by augmenting elements with sequence numbers giving the time they were inserted,
using these to break ties among equal elements.
1
3
2 3 parent(i) left-child(i) right-child(i)
6 12
4 i=5 6 7
18 9 15 13 1 2 3 4 i=5 6 7 8 9 10 11 12
8 9 10 11 12 A: 3 6 12 18 9 15 13 25 29 9 10 19
25 20 9 10 19
(a) (b)
Figure 5.2: A binary heap (a) depicted as a tree and (b) stored as an array.
To insert a new element, we increment the size of the heap, n, place the new
element in A[n] (corresponding to the next open slot in the bottom row of the
heap), and call sift-up on it, to fix any potential violation of the heap property
with its parent.
Remove-min swaps A[1] and A[n] and decrements n, so the element we want
to remove is deposited at the end of our array, now one position beyond the
end of the heap. We are left with an element at the root (formerly A[n]) that
might now violate the heap property with its children, so we call sift-down on
the root.
All of these operations call either sift-up or sift-down or both, so they all run in
O(log n) time. [Animated explanation of heap operations]
Building a Binary Heap in Linear Time. To build a binary heap on n elements,
we could start with an empty heap and make n calls to insert, although this takes
(n log n) time in the worst case where we insert elements in decreasing order, with
every element being pulled all the way up to the root by sift-up. Surprisingly, there
is an even better way to build a binary heap in only (n) time: starting with our
n elements in an unordered array A[1 . . . n], we simply call sift-down(i) on each
element i from n down to 1 in sequence. It is not immediately obvious why this
approach would build a valid binary heap or that it runs in (n) time, but we can
show both using a bit of careful analysis. [Careful analysis]
Note that the approach above builds a binary heap in place, converting an arbitrary
array into a heap-ordered array. Also note that the fast (n) running time for build
does not violate the (n log n) lower bound for comparison-based sorting, since it
still takes O(n log n) time to remove the elements from a binary heap in sorted order.
We have already described how to sort using a generic priority queue: first build
the queue and then perform n consecutive remove-min operations. In a binary
heap, this leads to a particularly nice result: after building a binary heap out of
an unsorted array A[1 . . . n], the array actually ends up in decreasing sorted order
as a side effect of calling remove-min n times. The first remove-min swaps A[1]
(the minimum) with A[n] and then decrements the size of the heap, leaving the
minimum at the end of the array. The next remove-min swaps A[1] (the second-
smallest element) with A[n 1], and so on. Of course, if we wanted our array to end
up in forward sorted order, we could have used a max binary heap, from which
we always remove the maximum instead of the minimum.
1 Achieving O(1) amortized time for insert, by itself, is actually not usually a big win in terms
of total running time, since every element is usually the victim of a remove-min call down the
road, so it still incurs O(log n) work during its lifetime in the structure.
This algorithm, called heap sort, is easy to implement, takes O(n log n) time, sorts
in place, and runs very quickly in practice. Heap sort is not stable since the binary
heap is not a stable heap, but if in-place operation and a deterministic O(n log n)
running time are important, heap sort may well be the sorting algorithm of choice.
Its closest competitor with these features, the deterministic O(n log n) variant of
quicksort, runs much slower in practice.
While the binary heap itself does not support an efficient merge operation, there
are many close relatives that do. The most elegant of the bunch, in the authors
opinion, is the randomized mergeable binary heap, which performs all priority queue
operations, including merge, in O(log n) time with high probability.
The randomized mergeable binary heap is nothing more than a heap-ordered binary
tree. It can be have arbitrary shape and may be quite unbalanced2 , so instead of
storing it in a simple array like the binary heap, we store it like most other trees.
Each element resides in its own block of memory, and maintains a pointer to its
parent, left child, and right child. Since the tree height may be significantly larger
than O(log n), the use of sift-up and sift-down is no longer prudent. Fortunately, we
can now abandon sift-up and sift-down entirely and implement every priority queue
operation in a very simple fashion using merge:
We implement remove-min by removing the root element and merging its two
former child subtrees (which are themselves valid heap-ordered trees).
2 In the next chapter, we will spent quite a bit of effort trying to keep binary trees balanced for
performance reasons. By contrast, the heap-ordered trees in this section can be quite unbalanced
with no negative consequences. Even a degenerate tree in the shape of a long n-element sorted
path would be acceptable.
H1 : 4 heads H2 : 7
tails
Figure 5.3: Recursively merging heap H2 (larger root) with a random child of
heap H1 (smaller root).
Since all of these operations involve a constant number of calls to merge, they all
run in O(log n) time with high probability as long as the same is true for merge.
Randomization gives us an extremely simple approach for merging two heaps H1
and H2 . Assuming H1 has the smaller root, its root becomes the root of the merged
tree, and we merge H2 recursively into one of its child subtrees. As shown in Figure
5.3, if a fair coin toss results in heads, we recursively merge H2 with the left subtree
of H1 . If we see tails, we recursively merge H2 with the right subtree of H1 . The
process ends when we reach the bottom of one of the trees and try to merge some
heap H1 with an empty heap H2 , in which case the result is just H1 . The fact
that this runs in O(log n) time with high probability follows immediately from the
randomized reduction lemma: with probability 1/2, we choose to merge with the
smaller of H1 s two subtrees, in which case we effectively reduce the size of H1 to
at most half of its current value.
Another natural way to merge two heap-ordered trees H1 and H2 is as follows: select
a path in H1 from its root down to an empty space at the bottom of the tree
(known as a null path), select a similar path in H2 , and merge along these paths, as
shown in Figure 5.4. Due to the heap property, null paths are sorted from top to
bottom, and consequently the process of merging along these paths is completely
analogous the familiar process of merging two sorted sequences.
One way to merge two sorted sequences s1 and s2 (say, with s1 having the smaller
initial element) is by taking the initial element of s1 followed by the result of recur-
sively merging the rest of s1 with s2 . Similarly, we merge heaps H1 and H2 along
null paths (say, with H1 having the smaller root), by recursively merging H2 with
either the left or right subtree of H1 , depending on the direction of the null path in
3
1
7 9
8 3
10 15 32
25 4 9
19 12
21 6 15 32
26 7
1
10
8 4
13 12
25 21 6
19
26 13
Figure 5.4: Merging two heap-ordered trees along a null path in each tree.
Rather than using null pointers to indicate the lack of a child at the bottom of
a tree, we have marked the bottom of each tree using dummy sentinel elements.
H1 . If you prefer the iterative outlook on merging two sequences, a similar process
works for heaps, where we step two pointers in tandem down our null paths, always
taking a step from the pointer to the smaller element, splicing the heaps together
as we go. Both the recursive and iterative approaches are completely equivalent.
As in the case of merging two sequences, they are just two different ways of looking
at the same process.
Since the amount of time required to merge two heap-ordered trees along null paths
is proportional to the combined heights of these paths, we clearly want to select
two paths of low height in order to merge quickly. The randomized mergeable
binary heap does this in perhaps the simplest possible fashion by choosing paths at
random moving left or right at each step according to the result of a fair coin flip.
This gives null paths of length O(log n) with high probability, thanks to our earlier
analysis with the randomized reduction lemma. There are several other common
alternatives for choosing good null paths, however, which we outline below.
Size-Augmented and Null-Path-Length Augmented Heaps. We could de-
randomize the randomized mergeable binary heap by augmenting each element in
our heap with a count of the number of elements in its subtree. If we then choose
a path by repeatedly stepping to whichever of our child subtrees has a smaller size,
this gives a deterministic approach for finding a path of height at most log n, since
every step downward at least halves the size of our current subtree. Similarly, we
could augment each element e with its null path length, npl(e), which gives the length
of the shortest null path downward from e. By repeatedly stepping to whichever
child has a smaller value, this also gives a deterministic approach for finding a null
path of height at most log n.
Leftist Heaps. The leftist heap is a heap where we augment each element with its
null path length and also maintain the invariant that npl(left-child(e)) npl(right-
child(e)) for every element e. This results in a tree that is somewhat left heavy in
which the best way to find a path for merging is to follow the right spine of the tree,
which has height at most log n. We therefore always merge two trees by merging
along their right spines, and then walk back up the right spine of the merged tree
swapping children anywhere necessary to restore our invariant. This approach is
essentially the same as our approach above where we augment each element with
its null path length, except we always treat the child with smaller null path length
as the right child.
Skew Heaps. The skew heap is a relaxed, amortized cousin of the leftist heap
that is simpler to implement (perhaps even almost as simple as the randomized
mergeable binary heap) but slightly more intriguing to analyze. Like the leftist
heap, we merge two heaps by merging their right spines and then adjusting the
structure of the merged tree slightly. The readjustment step, however, is now much
easier: we just walk up the right spine of the merged heap and swap the two children
of every element along the way (except the lowest). As one can prove, this results
in the tree being sufficiently leftist that merge (and hence every other priority
queue operation) runs in O(log n) amortized time. [Detailed analysis of skew heaps]
An alternative view of the skew heap that perhaps illustrates its operation more
clearly is the following: let us imagine that every element is augmented with a coin
whose state is either heads or tails. To merge two heaps H1 and H2 (with H1
having the smaller root), we look at the coin at the root of H1 ; if heads, we merge
H2 recursively with the left subtree of H1 , and if tails, we merge with the right
subtree. We then flip over the coin on the root of H1 , toggling its state. Intuitively,
this would seem to keep H1 somewhat balanced, by alternatively merging into its
left and right subtrees. Indeed, if you think carefully about the operation of this
structure, you will see that it behaves exactly the same as the skew heap as described
above; we have only described it in a top-down recursive manner3 . This top-down
description also highlights the similarity between the randomized mergeable binary
heap and the skew heap, with the only difference being that where the randomized
mergeable binary heap flips a coin, the skew heap flips the coin over.
Skew heaps and their distant relatives splay trees (Section 6.2.7) are known as
self-adjusting data structures since they can (somewhat miraculously) continually
adjust their structure so as to remain efficient despite maintaining no additional
augmented information to help them in this task. In fact, among the five different
null path mergeable heap implementations we have seen above, the randomized
mergeable binary heap and skew heap stand out in that they do not require us to
keep any augmented state in our heap. The other approaches require augmenting
elements with either null path lengths or subtree sizes, and while this extra informa-
3 To make the correspondence even more direct, we can always recursively merge with the right
subtree, and then swap the left and right subtrees. This gives the same mechanics of merging with
alternating subtrees, but avoids the need to explicitly store the state of the coin.
(a) (b)
rank 0
rank 1
rank 3
rank 3 rank 3 rank 2
Figure 5.5: Recursive construction a rank-4 binomial tree by (a) linking two
rank-3 binomial trees, or (b) joining rank-0 through rank-3 trees as siblings under
a new common root.
tion can be easily updated with no running time penalty during the merge process,
it does cause trouble for the operations delete, decrease-key, and increase-key, as we
discover in the following problem.
In this section we describe the binomial heap, an elegant priority queue data struc-
ture that performs all fundamental operations including merge in O(log n) time in
the worst case. Using the binomial heap as a stepping stone, we then describe the
Fibonacci heap, a more sophisticated relative designed to speed up decrease-key to
run in O(1) amortized time.
In all of the tree-based priority queues we have seen so far, elements are stored in a
single heap-ordered tree. By contrast, the elements in a binomial heap are divided
up into a collection of heap-ordered trees. Each element may potentially have many
children, stored in a doubly-linked list. Therefore, along with each element we store
its rank (number of children), a pointer to its parent, a pointer to its first child, and
pointers to its previous and next siblings.
The trees in a binomial heap come in specific shapes, known as binomial trees, that
are built in a recursive fashion, as shown in Figure 5.5. There is a unique shape
associated with every rank, where the rank of a tree is the number of children of
its root element. A rank-0 tree consists of a single element. More complicated trees
are recursively constructed: we either link two trees of rank j 1 (one as a child of
the other) to obtain a rank j tree, or we join trees of ranks 0 . . . j 1 under a new
root to obtain a tree of rank j. It is not hard to see from this construction that a
tree of rank j has depth j and contains 2j elements, 2j1 of which are leaves. The
name binomial tree comes from the fact that number
of elements at depth d in a
rank-j tree is exactly the binomial coefficient dj .
Binomial trees arise in a number of interesting algorithmic situations. For example,
suppose you broadcast a message from a single source node to all other nodes in
a network in a number of rounds, where in each round, every node that has heard
the message so far transmits the message to a distinct node that has not yet heard
it. The resulting transmission pattern will have the form of a binomial tree. In
Chapter 8, we will also study a close relative of the binomial tree known as the
binary indexed tree.
As shown in Figure 5.6, a binomial heap is built from a collection of binomial trees,
at most one of each rank, whose roots are all connected together in a doubly-linked
list. It is clear that there can be at most log n such trees represented in the list,
since a tree of rank larger than log n would contain more than 2log n = n elements.
Since all of the trees in the binomial heap satisfy the heap property, the minimum
element must reside at one of their root elements, so we can find it in O(log n) time
by scanning the root list.
Owing to the fact that binomial trees have sizes that are powers of two, there is a
only one unique configuration of trees that represents a valid binomial heap on n
elements, corresponding precisely to the binary representation of n. As shown in
Figure 5.6, if we form a binary number in which the j th bit indicates the presence
or absence of a tree of rank j, this gives the binary representation of n.
We will describe how to merge two binomial heaps in O(log n) time in a moment, but
let us first see how easy it is to write the remaining fundamental operations in terms
of merge. To insert an element in O(log n) time, we build a new 1-element binomial
heap and merge it with our existing heap. Remove-min is also simple to express
in O(log n) time in terms of merge. Observe from Figure 5.6 that if we remove the
root corresponding to the minimum element, the linked list of child subtrees of this
root is itself a valid binomial heap. Therefore, to remove the minimum element, we
splice out the root containing the minimum element and merge the binomial heap
consisting of its children back into the main heap. Decrease-key works the same as
in a regular binary heap, by using sift-up. Notice that sift-up doesnt change the
shape of any of our trees, and it runs in O(log n) time since every element belongs
to a tree of height O(log n). We can write delete in terms of decrease-key followed
by remove-min, and we can write increase-key by deleting an element and then
re-inserting it with a new key. Note that we could implement increase-key using
sift-down, but this would be somewhat slower (O(log2 n) time) since each node has
up to log n children; recall from problem 81 that as opposed to sift-up, sift-down
becomes slower when nodes have more children.
All that remains is to show how to merge two binomial heaps H1 and H2 in O(log n)
time. If H1 and H2 have n1 and n2 elements, the merge process corresponds exactly
to the binary addition of n1 and n2 . This makes intuitive sense, because the root
lists in H1 and H2 reflect the binary composition of n1 and n2 , and we are producing
a merged heap whose root list needs to reflect the binary composition of n1 + n2 .
When adding two binary numbers, we add each bit position starting from the least
significant; when we find several 1 bits in position j, we add two of them to form
a carry bit that is added to position j + 1. Translated to our merge operation, we
splice together the two root lists, starting from the smallest rank, and whenever we
find more than one tree of rank j, we link them together in O(1) time to form a
carry tree of rank j + 1. [Detailed explanation of the merge operation]
If we insert successive elements into a binomial heap, the resulting merge operations
1 0 1 0 1
root list:
rank 0
rank 1
rank 2 (absent)
rank 3
rank 4 (absent)
The Fibonacci heap cleverly extends the binomial heap to support decrease-key in
O(1) amortized time, which yields improvements in efficiency for several important
algorithms, such as Dijkstras shortest path algorithm and Jarnks (Prims) min-
imum spanning tree algorithm. However, these gains tend to be more theoretical
than practical, as they rarely outweigh the added overhead of the Fibonacci heap
(for example with Dijsktras shortest path algorithm, the author has not yet found
even a single real-world scenario where Fibonacci heaps are advantageous).
Starting from a binomial heap, two key enhancements appear in the Fibonacci heap:
Figure 5.7: Running times for the operations of several mergeable heaps. The
class of null path mergeable binary heaps from Sections 5.4.1 and 5.4.2 in-
cludes randomized mergeable heaps (whose running time bounds hold with high
probability), skew heaps (whose running time bounds are amortized), and also
leftist heaps, size-augmented heaps, and null-path-length-augmented heaps.
The running time bounds of these last three are deterministic as long as they
dont need to support delete, decrease-key, or increase-key, and otherwise they
are amortized.
The machinery above for marking and cutting may seem a bit mysterious at first,
but it gives us two important properties. First, it allows us to perform decrease-key
in O(1) amortized time. To decrease es key, we detach e from its parent, decrease
es key, and re-insert es subtree it into the list of roots. The act of detaching e from
its parent could initiate a cascading cut, but it turns out that large cascading cuts
happen infrequently. This is another excellent example of the recursive slowdown
principle (Section 4.5), since it takes two deletions from an element to cause a cut
to propagate to its parent, thereby slowing the rate of a cascading cut by 1/2 every
step up the tree. Amortized analysis of decrease-key is actually quite simple. We
associate one unit of credit with every marked node, allowing it to pay for its part
(a) (b) 0 1
0 1
0 1 1 0 1
000 001 011 110 111
Figure 5.8: Illustrations of (a) a simple two-list priority queue data structure of
elements whose keys only take the values zero and one, and (b) the generalization
of this structure to a radix tree. Each leaf node in the radix tree contains a single
element, whose key is written above in binary.
in any cascading cut. The only place decrease-key needs to actually pay is at the
very end of the cut, where it finally reaches an unmarked element e. Here, it pays
1 unit to delete a child from e and mark e, and then 1 more unit to install the
necessary credit on e.
The second important property afforded by the cutting machinery above is the
following: in a binomial heap, a tree of rank j contains exactly 2j elements. This
fact was important to guarantee that there were O(log n) subtrees in the heap.
For the case of the Fibonacci heap, it is no longer the case that a rank j tree
contains exactly 2j elements, since decrease-key can detach subtrees within a tree.
This is slightly worrisome, since the amortized O(log n) running time of remove-min
depends on the fact that only O(log n) trees remain after consolidating the root list.
Fortunately, since we are careful to delete at most one child from every non-root
element, we can show that a tree of rank j must contain at least Fj+2 elements,
where Fj denotes the j th Fibonacci number [Proof]. As one might guess, this fact
explains how Fibonacci heaps get their name. Since Fj is exponentially large in j
(one can show that Fj+2 j , where 1.618 is the so-called golden ratio), a
tree of rank larger than log n cannot exist in an n-element Fibonacci heap, so our
root list will still contain only O(log n) elements after consolidation, as desired.
The two-list data structure for C = 2 shown in Figure 5.8(a) can be generalized
via recursion to a much more versatile and powerful structure called a radix tree,
shown in Figure 5.8(b). A radix tree is a binary tree whose left subtree is a radix
tree recursively constructed from all keys having a most significant bit of zero, and
whose right subtree is a radix tree constructed from keys having most significant
bit of one. At the next level, the tree branches on the 2nd most significant bit, and
so on down the tree. Elements are stored in leaf nodes, and the root-to-leaf path for
each element corresponds precisely to its binary representation zero for a step
to the left, one for a step to the right. Every key is represented by log C bits4 , so
the tree has height log C. Since we do not store empty subtrees, a radix tree on
n elements requires only O(n log C) space, because we store log C nodes along the
root-to-leaf path for each element.
The radix tree is actually a very general data structure that has many uses beyond
serving as a good priority queue, which we discuss in Chapter 7. Used as a priority
queue, it supports all fundamental operations in O(log C) time, all relatively easy
to implement. For example, to insert a new element with key k, we walk downward
from the root according to the binary representation of k, adding nodes when neces-
sary until we finally deposit the new element as a leaf. To remove the minimum, we
first locate the leaf containing this element by walking down from the root (always
moving left when possible), and after removing it we walk back up the tree cleaning
up any subtrees that become empty as a result.
Another way to generalize our trivial two-list structure for C = 2 to higher values
of C is to build it out horizontally, as shown in Figure 5.9(a), giving an array
A[0 . . . C 1] of C buckets, where A[k] points to a list of all elements with key
k. Here, all standard priority queue operations take O(1) time except remove-min,
which takes (C) worst-case time since it requires scanning through A to find the
first non-empty bucket.
For large values of C, either the (C) worst-case running time of remove-min or
the (C) space required to store the structure may be prohibitively large. To
remedy this, we again generalize our structure in a hierarchical fashion, arriving at
what is sometimes called a multilevel bucket data structure. For example, by taking
C = 100 with k = 2 levels
of hierarchy, we get the structure shown in Figure 5.9(b).
The top level contains C super buckets, each corresponding to a range of C
different
key values. A non-empty top-level bucket points to a second-level array of
C buckets, each containing a list of elements with a specific key.
We can easily extend such a data structure from a tree of height 2 to one of height
k, where each tree node is an array of buckets of length B = C 1/k . If we take
k = log C, we get a structure with log C levels where every bucket array has length
B = C 1/ log C = 2. This structure should look familiar, as it is precisely the radix
tree! In fact, the multilevel bucket data structure is nothing more than a B-ary
radix tree, where we write our keys in base B, and at every node we branch in one
4 We usually assume here that C is a power of two, making log C an integer.
20 21 29
...
A[2]: list of elements with key = 2
(a) (b)
Figure 5.9: The multi-level bucket data structure: (a) a one-level array of C
buckets, and (b) a two-level structure, which one
can interpret as a two-level
tree where every node contains an array of length C.
of B ways based on the value of a particular digit. For the tree in Figure 5.9(b), we
write our keys in base 10.
Suppose our the tree has height k, so each node has a width of B = C 1/k buckets.
Since insert, delete, decrease-key, and increase-key only require walking vertically
through the tree, they take O(k) time. This is constant for the 2-level structure
in Figure 5.9(b), but O(log C) in a radix tree. By contrast, remove-min requires
scanning horizontally through each node (to locate the first non-empty bucket) as
1/k
it walks down the tree, so its running time is O(kB) = O(kC ). For the 2-level
structure in Figure 5.9(b), this is O( C); for the radix tree, it is O(log C). As you
can see, the radix tree balances the cost of all operations at O(log C).
We can often gain efficiency in the special case of a monotone integer priority
queue, where it is guaranteed that the sequence of minimum elements removed
from the queue will be monotonically nondecreasing. That is, if k denotes the key
of the most-recently-removed minimum element, then we promise never to insert
an element with key less than k, and we also promise never to decrease the key
of an existing element to a value less than k. Such monotonic behavior arises in
many important applications; for example, Dijkstras shortest path algorithm uses
a monotone priority queue to processes the nodes of a graph in nondecreasing order
of distance from a given source node.
For many applications (again, Dijkstras algorithm is a good example), the number
of calls to decrease-key is expected to be significantly higher than the number of calls
to insert and remove-min. This was one of the main motivations for developing the
Fibonacci heap, since it allows for decrease-key to run in O(1) amortized time. It
turns out that monotonicity allows the radix tree / multilevel bucket data structure
to perform decrease-key also in O(1) amortized time, giving a data structure often
called a radix heap.
The radix heap is a lazy radix tree. Monotonicity tells us that elements will be
removed from the left subtree of the root for a while, but if remove-min ever hap-
Figure 5.10: Running times for the operations of integer priority queues. The
improved monotone radix heap data structure refers to the result of problem 83.
pens to extract an element from the right subtree, we know that the left subtree will
never again be used. Elements in the right subtree are in some sense not impor-
tant until we reach this crossover point, so instead of maintaining them in a proper
radix tree, we store them in a simple doubly-linked list, just as in Figure 5.8(a).
This allows elements in the right subtree to be inserted, deleted, and have their
keys decreased efficiently. When we reach the crossover point in time when remove-
min needs to extract from the right subtree, only then do we build and henceforth
maintain a tree on these elements. Moreover, we apply the same technique to this
tree, leaving its right subtree unbuilt until absolutely necessary, and so on. It is
easy to show that this approach reduces the running time of decrease-key to O(1)
amortized. [Details]
2 3
0 4 5 6
7 0 8 0 0 0
0 0 0 9
0 0
i = 10
1 2 3 4 5 6 7 8 9 11 12 13 14 15 16 17 18 19
A: 1 2 3 0 4 5 6 7 0 8 0 0 0 0 0 0 9 0 0
left-child(i)
right-child(i)
the sequence, outputting at each location the median element within the window. There
are several nice ways to implement a median filter in O(n log k) time (the best possible
in the comparison model). For example, we can store the contents of the window in a
balanced binary search tree (discussed in the next chapter), which supports the operations
insert, delete, and select (for finding the median) all in O(log k) time. Since a balanced
binary search tree can be complicated to implement, however, please discuss how you
might implement a median filter in O(n log k) time using a pair of binary heaps instead.
In general, show how to build a data structure out of two binary heaps capable of tracking
the rth order statistic of a dynamic data set (with r known in advance and unchanging
over the lifetime of the structure) such that insertion or deletion takes only O(log n) time.
[Solution]
Problem 90 (Level-Order Encoding of an Arbitrary Binary Tree). We
have emphasized earlier that one of the benefits of a binary heap being shaped like
an almost-complete binary tree is that one can represent the heap in memory using
nothing more than a simple array. Moreover, we can step around within the array as if
we were moving in the tree, since the left and right children of the node at index i are
located at indices 2i and 2i + 1. In this problem, we show that a generalization of this
mapping allows us to represent any static binary tree within an array, also allowing for
easy tree-based movement. As shown in Figure 5.11, we map an n-node tree into an array
A[1 . . . 2n + 1] in a level-by-level fashion, storing the nodes of the tree as well as a set
of dummy external nodes representing null spaces at the bottom of the tree (these are
stored as zeros in the array). Note that the almost-complete binary heap is just a special
case of this mapping, where all the external nodes lie at the end of the array. Movement
from parent to child generalizes in a pleasantly simple way: please prove that the children
of the node at index i live at indices 2A[i] and 2A[i] + 1. We will revisit this mapping
later in problem 138, when we use it to develop a succinct data structure for encoding
a static, rooted tree. [Solution]
(a) Given a pointer to the root of a Braun tree satisfying the heap property, show how
to insert a new element in O(log n) time while maintaining the heap property. As a
hint, you may want to draw inspiration from the way nodes in a skew heap swap their
children. [Solution]
(b) Given a pointer to the root of a Braun tree satisfying the heap property, show how
to remove the root (the minimum element) in O(log2 n) time. Similarly, given a
pointer to an arbitrary element, show how to delete that element in O(log2 n) time,
thereby enabling support for operations like decrease-key and increase-key. All of your
operations should preserve the heap property. [Solution]
(c) For a challenge, show how to compute the size of a Braun tree in O(log2 n) time, given
only a pointer to its root. [Solution]
In this problem, we show how to take any fast priority queue (not even one supporting
merge) and make it mergeable, while also supporting the disjoint set find operation. Sup-
pose we take the highly efficient tree-based data structure for disjoint sets from Section
4.6 (supporting union and find in O((n)) amortized time) and modify it slightly. In each
tree, we will store elements only at leaves, and each non-leaf (internal) node will maintain
a priority queue. Please determine what we should store in these priority queues so that if
our original priority queue operations run in O(T ) amortized time, the operations insert,
remove-min, find, and merge/union all now take O(T (n)) amortized time in this hybrid
data structure. [Solution]