Dijkstra's Shortest Path Algorithm Runtime
Pseudocode
Pseudocode for Dijkstra's algorithm is provided below. Remember that the priority value of a vertex in the priority queue corresponds to the shortest distance we've found (so far) to that vertex from the starting vertex. Also, you can treat our priority queue as a min heap.
Add the starting vertex s to the initially empty fringe with priority value 0
Add all other vertices to the fringe with priority value of infinity
While the fringe is not empty:
Remove the vertex in the fringe with the minimum priority.
We'll call this vertex u.
Its priority is the shortest distance from s to u.
For each of u's neighbors v:
If v is not already in the priority queue, do nothing.
(We've already found the shortest distance from s to v.)
Otherwise, update v's predecessor to u, and update its priority value to the minimum of:
Its current priority value
The shortest distance from s to u + the weight of the edge (u, v)
Calculating running time
Now let's calculate the running time of Dijkstra's algorithm using a binary min-heap priority queue as the fringe. Let |E| and |V| be the number of edges and the number of vertices in the graph respectively. (In graph theory, E and V refer to the set of a graph's edges and the set of a graph's vertices; |E| and |V| are the sizes of these sets.)
  1. Adding all |V| vertices of the graph to a binary min-heap takes a total of O(|V|) time. (Remember the linear time heapify example from lecture?)
  2. Updating a vertex's priority value in the priority queue can be done with a binary min-heap by removing it, then readding it with an updated priority value. If we keep a hash map of vertices and their indices in the binary min-heap array and assume that the hash map put operation takes constant time, then we can remove and readd a vertex to a binary min-heap in O(log|V|) time. This happens to a vertex at most once for each of its neighbors. Thus, this will happen at most |E| times over the course of an entire run of Dijkstra's algorithm. Since each priority value update takes O(log|V|) time, the total of all priority value updates takes O(|E|log|V|).
  3. If we keep an adjacency matrix of edge weights, then we can access edge weights in constant time. If we keep a hash map of vertices with their priority values, then accessing a vertex's priority value is also a constant time operation. This makes calculating a vertex's updated priority value a constant time operation. Since we only have to calculate updated priority values O(|E|) times, all update calculations take a total of O(|E|) time.
  4. Notice that each vertex is removed from the fringe exactly once, and never readded to the fringe (excluding priority value updates). Dijkstra's algorithm only removes from the priority queue |V| times, and each removal takes O(log|V|) time for a total of O(|V|log|V|) time for all vertex removals.
  5. Checking whether the priority queue is empty is a constaint time operation and happens O(|V|) times (once right before each vertex is removed from the priority queue). Thus, all isEmpty checks take a total of O(|V|) time.
  6. Iterating through a vertex's neighbors can be done in time proportional to that vertex's degree (the number of neighbors it has) with an adjacency list. Therefore iterating over all vertices' neighbors over the course of a run of Dijkstra's algorithm takes O(|E|) time.
Adding these running times together, we have O(|E|log|V|) for all priority value updates and O(|V|log|V|) for removing all vertices (there are also some other O(|E|) and O(|V|) additive terms, but they are dominated by these two terms). This means the running time for Dijkstra's algorithm using a binary min-heap as a priority queue is O((|E|+|V|)log|V|).
A directed graph is weakly connected if replacing all of its directed edges with undirected edges produces a connected undirected graph. If we assume that the input graph is weakly connected, then the graph must have at least |V| - 1 edges. This implies that |V| is in O(|E|), which means we can simplify the running time above to O(|E|log|V|).
Fun fact: With a more advanced data structure called a Fibonacci heap, the running time of Dijkstra's algorithm can be reduced to O(|E|+|V|log|V|)
Activity: Coding Dijkstra's algorithm
Add Dijkstra's algorithm to Graph.java from the previous lab. Here's the method header:
public ArrayList<Integer> shortestPath(int startVertex, int endVertex) {
// YOUR CODE HERE
}
For this method, assume that each edge in the graph has a myEdgeInfo object that is an Integer. Note: you do not have to implement the O((|E| + |V|)log|V|) optimized version of Dijkstra's algorithm described above (although you can if you want a challenge).