Tutorials › Graph Theory (Undergraduate) › Priority Queues And Dijkstra’s Running Time

Shortest Paths · Tutorial 241 of 799

Priority Queues And Dijkstra's Running Time

See how the priority-queue representation changes Dijkstra’s running time, and trace three implementations on the same graph.

Intermediate 12 min read

What You'll Learn

  • State Dijkstra’s assumptions and the role of the unsettled-vertex priority queue
  • Trace minimum-label selection with an unsorted array
  • Explain lazy binary-heap entries and why obsolete entries are skipped
  • Use an indexed priority queue to support decrease-key
  • Derive each implementation’s time and space bounds
  • Recognize why changing a heap key requires restoring the heap property

Why the Priority Queue Matters

Dijkstra’s algorithm repeatedly chooses an unsettled vertex with the smallest tentative distance, then relaxes the outgoing arcs from that vertex. The distance-label updates are the same in each implementation; what changes is how the next minimum is found and how a decreased label is handled. That choice determines the running time.

Throughout, let \(G=(V,E)\) be a finite directed graph with nonnegative arc weights, \(n=|V|\geq 1\), and \(m=|E|\). Write \(w(u,v)\) for the weight of an arc \(uv\). Assume an adjacency-list representation, so the outgoing arcs of a vertex can be scanned in time proportional to their number. A source \(s\in V\) is given. The algorithm keeps a tentative label \(d[v]\), initially \(d[s]=0\) and \(d[v]=\infty\) for \(v\ne s\). When an unsettled vertex \(u\) is selected, each outgoing arc \(uv\) is relaxed: if \(d[u]+w(u,v)<d[v]\), set \(d[v]=d[u]+w(u,v)\).

Definition: A priority queue stores items with keys and supports inserting an item, finding or removing an item with minimum key, and, in some implementations, decreasing an item’s key. In Dijkstra’s algorithm, the items are vertices and their keys are tentative distance labels.

The nonnegative-weight assumption is essential to Dijkstra’s correctness. The earlier material on the settled-vertex invariant establishes that, when the minimum tentative label is selected, that label is the true shortest-path distance. Each implementation below must therefore select a minimum current label; the data structure affects the cost of doing so, not the invariant itself.

Three Ways to Maintain the Minimum

An unsorted array keeps the unsettled vertices and their current labels, but does not keep them in key order. To extract the minimum, scan all unsettled vertices. Updating a label is constant time once its vertex is known. The array is simple, but each extraction can require a long scan.

A binary heap stores entries in a complete binary tree, represented by an array, with each parent’s key no greater than its children’s keys. The root is a minimum. Insertion and removal of the root take logarithmic time by moving an entry up or down the tree. One convenient version for Dijkstra does not implement decrease-key: every strict improvement inserts a new entry \((d[v],v)\). When an entry is removed, it is discarded if its key is no longer \(d[v]\), or if \(v\) has already been settled. This is the lazy binary-heap implementation.

An indexed priority queue stores at most one entry per vertex and maintains an index giving each vertex’s current heap position. When a label decreases, the queue changes that entry’s key and moves it up the heap as needed. This operation is called decrease-key. The position index is what distinguishes this implementation from a heap that has no direct way to locate a vertex.

Result: With adjacency lists, an unsorted array takes \(O(n^2+m)\) time. A lazy binary heap takes \(O(n+(m+1)\log(m+2))\) time. An indexed priority queue takes \(O((n+m)\log(n+1))\) time. For a simple directed graph, \(m=O(n^2)\), so the lazy-heap bound is also \(O((n+m)\log(n+1))\). These are worst-case upper bounds.

Worked Traces on One Weighted Digraph

Worked Example: Unsorted-Array Dijkstra

Let \(V=\{s,a,b,c,d\}\) and let the weighted directed arc set be \(E=\{(s,a,4),(s,b,1),(b,a,2),(a,c,1),(b,c,5),(c,d,3),(a,d,7),(b,d,10)\}\), where each triple gives tail, head and weight. The source is \(s\). The tentative labels initially are \(d[s]=0\) and \(d[a]=d[b]=d[c]=d[d]=\infty\).

At each row, scan the unsettled labels and remove the smallest. Then scan every outgoing arc of the selected vertex and apply any strict improvement.

Selected vertexMinimum found by scanLabel changes after relaxing outgoing arcsLabels \(s,a,b,c,d\)
\(s\)\(0\)\(a:\infty\to4,\ b:\infty\to1\)\(0,4,1,\infty,\infty\)
\(b\)\(1\)\(a:4\to3,\ c:\infty\to6,\ d:\infty\to11\)\(0,3,1,6,11\)
\(a\)\(3\)\(c:6\to4,\ d:11\to10\)\(0,3,1,4,10\)
\(c\)\(4\)\(d:10\to7\)\(0,3,1,4,7\)
\(d\)\(7\)No outgoing arcs\(0,3,1,4,7\)

