Single source shortest path is one of most popular the topics taught in a introductory algorithm course. Given a graph with vertices and edges, the famous Dijkstra's algorithm has runtime by using a binary heap. When more advanced heap such such Fibonacci heap is deployed, the runtime can be improved to . Most introductory courses are satisfied with the above solutions without going further.
Here we explore the topic by considering a special case where all edge weights are non-negative integers within the set . We develop new heaps incrementally and we will finally see that Dijkstra algorithm can do substantially better than .
Time: , Space: .
As a warm up, we demonstrate an algorithm with runtime and space overhead . Here we exclude the space required for storing the graph to simplify the discussion and comparison between the algorithms.
The idea is very simple. We know that the possible distance from any vertex to the source vertex (denoted as ) is no less than . It suffices to keep an array with length , such that maintains the set of vertices with temporary distance . In this context, we call a bucket and the array itself the buckets.
Initially all 's are empty except that contains the source vertex. The algorithm iterates over to find an unmarked vertex with the minimum distance, marked it, update its neighbors' distances and move them to the corresponding buckets.
Each vertex is inserted into once. There are at most updates of vertices' distances. As the minimum distance of the unmarked vertices is monotonically increasing, we scan the array only once. Summing over the cost we get the time complexity bound.
Time: , Space: .
The space usage of the above algorithm can be improve to . Relabel the vertices as according to the order they are marked.
Suppose that Dijkstra's algorithm uses an array to record the vertices' distances to . Initially, and for . Now, consider the moment that () vertices have been marked. For , the value of is either
-
, if is not adjacent to any vertex for . In this case is not in the array .
-
for some , where is the edge weight between and .
By the property of Dijkstra's algorithm, at this moment it also holds that
Therefore, for , either is not in or
All un-marked vertices that are in must be in the range of
In general, let be the distance of the least marked vertex. It suffices to maintain a window of . We can implement this by initializing with size and use it in a wrap-around manner.
Time: , Space: .
In previous section, we successfully reduce the size of to . But the runtime remains . To motivate the algorithm discussed in this section, we first provide an alternative view of this complexity:
-
We update the vertices' distance and move the vertices between buckets, which has cost ;
-
At each iteration of Dijkstra's algorithm, in the worst case, we need to scan the entire array of to find an unmarked vertex with minimum distance, which takes times. This is repeated times.
Where could we have wasted our time? Do we really have spend time to find the unmarked vertex with minimum distance?
We can improve this to by using two-level buckets. We break the buckets in into blocks, each of size (assume here is an integer for simplicity). For each block, we use one flag bit to indicate whether this block is empty. This results in flags bits, which are stored in bit array . From now on,

Now, to find an unmarked vertex with minimum distance, we scan to find the first non-zero bit then the corresponding bucket in , which takes time. Totaling the runtime, we obtain a bound of .
Question to ponder: calculate the size of for some values of which you consider reasonable in real applications.
Time: , Space: .
It is natural to extend the idea to -level buckets. Suppose the block size if . Then the third level contains a single bit block of size , the second level contains bit blocks (with total size ), and the first level contains the buckets of size . It follows that and the time complexity is .
We may use four level buckets, in which and the time complexity is .
Question to ponder: calculate the size of and for some values of which you consider reasonable in real applications.
Does the analysis carry for -level buckets for general value of ? No. We can no longer consider the as a constant and have to take into consider the overhead of insertion, decrease-key and deletion explicitly.
For a -level bucket structure with bucket size ,
-
insertion takes time,
-
decrease-key takes time,
as we need to maintain the indicators bits in levels. Finally,
- delete-min takes time,
as we need to scan buckets, each of size , to find the min element and delete it.

The time complexity now becomes
Balancing the two terms, we get , i.e., . To summarize, the time is
and the space overhead is
Question to ponder: calculate the size of for some values of which you consider reasonable in real applications.
Time: , Space: .
The core idea underlying the k-level bucket structure discussed in the previous section is that every integer in can be written as a base- number with length at most :
where . For convenience, we write it as
Now the meaning of the -level buckets is clear. The level partitions the 's according to the value of . The level further partitions the 's according to the value of , and so on. The path from the top level to bottom level is uniquely determined by the sequence . Indeed, this is a special case of trie, which has alphabet size and contains only sequence of length .
But we may still waste some work. Remember that our goal for designing the data structure is to find the min element. Therefore, it suffices to distinguish the min element from the rest. We should not waste our effort in determining the relative order between the non-min elements.
In the following example, the min elements fall into the subtree of the first bucket on the level while fall into the last one. As are not min elements, it is unnecessary to maintain a subtree to separate them. Worse still, if later decrease-key is applied to either or , and if the new key has to be moved to a new bucket in the level, the effort we have spent in constructing the sub-tree is completely useless.
Instead, we can create an auxiliary set to the last bucket on level , to pus into the set directly. Similar idea apply to in the example.

