Mastering Graphs in Java: A Comprehensive Guide for Developers

Table of Contents

Graphs are fundamental data structures in computer science and are widely used to model complex relationships between entities. They are versatile and can represent a variety of real-world scenarios such as social networks, transportation networks, and computer networks. In this blog post, we’ll delve into some essential graph algorithms implemented in Java, covering Depth-First Search (DFS), Breadth-First Search (BFS), Dijkstra’s Shortest Path Algorithm, detecting cycles in a directed graph, and performing a topological sort.

Understanding Graphs

Before diving into the algorithms, let’s briefly review what graphs are. We know that data arranged in a linear manner is called linear data structures, such as Arrays, LinkedLists, Stacks, and Queues. Whereas, when data is stored non-linearly, it is referred to as non-linear data structures, such as Trees and Graphs. A graph consists of a set of vertices (nodes) and a set of edges (connections) that establish relationships between these vertices. Edges can be directed or undirected, depending on whether they have a specific direction or not. Graphs can also be weighted, where each edge has an associated weight or cost.

A graph is a powerful non-linear data structure consisting of two components:

  • Vertices (nodes): Represent individual entities or data points. Think of them as points on a map.
  • Edges: Represent connections between vertices, indicating relationships or interactions. Edges can be bidirectional (undirected) or unidirectional (directed), like arrows showing one-way relationships.

A graph G is an ordered pair consisting of a set V of vertices and a set E of edges.

G = (V,E)

In an ordered pair, (a,b) is not equivalent to (b,a) if a ≠ b.

In an unordered pair, {a,b} equals {b,a}.

Edges:

  • Directed: (u,v) i.e., u → v
  • Undirected: {u,v} i.e., u — v

Example:

V = {v1, v2, v3, v4, v5, v6, v7, v8} // Set of vertices

E = { {v1, v2}, {v1, v3}, {v2, v4}, {v3, v5}, {v4, v6}, {v5, v7}, {v6, v8}, {v7, v8}, {v2, v5}, {v3, v6} }

Graph Examples

Consider a social network like Facebook. Users are represented as vertices, and friendships are represented as edges. If A and B are friends, we can have either a directed edge A -> B (meaning A follows B) or an undirected edge {A, B} (meaning they follow each other).

Common Graphs Examples

  • Maps: Represent cities as vertices and roads as edges (weighted to denote distance).
  • World Wide Web: Represent web pages as vertices and hyperlinks as directed edges pointing from source pages to destination pages.
  • File systems: Represent directories as vertices and subdirectory/file connections as edges.

Graph Representations:

  • Adjacency Matrix: Square matrix, where (i,j) reflects the edge weight between vertices i and j. Suitable for dense graphs with many edges.
  • Adjacency List: Array or list of lists, where each index stores connections for a specific vertex. More efficient for sparse graphs with fewer edges.

Implementing Graphs in Java

To start, we’ll implement a simple graph representation in Java. We’ll use an adjacency list to store the graph, which is a list of lists where each list represents the neighbors of a particular vertex.

Java
import java.util.*;

class Graph {
    private int V; // Number of vertices
    private LinkedList<Integer> adjList[]; // Adjacency list representation

    // Constructor
    Graph(int v) {
        V = v;
        adjList = new LinkedList[v];
        for (int i = 0; i < v; ++i)
            adjList[i] = new LinkedList<>();
    }

    // Function to add an edge to the graph
    void addEdge(int v, int w) {
        adjList[v].add(w);
    }

    // Getter for the adjacency list of a vertex
    LinkedList<Integer> getAdjList(int v) {
        return adjList[v];
    }

    public static void main(String[] args) {
        Graph graph = new Graph(4);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 3);

        for (int i = 0; i < graph.V; i++) {
            LinkedList<Integer> list = graph.getAdjList(i);
            System.out.print("Adjacency list of vertex " + i + ": ");
            for (Integer node : list) {
                System.out.print(node + " ");
            }
            System.out.println();
        }
    }
}

This Graph class represents an undirected graph using an adjacency list representation. It has a constructor to initialize the graph with a given number of vertices. The addEdge method adds an edge between two vertices. The getAdjList method returns the adjacency list of a specified vertex.

The main method demonstrates how to use the Graph class by creating a graph with 4 vertices and adding some edges. Then, it prints the adjacency list of each vertex.