The extraction order is \(s,b,a,c,d\). For example, when \(b\) is selected, the candidate through \(b\) to \(a\) has length \(1+2=3\), which improves \(4\); the candidate to \(c\) has length \(1+5=6\); and the candidate to \(d\) has length \(1+10=11\). The final labels are \(0,3,1,4,7\).

Check: the routes \(s\), \(s\to b\), \(s\to b\to a\), \(s\to b\to a\to c\), and \(s\to b\to a\to c\to d\) have respective weights \(0\), \(1\), \(1+2=3\), \(1+2+1=4\), and \(1+2+1+3=7\). Also, the array performs \(5+4+3+2+1=15\) label inspections to find its five minima; this is the triangular sum \(5\cdot6/2=15\). It scans \(8\) outgoing arcs in total, once each.

Worked Example: Lazy Binary-Heap Dijkstra

Use the same explicitly defined weighted digraph from the preceding example. Heap entries are written \((k,v)\), with key \(k\). The table records the entries as a collection satisfying the heap order; the heap may store them in different internal array arrangements. A strict label improvement inserts a new entry, leaving the old one in the heap.

Entry removedActionHeap entries after the action
\((0,s)\)Relax \(sa,sb\); insert \((4,a),(1,b)\)\((1,b),(4,a)\)
\((1,b)\)Improve \(a\) to \(3\); set \(c=6,d=11\); insert those entries\((3,a),(4,a),(6,c),(11,d)\)
\((3,a)\)Improve \(c\) to \(4\) and \(d\) to \(10\)\((4,c),(4,a),(6,c),(10,d),(11,d)\)
\((4,c)\)Improve \(d\) to \(7\)\((4,a),(6,c),(7,d),(10,d),(11,d)\)
\((4,a)\)Discard: \(a\) is already settled\((6,c),(7,d),(10,d),(11,d)\)
\((6,c)\)Discard: \(c\) is already settled\((7,d),(10,d),(11,d)\)
\((7,d)\)Settle \(d\); it has no outgoing arcs\((10,d),(11,d)\)
\((10,d),(11,d)\)Discard both: \(d\) is already settledEmpty

The current labels at settlement are again \(d[s]=0,d[b]=1,d[a]=3,d[c]=4,d[d]=7\). The entries \((4,a),(6,c),(10,d),(11,d)\) are obsolete: each was created before a smaller label for that vertex was found, or remained after that vertex was settled.

Check: there are \(8\) arcs, so there can be at most \(8\) successful relaxations and at most \(1+8=9\) inserted entries, including the source entry. In this trace, the inserted entries are \((0,s),(4,a),(1,b),(3,a),(6,c),(11,d),(4,c),(10,d),(7,d)\), exactly \(9\). The final labels agree with the route weights checked in the array example.

Worked Example: Indexed-Priority-Queue Dijkstra

Again use the same vertex and weighted arc sets. This time the queue stores one entry per discovered, unsettled vertex, shown as \(v:k\). A label decrease changes the existing entry rather than adding another one.

Vertex removedQueue before removalUpdates to remaining queue
\(s:0\)\(s:0\)Insert \(a:4,b:1\)
\(b:1\)\(b:1,a:4\)Decrease \(a:4\to3\); insert \(c:6,d:11\)
\(a:3\)\(a:3,c:6,d:11\)Decrease \(c:6\to4\) and \(d:11\to10\)
\(c:4\)\(c:4,d:10\)Decrease \(d:10\to7\)
\(d:7\)\(d:7\)Queue becomes empty

Each vertex is inserted only when its label first becomes finite, and later improvements decrease its existing key. Thus this trace has \(5\) extractions, \(4\) later decreases, and no obsolete entries. Its output labels are \(0,3,1,4,7\).

Check: the indexed queue stores at most \(5\) vertices at once, since there are \(5\) vertices in \(V\). The final labels are verified by the same five route weights \(0,1,3,4,7\); the arc scans and label changes match the earlier traces.

Why the Bounds Hold

Unsorted array. Initializing the \(n\) labels takes \(O(n)\) time. If the algorithm extracts a minimum for every vertex, the scans inspect \(n,n-1,\ldots,1\) unsettled labels, for \(n(n+1)/2=O(n^2)\) inspections. If it stops when the smallest remaining label is \(\infty\), it performs fewer scans, so the same upper bound holds. Each reachable vertex’s outgoing arcs are scanned at most once, contributing at most \(O(m)\). Hence the total is \(O(n^2+m)\), with \(O(n+m)\) space for labels, settled status, and adjacency lists.

