DEV Community

Cover image for Discovering JavaScript's Hidden Secrets: Understanding Graph Algorithms.
David
David

Posted on

Discovering JavaScript's Hidden Secrets: Understanding Graph Algorithms.

Welcome to the Algorithm Series: Graph Algorithms

When we think of algorithms, sorting and searching often come to mind first because most of the time we feel they are considered the building blocks of computer science. But beyond these foundations lies a more powerful concept: graphs. Graphs aren’t just abstract data structures; they mirror the way our world is connected. Cities and roads, friends on social media, airline routes, even the pathways in our brains — all of these can be represented as graphs.

In this article, we’ll explore how JavaScript can be used to bring graph algorithms to life. From understanding nodes and edges to traversals like BFS and DFS, and finally to real-world applications such as pathfinding and network optimization, you’ll see how graphs unlock solutions to problems that ordinary arrays and objects simply can’t handle.

Understanding Graphs and Graph Algorithms

Before diving into graph algorithms, it’s important to first understand what a graph is as a data structure.

What is a Graph?

A graph is a data structure made up of:

  • Nodes (or vertices): the objects or points in the graph.
  • Edges (or connections): the links that connect one node to another.

You can imagine a graph like a map of a city:

  • The intersections are the nodes.
  • The roads are the edges that connect those intersections.

For a more detailed explanation of graph data structures, you can check out this article: Understanding Graphs as a Non-Linear Data Structure

What is a Graph Algorithm?

A graph algorithm is a method or set of steps used to solve problems involving graphs. Graph algorithms play a vital role in analyzing and processing graphs across various domains, including: Networking, Social Media, Search Engines, Recommendation Systems, Route Optimization.

Graph algorithms help us work with these connections and answer questions such as:

  • What’s the fastest route from one point to another?
  • Which connections are the most important?
  • How do we find groups or clusters inside a network?

Common categories of Graph Algorithms

  • Graph Traversal Algorithms
  • Pathfinding Algorithms
  • Minimum Spanning Tree (MST) Algorithms

Graph traversal refers to the process of systematically visiting all the nodes and edges in a graph. It is often the first step in understanding a graph’s structure and is divided into two main techniques:

  • Breadth-First Search (BFS)
  • Depth-First Search (DFS)

Breadth-First Search (BFS) Breadth-First Search is a fundamental algorithm used for traversing or searching through tree or graph data structures. It starts at a selected node (called the source or root) and explores all of its neighbor nodes at the present depth level before moving on to nodes at the next depth level.

A Visual Example Of Breadth-First Search Algorithm

        A
       / \
      B   C
     / \   \
    D   E   F
Enter fullscreen mode Exit fullscreen mode

Traversal Order: A → B → C → D → E → F

JavaScript Implementation

function breadthFirstSearch(graph, startNode) {
  const queue = [startNode];       // Queue to manage nodes
  const visited = new Set([startNode]);

  while (queue.length > 0) {
    const current = queue.shift();
    console.log(`Visiting node: ${current}`);

    for (const neighbor of graph[current]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push(neighbor);
      }
    }
  }
}

// Example graph (Adjacency List)
const graph = {
  'A': ['B', 'C'],
  'B': ['A', 'D', 'E'],
  'C': ['A', 'F'],
  'D': ['B'],
  'E': ['B', 'F'],
  'F': ['C', 'E']
};

breadthFirstSearch(graph, 'A');
// Output: A, B, C, D, E, F
Enter fullscreen mode Exit fullscreen mode

Depth-First Search (DFS): Depth-First Search is a fundamental algorithm for traversing or searching tree or graph data structures. The algorithm starts at a selected node (called the source or root) and explores as far as possible along each branch before backtracking.

A Visual Example of Depth-First Search Algorithm

        A
       / \
      B   C
     / \   \
    D   E   F
Enter fullscreen mode Exit fullscreen mode

Possible Traversal Order: A → B → D → E → C → F

JavaScript Implementation

function depthFirstSearch(graph, node, visited = new Set()) {
  visited.add(node);
  console.log(`Visiting node: ${node}`);

  for (const neighbor of graph[node]) {
    if (!visited.has(neighbor)) {
      depthFirstSearch(graph, neighbor, visited);
    }
  }
}

// Reuse the same graph
depthFirstSearch(graph, 'A');
// Possible Output: A, B, D, E, F, C
Enter fullscreen mode Exit fullscreen mode

Pathfinding Algorithms: A pathfinding algorithm is a step-by-step procedure designed to find the shortest or most optimal path between two points in a graph. Breadth-First Search (BFS) and the Depth-First Search (DFS) are basic types of Pathfinding Algorithms. Other advanced types of pathfinding algorithms include Dijkstra's Algorithm, Bellman-Ford, and Floyd-Warshall. For the purpose of this article, we will be treating Dijkstra's Algorithm.

Dijkstra's Algorithm: Dijkstra's Algorithm finds the shortest path from a single starting point to all other nodes in a graph. It works on graphs where:

Key Features of Dijkstra's Algorithm

  • Works on weighted graphs (edges have weights like distance, time, or cost).
  • Requires non-negative edge weights.
  • Guarantees the shortest path.

A Visual Example of Dijkstra's Algorithm

         4
    (A) ------> (B)
     |           |
     | 2         | 5
     |           |
    \/          \/
    (C) ------> (D)
         1
Enter fullscreen mode Exit fullscreen mode

Shortest path from A to D: A → C → D with total cost 3.

JavaScript Implementation

class MinHeap {
  constructor() {
    this.heap = [];
  }