Common Types of Graphs

Here are some common types of graphs and their java implementations:

Directed Graphs (Digraphs)

Directed graphs, also known as digraphs, are graphs in which edges have a direction associated with them. This means that the relationship between nodes is one-way. They are used to model relationships like dependencies, flows, or hierarchical structures.

Java
   A
  ↗ ↘
 B   C
  ↘ ↗
   D
  • Vertices are represented by letters: A, B, C, D.
  • Arrows indicate the direction of the edges.
  • Arrows point from the source vertex to the destination vertex, representing a directed edge from the source to the destination.

Implementation: In Java, you can implement a directed graph using adjacency lists or adjacency matrices. Here’s a basic example of implementing a directed graph using adjacency lists:

Java
import java.util.*;

class Digraph {
    private int V;
    private List<Integer>[] adj;

    public Digraph(int V) {
        this.V = V;
        adj = new ArrayList[V];
        for (int i = 0; i < V; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    public void addEdge(int v, int w) {
        adj[v].add(w);
    }

    public Iterable<Integer> adj(int v) {
        return adj[v];
    }
}

Undirected Graphs

Undirected graphs are graphs in which edges do not have any direction associated with them. The relationship between nodes is bidirectional. They are used to model symmetric relationships, such as friendships in social networks or connections in a network.

Java
   A
  / \
 /   \
B --- C
 \   /
  \ /
   D
  • Vertices are represented by letters: A, B, C, D.
  • Edges are represented by lines connecting the vertices.
  • Each line connects two vertices, representing an undirected edge between them.

Implementation: Similar to directed graphs, undirected graphs can be implemented using adjacency lists or adjacency matrices. Here’s a basic example of implementing an undirected graph using adjacency lists:

Java
import java.util.*;

class Graph {
    private int V;
    private List<Integer>[] adj;

    public Graph(int V) {
        this.V = V;
        adj = new ArrayList[V];
        for (int i = 0; i < V; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    public void addEdge(int v, int w) {
        adj[v].add(w);
        adj[w].add(v);
    }

    public Iterable<Integer> adj(int v) {
        return adj[v];
    }
}

Weighted Graphs

Weighted graphs are graphs in which each edge has an associated weight or cost. They are used to model scenarios where the relationship between nodes has a numerical value associated with it, such as distances between cities or costs of transportation.

Java
   
   A
2/   \4
/     \
B--5-- C
\1   3/
 \   /
   D
  • Vertices are represented by letters: A, B, C, D.
  • Edges are represented by lines connecting the vertices.
  • Each line connecting two vertices represents an edge.
  • Beside each edge, there is a number indicating the weight of that edge.
  • For example, the edge between vertices A and B has a weight of 2, the edge between vertices B and D has a weight of 1, etc.

Implementation: Weighted graphs can be implemented similarly to undirected or directed graphs, with the addition of storing weights along with edges. Here’s an example of implementing a weighted graph using adjacency lists:

Java
import java.util.*;

class WeightedGraph {
    private int V;
    private List<Edge>[] adj;

    class Edge {
        int v, w;

        Edge(int v, int w) {
            this.v = v;
            this.w = w;
        }
    }

    public WeightedGraph(int V) {
        this.V = V;
        adj = new ArrayList[V];
        for (int i = 0; i < V; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    public void addEdge(int v, int w, int weight) {
        adj[v].add(new Edge(w, weight));
        adj[w].add(new Edge(v, weight)); // For undirected graphs
    }

    public Iterable<Edge> adj(int v) {
        return adj[v];
    }
}

Graph Algorithms

Now that we have our graph implementation ready, let’s explore some fundamental graph algorithms.

Depth-First Search (DFS) Traversal Algorithm

DFS is a graph traversal algorithm that explores as far as possible along each branch before backtracking. It uses a stack (or recursion) to keep track of vertices to visit next.

Java
class DFS {
    // Depth-First Search traversal
    void dfsUtil(int v, boolean visited[], Graph graph) {
        visited[v] = true;
        System.out.print(v + " ");

        LinkedList<Integer> neighbors = graph.getAdjList(v);
        for (Integer neighbor : neighbors) {
            if (!visited[neighbor])
                dfsUtil(neighbor, visited, graph);
        }
    }

    void dfs(int v, Graph graph) {
        boolean visited[] = new boolean[graph.V];
        dfsUtil(v, visited, graph);
    }

    public static void main(String[] args) {
        Graph graph = new Graph(4);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 3);

        DFS dfs = new DFS();
        System.out.print("Depth-First Search traversal starting from vertex 2: ");
        dfs.dfs(2, graph);
    }
}

This DFS class contains methods for performing a Depth-First Search (DFS) traversal on a graph. The dfsUtil method is a recursive utility function that performs DFS traversal starting from a specified vertex (v). It marks the vertex as visited, prints its value, and then recursively visits its unvisited neighbors. The dfs method initializes an array to track visited vertices and calls dfsUtil to start the DFS traversal from a specified vertex.

The main method demonstrates how to use the DFS class by creating a graph with 4 vertices and adding some edges. Then, it performs a DFS traversal starting from vertex 2 and prints the traversal result.

Breadth-First Search (BFS) Traversal Algorithm

BFS is another traversal algorithm that explores all the neighboring nodes at the present depth prior to moving on to the nodes at the next depth level.

Java
import java.util.LinkedList;

class BFS {
    // Breadth-First Search traversal
    void bfs(int v, Graph graph) {
        boolean visited[] = new boolean[graph.V];
        LinkedList<Integer> queue = new LinkedList<>();

        visited[v] = true;
        queue.add(v);

        while (!queue.isEmpty()) {
            v = queue.poll();
            System.out.print(v + " ");

            LinkedList<Integer> neighbors = graph.getAdjList(v);
            for (Integer neighbor : neighbors) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.add(neighbor);
                }
            }
        }
    }

    public static void main(String[] args) {
        Graph graph = new Graph(4);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 3);

        BFS bfs = new BFS();
        System.out.print("Breadth-First Search traversal starting from vertex 2: ");
        bfs.bfs(2, graph);
    }
}

This BFS class contains a method for performing a Breadth-First Search (BFS) traversal on a graph. The bfs method initializes a boolean array to track visited vertices and a queue to store vertices for traversal. It starts the traversal from a specified vertex (v), marks it as visited, and adds it to the queue. Then, it repeatedly dequeues vertices from the queue, prints them, and adds their unvisited neighbors to the queue.

The main method demonstrates how to use the BFS class by creating a graph with 4 vertices and adding some edges. Then, it performs a BFS traversal starting from vertex 2 and prints the traversal result.

Dijkstra’s Shortest Path Algorithm

Dijkstra’s algorithm finds the shortest path from a source vertex to all other vertices in a weighted graph. It maintains a priority queue to continuously select the vertex with the smallest distance from the source.

Java
import java.util.*;

class Dijkstra {
    // Dijkstra's Shortest Path Algorithm
    void dijkstra(int src, Graph graph) {
        int V = graph.V;
        int[] dist = new int[V];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[src] = 0;

        PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(i -> dist[i]));
        pq.add(src);

        while (!pq.isEmpty()) {
            int u = pq.poll();
            LinkedList<Integer> neighbors = graph.getAdjList(u);
            for (Integer v : neighbors) {
                int weight = 1; // Assuming unweighted graph, modify for weighted graph
                if (dist[u] + weight < dist[v]) {
                    dist[v] = dist[u] + weight;
                    pq.add(v);
                }
            }
        }

        // Print shortest distances
        System.out.println("Shortest distances from source vertex " + src + ":");
        for (int i = 0; i < V; i++) {
            System.out.println("Vertex " + i + ": " + dist[i]);
        }
    }

    public static void main(String[] args) {
        Graph graph = new Graph(6);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(1, 3);
        graph.addEdge(2, 4);
        graph.addEdge(3, 4);
        graph.addEdge(3, 5);
        graph.addEdge(4, 5);

        Dijkstra dijkstra = new Dijkstra();
        int sourceVertex = 0;
        dijkstra.dijkstra(sourceVertex, graph);
    }
}

This Dijkstra class contains a method for finding the shortest path distances from a source vertex to all other vertices in a graph using Dijkstra’s algorithm. The dijkstra method initializes an array to store shortest distances (dist), initializes all distances to infinity except for the source vertex distance which is set to 0. It then uses a priority queue (pq) to greedily select the vertex with the shortest distance and updates the distances to its neighbors if a shorter path is found. Finally, it prints the shortest distances from the source vertex to all other vertices.

The main method demonstrates how to use the Dijkstra class by creating a graph with 6 vertices and adding some edges. Then, it performs Dijkstra’s algorithm starting from a specified source vertex and prints the shortest distances to all other vertices.

Detecting Cycles in a Directed Graph

Detecting cycles in a directed graph is crucial in various applications such as deadlock detection and task scheduling. We’ll use a modified DFS approach to detect cycles.

Java
class CycleDetection {
    boolean isCyclicUtil(int v, boolean visited[], boolean recStack[], Graph graph) {
        if (recStack[v])
            return true;

        if (visited[v])
            return false;

        visited[v] = true;
        recStack[v] = true;

        LinkedList<Integer> neighbors = graph.getAdjList(v);
        for (Integer neighbor : neighbors) {
            if (isCyclicUtil(neighbor, visited, recStack, graph))
                return true;
        }

        recStack[v] = false;
        return false;
    }

