def heap_sort(unsorted_arr): # Converting unsorted array to a Max Heap heap_arr = heap.Heap(unsorted_arr, len(unsorted_arr) - 1) heap_arr.build_max_heap() # Needed to store elements in correct order sorted_arr = [None] * heap_arr.n sorted_arr_index = heap_arr.n - 1 while not heap_arr.is_empty(): # Storing highest element in new array, ascending order maintained max_element = heap_arr.heap[1] sorted_arr[sorted_arr_index] = max_element sorted_arr_index -= 1 # Removing root from heap by exchanging with last leaf heap_arr.heap[1], heap_arr.heap[heap_arr.n] = heap_arr.heap[ heap_arr.n], heap_arr.heap[1] heap_arr.n -= 1 # Above exchange may violate Max Heap property at root, correcting heap_arr.max_heapify(1) return sorted_arr
def dijkstra(self, start): ''' Dijkstra's algorithm builds off of Prim's algorithm. The objective of Dijkstra's algorithm is to find the shortest path between a starting node and a destination node. However, at the end of running Dijkstra's algorithm, we find that we have the shortest path to every other single vertex in our graph. When I am throwing around the word shortest path, I am referencing the smallest total edge weight path. Dijkstra's algorithm only changes one thing from Prim's algorithm: instead of updating the weight to be edge weight, we total up the total distance or edge weight we have seen so far and add it to the vertex's weight. Dijkstra's algorithm is very useful because it handles both directed and undirected graphs. In addition, Dijkstra's algorithm accounts will make the correct call when deciding between many small weighted edges and one large weighted edge. One downside of Dijkstra's algorithm is that it cannot handle neight weight cycles. Finally, if we are trying to find the path from start to finish, we start with the destination node and work backwards using each node's predecessor. INPUT: start: Starting node we are finding OUTPUT: Shortest path to every single vertex in our graph from the starting point Runtime - O(n + mlg(n)) - We know that 'initializing' each of the vertices takes O(n) time. Next, building a minHeap takes O(n) time. Once we enter the for loop and traverse all of the vertices, we have an outer runtime of O(n). On the inside of the for loop, we remove from the minheap which takes O(lg(n)) time since we might have to heapify down. So that small portion gives us a runtime of nlg(n). The other portion inside of the for loop runs in time proportional to mlg(n) since we visit m edges, and building the heap again takes lg(n) time. Once we add these up, we get a total runtime of O(mlg(n) + nlg(n)). Other implementations will get you better runtimes depending on what you want from the graph and the type of graph you are expecting. Using a adjacency list and minheap, we get this runtime. Using a adjacency matrix and a heap we also get the same runtime. However, if we swapped the heap for an unsorted array, we get a runtime of O(n^2) for both implementations of a graph. The best way to truely improve the runtime is to use a fibonacci heap, this would reduce total runtime to O(nlg(n) + m). ''' for v in self.vertices.keys(): v.weight = math.inf v.predecessor = None v.weight = 0 priorityQueue = heap.Heap() # heap to store our vertices priorityQueue.buildHeap(self.vertices.keys()) sssp = Graph() # Single source shortest path (Dijkstras) for __ in range(len(self.vertices.keys())): vert = priorityQueue.remove() sssp.__createdVertexInsertion(vert) if vert.predecessor: e = self.areAdjacent(vert.predecessor, vert) key = e.key weight = e.weight sssp.insertEdge(vert.predecessor, vert, key, weight) # Can add additional code to account for directed graphs. for adjvert in self.adjacentVertices(vert): if adjvert not in sssp.vertices.keys(): existing_weight = self.areAdjacent(adjvert, vert).weight + vert.weight if existing_weight < adjvert.weight: adjvert.weight = existing_weight adjvert.predecessor = vert priorityQueue.buildHeap() return sssp
def mstPrim(self, v): ''' Before we talk about the algorithm, lets touch base on what a MST exactly is once again. A minimum spanning tree (MST) is a graph that is minimally connected. This means that we have created a path between any two nodes in our graph, have no cycles, and have a minimum total weight. In order to build a MST using Prim's algorithm, we first give each vertex a weight and a predecessor. Once we do this, we set the predecessor and weight in each vertex to be none and +inf respectively. In Prim's algorithm, we need to have a starting vertex. We set the weight of the starting vertex to 0. We then build a minheap to hold all of our vertices. We also create a new graph object that will end up being our MST. Finally, here we start our algorithm. for each vertex in vertices: min = queue.remove() if predecessor: Mst.addedge(vertex,predecessor) for neighbor of vertex not in new graph: if current_weight > weight_of_edge_connecting_vertex_neighbor: neighbor.weight = weight_of_edge_connecting_vertex_neighbor neighbor.predecessor = vertex Runtime - O(n + mlg(n)) - We know that 'initializing' each of the vertices takes O(n) time. Next, building a minHeap takes O(n) time. Once we enter the for loop and traverse all of the vertices, we have an outer runtime of O(n). On the inside of the for loop, we remove from the minheap which takes O(lg(n)) time since we might have to heapify down. So that small portion gives us a runtime of nlg(n). The other portion inside of the for loop runs in time proportional to mlg(n) since we visit m edges, and building the heap again takes lg(n) time. Once we add these up, we get a total runtime of O(mlg(n) + nlg(n)). Other implementations will get you better runtimes depending on what you want from the graph and the type of graph you are expecting. Using a adjacency list and minheap, we get this runtime. Using a adjacency matrix and a heap we also get the same runtime. However, if we swapped the heap for an unsorted array, we get a runtime of O(n^2) for both implementations of a graph. The best way to truely improve the runtime is to use a fibonacci heap, this would reduce total runtime to O(nlg(n) + m). ''' for v in self.vertices.keys(): v.predecessor = None v.weight = math.inf v.weight = 0 priorityQueue = heap.Heap() priorityQueue.buildHeap(self.vertices.keys()) minimumSpanningTree = Graph() for __ in range(len(self.vertices)): e = priorityQueue.remove() minimumSpanningTree.__createdVertexInsertion(e) if e.predecessor: key = self.areAdjacent(e, e.predecessor).key minimumSpanningTree.insertEdge(e, e.predecessor, key, e.weight) for neighbor in self.adjacentVertices(e): if not neighbor in minimumSpanningTree.vertices.keys(): weight = self.areAdjacent(neighbor, e).weight if weight < neighbor.weight: neighbor.weight = weight neighbor.predecessor = e priorityQueue.buildHeap() return minimumSpanningTree
def mstKruskal(self): ''' Before we talk about the algorithm, lets touch base on what a MST exactly is once again. A minimum spanning tree (MST) is a graph that is minimally connected. This means that we have created a path between any two nodes in our graph, have no cycles, and have a minimum total weight. In order to build a MST using Kruskal's algorithm, we first put each vertex in a seperate set in a disjoint set. The purpose we do this is to check whether two vertices have the same representative element. If they do, that means they are part of some minimum spanning tree for that labeled set. Next, we place all of the edges in a heap and build a minheap based on the edge weights. This will allow for us to properly remove each edge and to have the edges in correct order in linear time (build heap runs in O(n) time). Finally, we create a new graph which will be the minimum spanning tree. Once we do all of this we follow this algorithm: # while edges < n - 1: # for each edge: # if vertices in edge are in seperate sets, union and make the edge a discovery edge # else make the edge not discovery but visited Runtime - O(n + mlg(n)) - Since it takes O(n) time to build the disjoint set, O(m) time to build a minheap(partially sorted), O(m) time to loop through the while loop since we might have to visit every edge, and finally O(lg(m)) = o(lg(n)) time to remove from a heap, we get a total runtime of O(n + mlg(n)). The runtime of implementing this algorithm using a sorted array is the same runtime. However, a minheap allows you to update edge weights with less of a cost and works better with a dense graph. ''' minimumSpanningTree = Graph() forest = disjointset.DisjointSet(self.vertices.keys()) edgeWeights = heap.Heap() # priority queue for our impl. of sorting edges. edgeWeights.buildHeap(self.edges.toList()) while len(minimumSpanningTree.edges) < (len(self.vertices.keys()) - 1): edge = edgeWeights.remove() index1 = index2 = 0 # Room for optimization for index, value in enumerate(forest.array): if value.data == edge.origin: index1 = index elif value.data == edge.destination: index2 = index if forest.find(index1) != forest.find(index2): forest.union(index1, index2) if not edge.origin in minimumSpanningTree.vertices.keys(): minimumSpanningTree.__createdVertexInsertion(edge.origin) if not edge.destination in minimumSpanningTree.vertices.keys(): minimumSpanningTree.__createdVertexInsertion(edge.destination) minimumSpanningTree.insertEdge(edge.origin, edge.destination, edge.key, edge.weight) return minimumSpanningTree
def sumWeights(self): ''' This is a helper function that sums the weights of all the edges in our graph. INPUT: none OUTPUT: Sum of all the weights of all the edges Runtime - O(m) - Since we have to sum up all of the edges, our algorithm runs in time proportional to O(m). ''' minHeap = heap.Heap() minHeap.buildHeap(self.edges.toList()) minSum = 0 while not minHeap.isEmpty(): minSum += minHeap.remove().weight return minSum