  push(node, priority) {
    this.heap.push({ node, priority });
    this.bubbleUp(this.heap.length - 1);
  }

  pop() {
    const min = this.heap[0];
    const last = this.heap.pop();
    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.sinkDown(0);
    }
    return min;
  }

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

  updatePriority(node, newPriority) {
    const index = this.heap.findIndex(item => item.node === node);
    if (index === -1) return;
    const oldPriority = this.heap[index].priority;
    this.heap[index].priority = newPriority;
    if (newPriority < oldPriority) this.bubbleUp(index);
    else this.sinkDown(index);
  }

  bubbleUp(index) {
    const element = this.heap[index];
    while (index > 0) {
      const parentIndex = Math.floor((index - 1) / 2);
      const parent = this.heap[parentIndex];
      if (element.priority >= parent.priority) break;
      this.heap[parentIndex] = element;
      this.heap[index] = parent;
      index = parentIndex;
    }
  }

  sinkDown(index) {
    const length = this.heap.length;
    const element = this.heap[index];
    while (true) {
      let left = 2 * index + 1;
      let right = 2 * index + 2;
      let swap = null;
      if (left < length && this.heap[left].priority < element.priority) {
        swap = left;
      }
      if (
        right < length &&
        ((swap === null && this.heap[right].priority < element.priority) ||
          (swap !== null && this.heap[right].priority < this.heap[left].priority))
      ) {
        swap = right;
      }
      if (swap === null) break;
      this.heap[index] = this.heap[swap];
      this.heap[swap] = element;
      index = swap;
    }
  }
}

function dijkstra(graph, source) {
  const dist = {};
  const prev = {};
  const Q = new MinHeap();

  // Initialize distances
  for (const vertex in graph) {
    dist[vertex] = Infinity;
    prev[vertex] = null;
    Q.push(vertex, Infinity);
  }

  // Start node distance = 0
  dist[source] = 0;
  Q.updatePriority(source, 0);

  while (!Q.isEmpty()) {
    const { node: u, priority: currentDist } = Q.pop();
    if (currentDist > dist[u]) continue;

    for (const neighbor in graph[u]) {
      const weight = graph[u][neighbor];
      const alt = dist[u] + weight;
      if (alt < dist[neighbor]) {
        dist[neighbor] = alt;
        prev[neighbor] = u;
        Q.updatePriority(neighbor, alt);
      }
    }
  }

  return { distances: dist, previous: prev };
}

function getShortestPath(prev, target) {
  const path = [];
  let current = target;
  while (current !== null) {
    path.unshift(current);
    current = prev[current];
  }
  return path;
}

// Example usage
const graph = {
  'A': { 'B': 4, 'C': 2 },
  'B': { 'D': 5 },
  'C': { 'D': 1, 'B': 1 },
  'D': {}
};

const result = dijkstra(graph, 'A');
console.log('Distances:', result.distances);
console.log('Previous nodes:', result.previous);

const pathToD = getShortestPath(result.previous, 'D');
console.log('Shortest path to D:', pathToD); // [ 'A', 'C', 'D' ]
Enter fullscreen mode Exit fullscreen mode

Minimum Spanning Tree Algorithm

A Minimum Spanning Tree (MST) is the subset of connections in a weighted network that achieves the absolute lowest total cost for connecting all points, while rigorously avoiding any redundant loops. It guarantees that every single point is included and reachable, yet it forms a efficient "tree" structure where there is only one unique path between any two points, ensuring no resources are wasted on unnecessary connections. A common example of a Minimum Spanning Tree algorithm is Kruskal's Algorithm.

Kruskal's algorithm: Kruskal's Algorithm is a greedy, efficient method for finding an MST by iteratively adding the smallest available edge and using the Union-Find data structure to intelligently avoid cycles, ensuring the result is both spanning and of minimum cost.

A Visual Example of Kruskal's Algorithm

        (A)
     5/  |  \5
    (B) 3|   (C)
     2\  |  /4
        (D)
Enter fullscreen mode Exit fullscreen mode
Steps
  1. Sort edges by weight: BD(2), AD(3), CD(4), AB(5), AC(5).
  2. Pick edges in order, skipping ones that form cycles.
  3. Final MST:
    • B–D (2)
    • A–D (3)
    • C–D (4)

Total Cost = 9

JavaScript Implementation

class UnionFind {
  constructor(size) {
    this.parent = Array.from({ length: size }, (_, i) => i);
    this.rank = new Array(size).fill(0);
  }

  find(x) {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]); // Path compression
    }
    return this.parent[x];
  }

  union(x, y) {
    const rootX = this.find(x);
    const rootY = this.find(y);
    if (rootX === rootY) return false;

    // Union by rank
    if (this.rank[rootX] < this.rank[rootY]) {
      this.parent[rootX] = rootY;
    } else if (this.rank[rootX] > this.rank[rootY]) {
      this.parent[rootY] = rootX;
    } else {
      this.parent[rootY] = rootX;
      this.rank[rootX]++;
    }
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Graphs give us a way to map complexity into something we can analyze, optimize, and solve with code. By learning how to implement graph algorithms in JavaScript, we open the door to practical applications like smarter navigation systems and efficient social media recommendations. What begins as a study of nodes and edges soon reveals itself as a tool for tackling some of the biggest challenges in technology and daily life.

As you continue experimenting with graph algorithms, remember: the value isn’t only in writing the code, but in learning to see the world as a network of connections so that you can use that perspective to design smarter solutions.

Top comments (0)