Based on theory, the implementation using adjacency matrix has a time complexity of E+V^2 and the implementation using min heap has a time complexity of (E+V)logV where E is the number of edges and V is the number of vertices.
When E>>V, such as for a complete graph the time complexity would be V^2 and (V^2)logV. This would mean that the implementation using min heap should be slower.
However I tested both implementations and found that the runtime for min heap is faster. Why is this so?
Here is my implementation:
- adjacency matrix and unsorted list
def dijkstraUnsortedArr(graph, start):
distances = [math.inf for v in range(len(graph))]
visited = [False for v in range(len(graph))]
predecessors = [v for v in range(len(graph))]
distances[start] = 0
while True:
shortest_distance = math.inf
shortest_vertex = -1
for v in range(len(graph)):
if distances[v] < shortest_distance and not visited[v]:
shortest_distance = distances[v]
shortest_vertex = v
if shortest_vertex == -1:
return [distances, predecessors]
for v in range(len(graph)):
edgeweight = graph[shortest_vertex][v]
if edgeweight != 0 and not visited[v]:
pathdist = distances[shortest_vertex] + edgeweight
if pathdist < distances[v]:
distances[v] = pathdist
predecessors[v] = shortest_vertex
visited[shortest_vertex] = True
- adjacency list and min heap
def dijkstraMinHeap(graph, start):
distances = [math.inf for v in range(len(graph))]
visited = [False for v in range(len(graph))]
predecessors = [v for v in range(len(graph))]
heap = Heap()
for v in range(len(graph)):
heap.array.append([v, distances[v]])
heap.pos.append(v)
distances[start] = 0
heap.decreaseKey(start, distances[start])
heap.size = len(graph)
while heap.isEmpty() == False:
min_node = heap.extractMin()
min_vertex = min_node[0]
for v, d in graph[min_vertex]:
if not visited[v]:
if (distances[min_vertex] + d) < distances[v]:
distances[v] = distances[min_vertex] + d
predecessors[v] = min_vertex
heap.decreaseKey(v, distances[v])
visited[min_vertex] = True
return [distances, predecessors]
class Heap():
def __init__(self):
self.array = []
self.size = 0
self.pos = []
def swapNode(self, u, v):
temp = self.array[v]
self.array[v] = self.array[u]
self.array[u] = temp
def minHeapify(self, index):
smallest = index
left = 2*index + 1
right = 2*index + 2
if left < self.size and self.array[left][1] < self.array[smallest][1]:
smallest = left
if right < self.size and self.array[right][1] < self.array[smallest][1]:
smallest = right
if smallest != index:
self.pos[self.array[smallest][0]] = index
self.pos[self.array[index][0]] = smallest
self.swapNode(smallest, index)
self.minHeapify(smallest)
def extractMin(self):
if self.isEmpty() == True:
return
root = self.array[0]
lastNode = self.array[self.size - 1]
self.array[0] = lastNode
self.pos[lastNode[0]] = 0
self.pos[root[0]] = self.size - 1
self.size -= 1
self.minHeapify(0)
return root
def isEmpty(self):
return True if self.size == 0 else False
def decreaseKey(self, v, dist):
i = self.pos[v]
self.array[i][1] = dist
while i > 0 and self.array[i][1] < self.array[(i - 1) // 2][1]:
self.pos[self.array[i][0]] = (i-1)//2
self.pos[self.array[(i-1)//2][0]] = i
self.swapNode(i, (i - 1)//2 )
i = (i - 1) // 2;
def isInMinHeap(self, v):
if self.pos[v] < self.size:
return True
return False
Here's the graph of the runtime against the number of vertices v:
unsortedArr
-implementation). And last but not least unless you feed both algorithms their individual worst-case input, the results won't reflect worst-case behavior. $\endgroup$def isEmpty(self): return True if self.size == 0 else False
Could be replaced bydef isEmpty(self): return (self.size==0)
$\endgroup$