DEV Community

Cover image for Graphs In Java (Part 2)
Knitely
Knitely

Posted on

Graphs In Java (Part 2)

Introduction

Welcome back, graph enthusiasts, for the highly anticipated sequel to our last discussion. Get ready for some significant renovations to the code we wrote last time.

To guide you through this architectural marvel, we've introduced a special instruction: if a code snippet is wrapped with this ---- sign, it means that piece of code is being evicted! Simply remove it and then immediately install the brand-new, shiny code directly below it. That being said, follow my Telegram Channel and let's go to business.

Topological ordering

A topological ordering, or topological sort, is like trying to put your socks on before your shoes (unless you're a true rebel, but then you wouldn't need a topological sort). It's a linear ordering of the vertices in a directed acyclic graph (DAG), which basically means it's a list of things where there's no going in circles (because that would just be chaotic, like trying to tie your shoelaces before you've even picked out the shoes). For every directed edge (u, v), vertex u absolutely, positively must come before vertex v. In simpler terms, it's arranging your tasks so that all dependencies are respected. So, if task A must be completed before task B, then A will appear before B in the sorted list. Otherwise, you'd end up trying to eat dessert before dinner, and we all know how well that goes over with Mom. Look at the figure below for more understanding.

Topological-Sort

Alright, our definition is done and dusted! Now, let's make our lovely graph's topological ordering happen. Grab that GraphAlgorithms.java file from our last adventure, and go ahead and write the following code right in there.

public static <V> ArrayList<Vertex<V>> topologicalSort(Graph<V> graph) {
      ArrayList<Vertex<V>> topological = new ArrayList<>();
      Stack<Vertex<V>> stack = new Stack<>();

      HashMap<Vertex<V>, Integer> inCount = new HashMap<>();
      for (Vertex<V> v : graph.vertices()) {
          inCount.put(v, graph.inDegree(v));
          if (inCount.get(v) == 0) {
              stack.push(v);
          }
      }

      while (!stack.isEmpty()) {
          Vertex<V> current = stack.pop();
          topological.add(current);
          for (Vertex<V> vertex : graph.outgoingVertices(current)) {
              inCount.put(vertex, inCount.get(vertex) - 1);
              if (inCount.get(vertex) == 0) {
                  stack.push(vertex);
              }
          }
      }

      return topological;
  }
Enter fullscreen mode Exit fullscreen mode

So, picture this: our HashMap is basically the graph's gossipy social network, listing every vertex and precisely how many 'friends' (incoming edges) they need to be introduced by. At first, our stack is super exclusive, only letting in the popular kids who have no dependencies—they just waltz right in! But here's the kicker: the more incoming edges a vertex has, the more demanding it is. It's like, 'Sorry, can't be processed until Tasks A, B, and C have cleared the path for me!' Talk about high-maintenance!

To see if the code actually, go on over to your Main.java file. Then, insert the following code snippet inside your main() method. This is where we'll create a graph that resembles the one in the figure below.

Topological-Sort-example

AdjacencyMapGraph<Integer> graph = new AdjacencyMapGraph<>(true);
Vertex<Integer> one = graph.insertVertex(1);
Vertex<Integer> two = graph.insertVertex(2);
Vertex<Integer> three = graph.insertVertex(3);
Vertex<Integer> four = graph.insertVertex(4);
Vertex<Integer> five = graph.insertVertex(5);

graph.insertEdge(one, two);
graph.insertEdge(one, three );
graph.insertEdge(one, five);
graph.insertEdge(two, three);
graph.insertEdge(three, four);
graph.insertEdge(three, five);
graph.insertEdge(four, five);

var topological = GraphAlgorithms.topologicalSort(graph);
for (Vertex<Integer> vertex : topological) {
    System.out.print(vertex.getElement() + ", "); // [1, 2, 3, 4, 5]
}
Enter fullscreen mode Exit fullscreen mode