The new structure we get is as follows:

In general, we may not even has a -level trie. In the example below, we can already determine the min-element at some level . Why should we bother to expand the buckets further? Note that this idea is similar to that of trie compression used in Patricia trie.

We can be even more lazy. We expand a bucket only when it is necessary. See the example below as an example. In this example, and .

Now, to delete-min, we need to determine the order of .

Inserting a new element follows the current structure, until it finds a bucket to fall in.

Delete-min begin the search from the lowest level non-empty bucket block.

After delete-min, we may need to delete the empty bucket blocks recursively. For each bucket block, we maintain counter for the number of non-empty buckets. This allows us to delete empty bucket blocks in time.

We decrease the key of to 9. Note that we do not need to compare and for the moment. Bucket block counter is maintained when is moved to another bucket.

Delete-min is invoked again. We need to expand the bucket containing and to distinguish them.

We continue to insert a new element .

We decrease the key of to . It is pushed down by one level.

We are now prepared to analyze the amortized cost of these operations:
-
Insert: we pay an amortized cost of insertion. Any subsequent pushed down operation on this element is charged to its insertion. Note that an element can be push-down by at most levels.
-
Decrease-key: we pay an amortized cost of decrease-key operation. The push-down operation is charged to its insertion.
-
Delete-min: to find the min-element, we scan the bucket block in the lowest level to find the first non-empty bucket. This has time . If the bucket found contains multiple elements, we perform an expansion and push the elements down, the cost of which is charged to insertion of these elements. Finally, we may need to delete empty bucket blocks recursively, which has cost .
Therefore, the Dijkstra's algorithm with this structure has runtime , which is minimized when
Solving the equation gives .
Time: , Space: .
Beginning from the naïve approach, we have improved a lot on both time and space overhead. Amazingly, this is not the end of the story. To motivate what is possibly the inefficient part of approach, look at the following example.
Suppose that is a large number. Initially the structure is empty. We insert an element followed by and they fall into the same bucket in the level. Next, we perform a delete-min operation. To distinguish, we expand the buckets all the way down to level.

But since there are two elements, why don't we compare them directly and return the minimum one? In such case we avoid the unnecessary expansions.
We can extend the idea further. If the number of elements in a bucket is too few, we refuse to expand it. Instead we make of copy of the elements and construct a comparison based priority queue (e.g., Fibonacci heap) on them. Further, note that we are only interested in the possible min-element. Therefore the heap is constructed for only one bucket that contains the min-element.
We use a parameter (to be determined) to control this number. In particular, if the number is at most , we construct a comparison based priority queue. We perform expansion only if the number exceeds .
-
Insert: insert an element as before. If the element falls into the bucket that is associated with a priority, then also insert this element into the heap.
-
Decrease-key: if the element is in the bucket that is associated with a priority queue, perform decrease-key in the queue. Otherwise, perform decrease-key as before. If the element falls into the bucket that is associated with a priority queue, then also insert this element into the heap.
-
Delete-min:
- Find the first non-empty bucket in the lowest level
- IF the bucket has more than elements THEN
- Expand the bucket
- GO TO STEP 1
- ELSE
- IF queue THEN
- Construct a queue
- Perform delete-min in the queue
Let be the time needed to perform insert, decrease-key and delete-min operation in a comparison based priority queue respectively. Then it is obvious that the the structure we propose, the amortized time we need for insert and decrease-key is
- Insert: .
- Decrease-key: .
To analyze the time needed for delete-min is more complicated. It relies on the implementation of the following:
Find the first non-empty bucket in the lowest level.
We record the previous deleted min element (initially set to 0). Denote the -base representation of as
Observe that the non-empty bucket block in the lowest level must contain . Denote this level as . To find the first non-empty bucket, we start scan from bucket in the level. Therefore, each bucket block is only scanned once. Further, the buck block in the level is created from the level because it contains more than elements. We can charge the scanned cost to these elements, each with . As there are level, a element could be charge at most times. It follows that the amortized cost of delete-min is given by
- Delete-min:
For Fibonacci heap, and . Therefore, we have runtime
To minimize it, we need to set . It holds that and , which implies that and . The time is
and the space overhead is .
Reference
[1]. David R. Karger, MIT Advanced Algorithm, 2013.
[2]. B. V. Cherkassky, A. V. Goldberg, and C. Silverstein, “Buckets, Heaps, Lists, and Monotone Priority Queues,” SIAM J. Comput., vol. 28, no. 4, pp. 1326–1346, Jan. 1999