Lazy binary heap. Every outgoing arc of a settled, reachable vertex is scanned at most once, so there are at most \(m\) relaxations. Only a strict improvement inserts an entry; therefore at most \(m+1\) entries are ever inserted, counting the source. A binary heap containing at most \(m+1\) entries takes \(O(\log(m+2))\) time per insertion or removal. Each inserted entry is removed once, and each arc scan takes constant time apart from a possible insertion. Initialization and bookkeeping cost \(O(n+m)\). The total is \(O(n+m+(m+1)\log(m+2))\), equivalently \(O(n+(m+1)\log(m+2))\). The heap can hold \(O(m+1)\) entries, so its additional space is \(O(m+1)\).

For a simple directed graph, \(m\leq n(n-1)\), so \(\log(m+2)=O(\log(n+1))\). Substitution gives the familiar \(O((n+m)\log(n+1))\) upper bound. Keeping the \(\log(m+2)\) expression is useful when stating the lazy-heap count directly in terms of the number of inserted entries.

Indexed priority queue. Each vertex is inserted at most once, extracted at most once, and has its key decreased at most once per successful relaxation. Thus there are at most \(n\) insertions and extractions and at most \(m\) decrease-key operations. Each heap operation costs \(O(\log(n+1))\), since the heap contains at most \(n\) vertices. Scanning adjacency lists costs \(O(m)\), so the total is \(O((n+m)\log(n+1))\). The queue, position index, labels and settled status require \(O(n)\) additional space.

Why the lazy rule is safe: For an unsettled vertex with a finite current label, the entry for its current label was inserted when that label was established. Any older entry for that vertex has a larger key, because entries are added only after strict improvements. Thus an obsolete entry cannot be mistaken for a smaller current label. If the vertex has already been settled, the entry is discarded. Consequently, removing and discarding obsolete entries still processes each vertex at its current minimum label.

Proof. Consider a removed heap entry \((k,v)\). If \(v\) is settled, the algorithm discards it and performs no relaxation. If \(v\) is unsettled and \(k\ne d[v]\), then \(k>d[v]\): the entry was created from an earlier label, and every later replacement of that label was a strict decrease. The current finite label \(d[v]\) has its own entry in the heap, inserted when that label was established. That entry has smaller key than \(k\), so the heap cannot remove \((k,v)\) as its minimum while the current entry remains. Therefore an entry that is actually used to settle a vertex has key \(d[v]\), the current minimum tentative label. The settled-vertex invariant applies to that selection, so the lazy implementation makes the same valid settlement as a queue that supports decrease-key. \(\square\)

A Common Heap Mistake

Changing a key in a binary heap is not automatically a constant-time decrease-key operation. The heap order must be restored, usually by moving the changed entry upward. For a concrete failure, let \(V=\{s,a,b,c,d\}\) and let the weighted arcs be \(E=\{(s,a,8),(s,b,1),(s,c,3),(b,a,1),(a,c,0),(c,d,1)\}\). All weights are nonnegative.

After removing \(s\), a valid heap array can have keys \([1:b,8:a,3:c]\). Removing \(b\) leaves \([3:c,8:a]\). Relaxing \(ba\) changes \(a\)'s key from \(8\) to \(2\). If the implementation changes the child’s key but does not sift it upward, the array remains \([3:c,2:a]\), which violates the heap property. It may remove \(c\) with key \(3\) before \(a\) with key \(2\). It then sets \(d[c]=3\) and \(d[d]=4\). When \(a\) is finally processed, it offers \(d[c]=2\), but \(c\) has already been settled; the algorithm can therefore return the incorrect labels \(d[c]=3,d[d]=4\).

The correction is to restore the heap order after every decrease-key, or to use the lazy strategy and insert a new \((2,a)\) entry. In the latter case the minimum is removed in the correct order, and the obsolete \((8,a)\) entry is later discarded. Dijkstra’s running time is therefore not just about the number of graph arcs: it also depends on whether the queue implements the required priority updates correctly.

Check Your Understanding

Use the stated assumptions of nonnegative weights and adjacency-list input.

  1. Why can an unsorted array take \(O(n^2+m)\) time even if there are few arcs?
  2. In a lazy heap, why must the algorithm discard an entry whose key differs from the current label?
  3. How many entries can the lazy heap insert in terms of \(m\), including the initial source entry?
  4. What data does an indexed priority queue need in order to find a vertex’s heap position for decrease-key?
  5. For a simple directed graph, explain why the lazy-heap bound can be written using \(\log(n+1)\).