While the in-degree method (Kahn's algorithm) is fantastic, the Depth First Search (DFS) approach is another elegant and equally powerful way to achieve a topological ordering.

The core idea with DFS is indeed to push vertices onto a stack after all their dependencies (their outgoing edges) have been fully explored. When you're done, flipping that stack gives you the perfect, reverse-engineered chronological order. I'll leave the thrilling implementation of this particular sorcery to you.

Weighted Graphs

A weighted graph is just a regular graph that started dealing with real-world expenses. Each edge is assigned a numerical (and let me reiterate, numerical) value, known as a 'weight.' This weight could be a cost (like how much that cross-town taxi really hits your wallet), a distance (because some detours are just plain rude), or any other metric that says, 'Hey, this connection actually matters more (or less) than that one. We need to know if that connection is a quick hop or an epic quest.
Okay, so our edges now come with a bonus feature: weights. To incorporate this crucial information into our graph, we'll need to update our existing code. That means it's time to modify some elements from our last post to accurately represent these weighted connections. Go ahead and create an Edge.java file, then add the provided code.

import java.util.ArrayList;

public class Edge<V> {

    private Vertex<V> origin, destination;
    private int weight;

    public Edge(Vertex<V> origin, Vertex<V> destination, int weight) {
        this.origin = origin;
        this.destination = destination;
        this.weight = weight;
    }

    public int getWeight() {
        return weight;
    }

    public ArrayList<Vertex<V>> endpoints() {
        ArrayList<Vertex<V>> endpoints = new ArrayList<>(2);
        endpoints.add(origin);
        endpoints.add(destination);
        return endpoints;
    }

    public Vertex<V> opposite(Vertex<V> v) {
        if (v == origin)
            return destination;

        return origin;
    }

    public Vertex<V> getOrigin() {
        return origin;
    }

    public Vertex<V> getDestination() {
        return destination;
    }
}

Enter fullscreen mode Exit fullscreen mode

Next go to Graph.java file and the following code in our interface.

    int outDegree(Vertex<V> v) throws IllegalArgumentException;
    int inDegree(Vertex<V> v) throws IllegalArgumentException;

    Iterable<Edge<V>> outgoingEdges(Vertex<V> v) throws IllegalArgumentException;
    Iterable<Edge<V>> incomingEdges(Vertex<V> v) throws IllegalArgumentException;

    Edge<V> getEdge(Vertex<V> u, Vertex<V> v) throws IllegalArgumentException;
}
Enter fullscreen mode Exit fullscreen mode

With the new methods added to our interface, the next logical step—for obvious reasons—is to provide their implementation within our AdjacencyMapGraph class. This is where we'll define the functionality for those methods. Please insert the code below into your AdjacencyMapGraph class.


        // ------ snip ----
        @Override
        public Iterable<Vertex<V>> outgoingVertices(Vertex<V> v) throws IllegalArgumentException {
            InnerVertex<V> outgoing = validate(v);
            ---- return outgoing.getOutgoing(); ----
            return outgoing.getOutgoing().keySet();
        }

        @Override
        public Iterable<Vertex<V>> incomingVertices(Vertex<V> v) throws IllegalArgumentException {
            InnerVertex<V> incoming = validate(v);
            ---- return incoming.getIncoming(); ----
            return incoming.getIncoming().keySet();
        }

    @Override
    public Iterable<Edge<V>> outgoingEdges(Vertex<V> v) throws IllegalArgumentException {
        InnerVertex<V> incoming = validate(v);
        return incoming.getOutgoing().values();
    }

    @Override
    public Iterable<Edge<V>> incomingEdges(Vertex<V> v) throws IllegalArgumentException {
        InnerVertex<V> outgoing = validate(v);
        return outgoing.getIncoming().values();
    }

    @Override
    public Edge<V> getEdge(Vertex<V> u, Vertex<V> v) throws IllegalArgumentException {
        if (!isValidVertex(u) || !isValidVertex(v)) {
            throw new IllegalArgumentException("Invalid vertex");
        }

        if (hasEdge(u, v)) {
            InnerVertex<V> origin = validate(u);
            InnerVertex<V> destination = validate(v);
            return origin.getOutgoing().get(destination);
        }
        return null;
    }

}
Enter fullscreen mode Exit fullscreen mode

Alright, it's time to get our InnerVertex class ready for the big leagues—where edges aren't just connections, they have weight. To make sure our vertices can properly keep track of these weighted relationships, we need to swap out that old HashSet for a more robust HashMap. This way, we can associate each connected vertex (our key) with its specific edge (our value).

So, head into your InnerVertex class and add the following code:

protected static class InnerVertex<V> implements Vertex<V> {
        // ---- snip ----
        ---- private HashSet<Vertex<V>> outgoing, incoming; ----
        private HashMap<Vertex<V>, Edge<V>> outgoing, incoming;

