Data Structures and Algorithms: Dijkstra's Algorithm

faraib

Farai Bvuma

Posted on September 20, 2024

Data Structures and Algorithms: Dijkstra's Algorithm

Introduction

Dijkstra's Algorithm is a greedy algorithm that is used to find the shortest path from a source vertex to all of the other vertices on a weighted directed graph. Being a greedy algorithm, an attempt is made to find the optimal solution by breaking the problem up into steps and solving these steps one at a time. The optimal solution in this case is the next vertex with shortest path. It is important to note that this algorithm cannot be used in a graph with negative weights.

Dijkstra's Algorithm Walkthrough

The algorithm starts by initialising the starting vertex of the graph with a distance of zero and all other vertices with distances of infinity. At this point all of the vertices are marked as unvisited. The algorithm will also initialise a priority queue to keep track of the vertices with the shortest known distances. The starting vertex is then dequeued(as it has a distance of zero) and is marked as visited. The algorithm will now check the edges of the adjacent vertices, calculating a new potential distance to each vertex by adding the weight to the distance from the current vertex. If the new distance is smaller than the currently known distance to the vertex, this distance is updated and the vertex is enqueued. The algorithm will then go on to select the next vertex from the priority queue(i.e. the vertex with the smallest distance), mark it as visited and will repeat the process until all of the vertices in the graph have been visited. We can illustrate this using the following graph:

Weighted graph

Starting at vertex A with a distance of zero, the distances from A to all of the remaining vertices will be set as infinity. A is then added to a priority queue, before being dequeued, marked as visited and checking the distances to vertices B and D. As these distances are less than infinity, their calculated distances are updated as 2 and 6 respectively. Both vertices are then added to the priority queue.

As B has the shortest distance, it will be the next vertex to be dequeued, marked as visited and processed by the algorithm. The algorithm now checks B's adjacent vertices(D and C) and checks their distances from the starting node, A -> B -> C(2 + 3 = 5) and A -> B -> D(2 + 7 = 9). The algorithm then compares these newly calculated distances to the previously stored distances. The distance to C is updated to 5 and the distance to D remains unchanged at 6. C is now also added to the priority queue.

The shortest distance is now 5 so the vertex C is dequeued and marked as visited. It only has E as an adjacent vertex, with a distance of 2 + 3 + 5 = 10(A -> B -> C -> E). The vertex E is also added to the priority queue.

D is the next vertex to be dequeued and marked as visited since it has a distance of 6(A -> D). The algorithm will now check D's adjacent vertices. Vertex C has already been visited so no further action will be taken. Finally the algorithm will finish by dequeuing E and processing it.

Implementing Dijkstra's Algorithm in JavaScript

To implement Dijkstra's Algorithm in JavaScript, we can start off by creating a PriorityQueue class.

class PriorityQueue {
  constructor() {
    this.elements = [];
  }

  enqueue(node, weight) {
    this.elements.push({ node, weight });
    this.elements.sort((a, b) => a.weight - b.weight);
  }

  dequeue() {
    return this.elements.shift(); 
  }

  isEmpty() {
    return this.elements.length === 0;
  }
}

Enter fullscreen mode Exit fullscreen mode

Along with the constructor, we will create an enqueue() method to add a vertex and its weight to the priority queue. These vertices are then sorted according to their weights, that is to say, the vertex with the smallest weight(or distance) will come first. The dequeue() method is used to return the vertex with the smallest weight from the priority queue and the isEmpty() method is used to check if the priority queue has any vertices.

Next we will create a Graph class which will have a constructor. This constructor will use an object to store an adjacency list.

  constructor() {
    this.adjacencyList = {}; 
  }
Enter fullscreen mode Exit fullscreen mode

The Graph class will also have the addNode() method which will be used to add a vertex and the addEdge() method which will be used to add edges to a vertex.

  addNode(node) {
    this.adjacencyList[node] = [];
  }

  addEdge(node1, node2, weight) {
    this.adjacencyList[node1].push({ node: node2, weight });
  }
Enter fullscreen mode Exit fullscreen mode

The addNode() method will initialize an empty array for the adjacency list for a given vertex. This array will then go on to store all of the edges for the node provided in the addEdge() method. For example, to add nodes A and B from our graph, we will use:

graph.addNode('A');
graph.addNode('B');
Enter fullscreen mode Exit fullscreen mode

The adjacency lists for the two nodes will look like this:

{
  A: [],
  B: []
}
Enter fullscreen mode Exit fullscreen mode

And then the edge can be added as follows

graph.addEdge('A', 'B', 2);
Enter fullscreen mode Exit fullscreen mode

This results in the adjacency list looking like this:

{
  A: [{ node: 'B', weight: 2 }],
}
Enter fullscreen mode Exit fullscreen mode

We will also need to create the dijkstra() method to run the algorithm:

dijkstra(start) {
    const distances = {};
    const previous = {};
    const pQueue = new PriorityQueue();

    for (const node in this.adjacencyList) {
      distances[node] = Infinity;
      previous[node] = null;
    }

    distances[start] = 0;
    pQueue.enqueue(start, 0);

    while (!pQueue.isEmpty()) {
      const { node: currentNode } = pQueue.dequeue();

      for (const neighbor of this.adjacencyList[currentNode]) {
        const { node: adjacent, weight } = neighbor;
        const newDist = distances[currentNode] + weight;

        if (newDist < distances[adjacent]) {
          distances[adjacent] = newDist;
          previous[adjacent] = currentNode;
          pQueue.enqueue(adjacent, newDist); 
        }
      }
    }

   return { distances, previous };
  }
Enter fullscreen mode Exit fullscreen mode

This method will have a distances object to map the calculated distances from the starting vertex to all of the remaining vertices in the graph, a previous object to keep track of each vertex's previous vertex and a priority queue which will be used to keep track of the vertices with the shortest distances. These will be initialised and the distances from the starting vertex will be calculated, before returning the distances and previous objects.

Lastly we will create the getShortestPath() method, which will be used to return the shortest path between two vertices.

 getShortestPath(start, end) {
    const { distances, previous } = this.dijkstra(start);
    const path = [];
    let currentNode = end;

    while (currentNode !== null) {
      path.push(currentNode);
      currentNode = previous[currentNode];
    }

    return {
      distance: distances[end],
      path: path.reverse(),
    };
  }
Enter fullscreen mode Exit fullscreen mode

Conclusion

This article explains the logic behind Dijkstra's Algorithm to find the shortest path in a graph. This article also shows how the algorithm can be implemented in JavaScript using a priority queue, although this could be done more efficiently using a min-heap. Some real world applications of Dijkstra's Algorithm include GPS and mapping software(Google Maps), route optimization in public transport systems, finding the most efficient route for sending data packets in internet routing protocols and AI pathfinding in video games.

💖 💪 🙅 🚩
faraib
Farai Bvuma

Posted on September 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related