    boolean isCyclic(Graph graph) {
        boolean visited[] = new boolean[graph.V];
        boolean recStack[] = new boolean[graph.V];

        for (int i = 0; i < graph.V; i++) {
            if (isCyclicUtil(i, visited, recStack, graph))
                return true;
        }

        return false;
    }

    public static void main(String[] args) {
        Graph graph = new Graph(4);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 3);

        CycleDetection cycleDetection = new CycleDetection();
        if (cycleDetection.isCyclic(graph))
            System.out.println("The graph contains a cycle");
        else
            System.out.println("The graph doesn't contain a cycle");
    }
}

This CycleDetection class contains methods to detect cycles in a graph using depth-first search (DFS). The isCyclicUtil method recursively checks if there is a cycle starting from a given vertex (v) by maintaining a boolean array (recStack) to keep track of the vertices in the current recursion stack. If a vertex is visited and also present in the recursion stack, it indicates the presence of a cycle. The isCyclic method iterates through all vertices in the graph and calls isCyclicUtil for each vertex to check for cycles.

The main method demonstrates how to use the CycleDetection class by creating a graph with 4 vertices and adding some edges. Then, it checks if the graph contains a cycle and prints the result.

Topological Sorting of a Graph

Topological sorting is a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge u → v, vertex u comes before vertex v in the ordering.