        public InnerVertex(V element, boolean isDirected) {
            this.element = element;
            ---- outgoing = new HashSet<>(); ----
            outgoing = new HashMap<>();
            if (isDirected) {
                    ---- incoming = new HashSet<>(); ----
                incoming = new HashMap<>();
            } else {
                incoming = outgoing;
            }
        }

        // ---- snip ----
        ---- 
        public HashSet<Vertex<V>> getOutgoing() throws IllegalArgumentException {
            return outgoing;
            } 
            ----
        public HashMap<Vertex<V>, Edge<V>> getOutgoing() throws IllegalArgumentException {
            return outgoing;
        }

        ----
        public HashSet<Vertex<V>> getIncoming() throws IllegalArgumentException {
            return incoming;
            } 
            ----
        public HashMap<Vertex<V>, Edge<V>> getIncoming() throws IllegalArgumentException {
            return incoming;
        }

        // ---- snip ----
    }
Enter fullscreen mode Exit fullscreen mode

To clear up those red lines in your AdjacencyMapGraph class, go ahead and add the following code inside it. This should sort out any errors you're seeing in your text editor.

// ---- snip ----
---- private void removeEdge(HashSet<Vertex<V>> edges, Vertex<V> vertex, boolean isOutgoing) { ----
private void removeEdge(HashMap<Vertex<V>, Edge<V>> edges, Vertex<V> vertex, boolean isOutgoing) {
    for (Vertex<V> ve : edges.keySet()) {
        InnerVertex<V> vert = validate(ve);
        if (isOutgoing) {
            vert.getIncoming().remove(vertex);
        } else {
            vert.getOutgoing().remove(vertex);
        }

        numEdges--;
    }
    edges.clear();
}

// -- snip --
@Override
public V removeVertex(Vertex<V> v) throws IllegalArgumentException {
    // ---- snip ----

        ---- HashSet<Vertex<V>> outgoing = vertex.getOutgoing(); ----
    HashMap<Vertex<V>, Edge<V>> outgoing = vertex.getOutgoing();
    removeEdge(outgoing, vertex, true);

        ---- HashSet<Vertex<V>> incoming = vertex.getIncoming(); ----
    HashMap<Vertex<V>, Edge<V>> incoming = vertex.getIncoming();
    removeEdge(incoming, vertex, false);

        // ---- snip ----
    return temp;
}

// ---- snip ----
@Override
public void insertEdge(Vertex<V> u, Vertex<V> v, int wight) throws IllegalArgumentException {
    // ---- snip ----
    if (!hasEdge(u, v)) {
            InnerVertex<V> origin = validate(u);
        InnerVertex<V> destination = validate(v);
        Edge<V> edge = new Edge<>(origin, destination, wight);

                ---- origin.getOutgoing().add(destination); ----
        ---- destination.getIncoming().add(origin); ----
        origin.getOutgoing().put(destination, edge);
        destination.getIncoming().put(origin, edge);
        numEdges++;
    } else
        throw new IllegalArgumentException("Edge from u to v exists");
}
Enter fullscreen mode Exit fullscreen mode

Dijkstra’s Algorithm

Dijkstra's algorithm a blessing designed for those moments when you absolutely, positively need to find the shortest path between two points in a tangled mess of connections.

This clever little method doesn't just guess; it meticulously explores the graph, like a highly organized detective with a clipboard. It keeps track of where it's been (the "visited nodes" list) and how far it is to everywhere else (the "distance table"). Then, it repeatedly updates its "best guess" for how to get to neighboring nodes until, voilà, it's figured out the shortest route to every single reachable spot. So, next time you're stuck in a labyrinth, just remember Dijkstra—it's probably already mapped the fastest way out!

Our algorithm uses a handy little HashMap to keep tabs on how far each vertex thinks it is from the starting line, and a 'cloud' (because 'set of visited nodes' just sounds too boring) for all the vertices that have already had their moment in the sun.

But the real MVP, the one we built a tiny shrine for, is our magnificent PriorityQueue.This unsung hero makes sure the vertex with the shortest distance always cuts to the front of the line.

Here's the lowdown: we start by stuffing our lovely PriorityQueue and our distance HashMap. If a vertex is the 'source,' we graciously grant it a distance of zero. Everyone else? Straight to 'maximum integer' distance – tough luck, chumps.

Then, it's a looping frenzy until our PriorityQueue is completely exhausted. In each round, we heroically pop the current 'shortest distance' champion. Then, we parade through its outgoing edges, doing some quick math. If we find a path (current distance + edge weight) that's shorter than what's currently in our distance HashMap , we update it and invite that lucky vertex to the PriorityQueue party. Because who doesn't love a good shortcut?

You've absorbed the wisdom; now, go ahead and paste the code below into your GraphAlgorithms class.

import java.util.PriorityQueue;
import java.util.Queue;

public class GraphAlgorithms {
// ----- snip -----
public static <V> HashMap<Vertex<V>, Integer> shortestPathLengths(Graph<V> graph, Vertex<V> source) {
      HashMap<Vertex<V>, Integer> distances = new HashMap<>();
      HashMap<Vertex<V>, Integer> cloud = new HashMap<>();

      Queue<Edge<V>> queue = new PriorityQueue<>(Comparator.comparingInt(Edge::getWeight));

      for (Vertex<V> vertex : graph.vertices()) {
          if (vertex == source) {
              distances.put(vertex, 0);
              queue.offer(new Edge<>(source, null, 0));
          } else {
              distances.put(vertex, Integer.MAX_VALUE);
              queue.offer(new Edge<>(source, null, Integer.MAX_VALUE));
          }
      }
      int size = queue.size();
      int counter = 0;

      while (size > counter && !queue.isEmpty()) {
          Edge<V> currentEdge = queue.poll();
          Vertex<V> currentVertex = currentEdge.getOrigin();
          int distance = currentEdge.getWeight();

          cloud.put(currentVertex, distance);
          for (Edge<V> edge : graph.outgoingEdges(currentVertex)) {
              Vertex<V> vertex = edge.opposite(currentVertex);

              if (cloud.get(vertex) == null) {
                  int weight = edge.getWeight();
                  int d = distance + weight;

                  if (d < distances.get(vertex)) {
                      distances.put(vertex, d);
                      queue.offer(new Edge<>(vertex, null, distances.get(vertex)));
                  }
              }
          }
          counter++;
      }

      return cloud;
  }

  // ----- snip -----
Enter fullscreen mode Exit fullscreen mode

Alright, let's build that graph and put our Dijkstra's algorithm to the ultimate test! You can see from the figure below that the shortest distance from vertex 0 to vertex 4 is 17. We'll set up the graph just like that, then run our code to confirm it. Head over to your main() method and drop in the following code.

Dijkstra’s_Algorithm

AdjacencyMapGraph<Integer> graph = new AdjacencyMapGraph<>(false);

Vertex<Integer> zero = graph.insertVertex(0);
Vertex<Integer> one = graph.insertVertex(1);
Vertex<Integer> two = graph.insertVertex(2);
Vertex<Integer> three = graph.insertVertex(3);
Vertex<Integer> four = graph.insertVertex(4);
Vertex<Integer> five = graph.insertVertex(5);
Vertex<Integer> six = graph.insertVertex(6);

graph.insertEdge(zero, two, 6);
graph.insertEdge(zero, one, 2);
graph.insertEdge(two, three, 8);
graph.insertEdge(one, three, 5);
graph.insertEdge(three, four, 10);
graph.insertEdge(three, five, 15);
graph.insertEdge(four, six, 2);
graph.insertEdge(five, six, 6);

var dijkstra = GraphAlgorithms.shortestPathLengths(graph, zero);
for (var vertex : dijkstra.entrySet()) {
    System.out.println("Start Vertex: " + zero.getElement() + " -> " +
            vertex.getKey().getElement()
            + ", Distance: " + vertex.getValue());
}
Enter fullscreen mode Exit fullscreen mode

Minimum Spanning Tree MST

Imagine you've got a bunch of scattered islands (vertices) and a limited budget for bridges (edges with weights). An MST is that clever little subset of bridges that connects every single island, using the absolute minimum amount of bridge-building material possible, all while avoiding any silly circular detours (cycles). Essentially, it's the "cheapest" and most efficient way to wire up your entire network without creating any redundant loops.

When tackling this problem, you'll meet two algorithms: Kruskal's Algorithm and Prim's Algorithm. They're both inherently greedy, always making the seemingly selfish choice at each step to get the job done.

Kruskal's algorithm

Kruskal's Algorithm, works by iteratively snatching up the lowest-weight edge it can find, as long as it doesn't create a cycle in our growing Minimum Spanning Tree. It keeps doing this until every single vertex has been personally invited to the party.

In essence, Kruskal's is the super-organized type: it first lines up all the graph's edges from smallest to largest, like a meticulous librarian. Then, it goes down the line, adding edges and nodes to the MST only if they promise not to cause a cyclical drama. It's all about picking the feather-light edges first and saving the heavy lifters for last. And for the grand finale? It gracefully merges any trees that haven't quite connected yet, like a matchmaker bringing lonely branches together.

Now, as for implementing this genius... ah, my dear friends, I shall leave that thrilling adventure to your capable hands.

Prim's Algorithm

Meet Prim's Algorithm, the other star in our MST show! If Kruskal's is the meticulous librarian, Prim's is more of a social climber. It's another greedy guru for finding the minimum spanning tree of a weighted, undirected graph.

How does it work? Imagine Prim's starting from an arbitrary vertex (like picking a random person at a party). Then, it continuously sniffs out the absolute cheapest connection that links its ever-expanding social circle (the growing tree) to someone new (a fresh vertex). It keeps adding these 'best new friends' until everyone at the party is connected in the most cost-effective way possible! No cycles allowed, just efficient networking.

For our implementation, we're getting super organized! We'll be using an ArrayList<Edge<V>> to neatly store all the edges that make it into our MST. To steer clear of the dreaded cycle, we've got a trusty HashSet on standby, ready to mark vertices as 'visited.'

And, of course, no heroic journey is complete without our hero: the PriorityQueue. Our algorithm will embark on an iterative quest, starting from the source, meticulously adding unvisited vertices to the queue and carefully selecting the edges to populate our ArrayList<Edge<V>>. It's all about precision, avoiding loops, and letting our PriorityQueue do the heavy lifting!

public static <V> ArrayList<Edge<V>> MST(Graph<V> graph, Vertex<V> source) {
      ArrayList<Edge<V>> tree = new ArrayList<>();
      HashSet<Vertex<V>> visited = new HashSet<>();

      Queue<Edge<V>> queue = new PriorityQueue<>(Comparator.comparingInt(Edge::getWeight));
      queue.add(new Edge<>(source, null, 0));

      while (!queue.isEmpty()) {
          Edge<V> currentEdge = queue.poll();
          Vertex<V> currentVertex;

          if (currentEdge.getDestination() == null) {
              currentVertex = currentEdge.getOrigin();
          } else {
              currentVertex = currentEdge.getDestination();
          }

          if (!visited.contains(currentEdge.getOrigin()) || !visited.contains(currentVertex)) {
              Vertex<V> destination = currentEdge.getDestination();

              Edge<V> edge = destination == null ? null : graph.getEdge(currentEdge.getOrigin(), destination);
              if (edge != null) {
                  tree.add(edge);
              }
              visited.add(currentVertex);
              visited.add(currentEdge.getOrigin());
          }

          for (Edge<V> edge : graph.outgoingEdges(currentVertex)) {
              Vertex<V> vertex = edge.opposite(currentVertex);
              int weight = edge.getWeight();

              if (!visited.contains(vertex) || !visited.contains(currentVertex)) {
                  Edge<V> edgeWeight = new Edge<>(currentVertex, vertex, weight);
                  queue.offer(edgeWeight);
              }
          }
      }

      return tree;
  }
Enter fullscreen mode Exit fullscreen mode

To prove Prim's algorithm, we're going to construct a graph in code that's identical to the figure below. This is where we see if all our hard work pays off. Dive into your main() method and add the code below.

minimum_spanning_tree_MST

AdjacencyMapGraph<Integer> graph = new AdjacencyMapGraph<>(false);

Vertex<Integer> one = graph.insertVertex(1);
Vertex<Integer> two = graph.insertVertex(2);
Vertex<Integer> three = graph.insertVertex(3);
Vertex<Integer> four = graph.insertVertex(4);
Vertex<Integer> five = graph.insertVertex(5);

// Insert edges 
graph.insertEdge(one, two, 2);
graph.insertEdge(one, three, 3);
graph.insertEdge(two, three, 1);
graph.insertEdge(two, four, 3);
graph.insertEdge(two, five, 2);
graph.insertEdge(four, five, 5);
graph.insertEdge(three, five, 1);

var mst = GraphAlgorithms.MST(graph, zero);
for (Edge<Integer> edge : mst) {
    System.out.println("Edge: " + edge.getOrigin().getElement() + " -> " +
            edge.getDestination().getElement() + ", Weight: " + edge.getWeight());
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)