Java
import java.util.*;

class TopologicalSort {
    Stack<Integer> stack = new Stack<>();

    void topologicalSortUtil(int v, boolean visited[], Graph graph) {
        visited[v] = true;

        LinkedList<Integer> neighbors = graph.getAdjList(v);
        for (Integer neighbor : neighbors) {
            if (!visited[neighbor])
                topologicalSortUtil(neighbor, visited, graph);
        }

        stack.push(v);
    }

    void topologicalSort(Graph graph) {
        int V = graph.V;
        boolean visited[] = new boolean[V];

        for (int i = 0; i < V; i++) {
            if (!visited[i])
                topologicalSortUtil(i, visited, graph);
        }

        // Print topological order
        System.out.println("Topological order:");
        while (!stack.isEmpty())
            System.out.print(stack.pop() + " ");
    }

    public static void main(String[] args) {
        Graph graph = new Graph(6);
        graph.addEdge(5, 2);
        graph.addEdge(5, 0);
        graph.addEdge(4, 0);
        graph.addEdge(4, 1);
        graph.addEdge(2, 3);
        graph.addEdge(3, 1);

        TopologicalSort topologicalSort = new TopologicalSort();
        topologicalSort.topologicalSort(graph);
    }
}

This TopologicalSort class contains methods to perform topological sorting on a directed acyclic graph (DAG) using depth-first search (DFS). The topologicalSortUtil method recursively traverses the graph, marking visited vertices and pushing them onto a stack after visiting all of their neighbors. The topologicalSort method initiates DFS traversal for each vertex and then prints the topological order by popping vertices from the stack.

The main method demonstrates how to use the TopologicalSort class by creating a graph with 6 vertices and adding directed edges between them. Then, it performs topological sorting and prints the topological order.

Conclusion

In this blog post, we explored several essential graph algorithms implemented in Java, including DFS, BFS, Dijkstra’s Shortest Path Algorithm, cycle detection in directed graphs, and topological sorting. Understanding these algorithms is crucial for tackling a wide range of graph-related problems efficiently. Feel free to experiment with these implementations and explore further applications in your projects. Happy coding!

Author

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!