A long time ago I posted my implementation of directed graph is Java. Recently I took a look at @coderodde's implementation and decided to write an implementation of undirected graph and directed graph. Besides I wrote some automated tests for both classes.
It's partially based on @coderodde's solution.
Here is the code:
AbstractGraph
package api;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
/**
* The implementation is partially based on
* https://codereview.stackexchange.com/q/116686/23821
*
* @author Maksim
*
* @param <T>
*/
public abstract class AbstractGraph<T> {
// O(V+E)
protected final Map<T, Map<T, Double>> graphData;
private final Map<T, Color> colorMap = new HashMap<>();
private int edgeCount;
public AbstractGraph() {
graphData = new HashMap<>();
}
public AbstractGraph(AbstractGraph<T> graph) {
graphData = new HashMap<>(graph.graphData);
}
/**
* Returns the number of nodes in this graph.
*
* @return the size of this graph.
*/
public final int getNodeCount() {
return graphData.size();
}
/**
* Returns the number of edges in this graph.
*
* @return the number of edges.
*/
public final int getEdgeCount() {
return edgeCount;
}
/**
* Adds the node with ID {@code nodeId} to this graph. O(1)
*
* @param nodeId
* the ID of the node to add.
* @return {@code true} if the structure of this graph has changed, which is
* the same as that the added node was not present in the graph.
*/
public final boolean addNode(T nodeId) {
if (hasNode(nodeId)) {
return false;
} else {
graphData.put(nodeId, null);
return true;
}
}
/**
* Checks whether the given node is present in this graph.
*
* @param nodeId
* the query node.
* @return {@code true} if the query node is in this graph. {@code false}
* otherwise.
*/
public final boolean hasNode(T nodeId) {
return graphData.containsKey(nodeId);
}
/**
* Removes the argument node from this graph.
*
* @param nodeId
* the node to remove.
* @return {@code true} only if the node was present in the graph which
* means that the structure of the graph has changed.
*/
public final boolean removeNode(T nodeId) {
final boolean hasNode = hasNode(nodeId);
if (hasNode) {
graphData.remove(nodeId);
Iterator<T> nodeIterator = graphData.keySet().iterator();
while (nodeIterator.hasNext()) {
graphData.get(nodeIterator.next()).remove(nodeId);
}
}
return hasNode;
}
/**
* @param from
* @param to
* @param weight
* the weight of the edge.
* @return {@code true} only if the edge was not present in the graph, or
* the weight of the edge has changed.
*/
public final boolean addEdge(T from, T to, double weight) {
if (weight == Double.NEGATIVE_INFINITY || Double.isNaN(weight)) {
throw new IllegalArgumentException("weight must be a number or the positive inifinity");
}
ensureCanAddEdge(from);
ensureCanAddEdge(to);
Double oldWeight = graphData.get(from).put(to, weight);
if (oldWeight == null) {
edgeCount++;
}
boolean fromEdgeUpdated = (oldWeight == null || Double.compare(oldWeight, weight) != 0);
if (fromEdgeUpdated) {
addOppositeEdge(to, from, weight);
}
return fromEdgeUpdated;
}
final void ensureCanAddEdge(T node) {
Map<T, Double> fromEdges = graphData.get(node);
if (fromEdges == null) {
graphData.put(node, new HashMap<>());
}
}
abstract void addOppositeEdge(T to, T from, double weight);
/**
* Creates an edge between {@code tailNodeId} and {@code headNodeId} with
* the default weight of 1.0. This method is a shortcut for constructing
* (virtually) unweighted graphs.
*
* @param tailNodeId
* the tail node of the edge.
* @param headNodeId
* the head node of the edge.
* @return {@code true} only if the edge was not present in the graph, or
* the weight of the edge has changed.
*/
public final boolean addEdge(T from, T to) {
return addEdge(from, to, 1.0);
}
/**
* Returns the weight of the edge {@code (tailNodeId, headNodeId)}. If the
* query edge does not exist, returns {@link java.lang.Double#NaN}.
*
* @param from
* @param to
* @return the weight of the edge.
*/
public final double getEdgeWeight(T from, T to) {
if (hasEdge(from, to)) {
return graphData.get(from).get(to);
} else {
return Double.NaN;
}
}
/**
* Removes the edge from {@code from} and {@code to}.
*
* @param from
* the tail node of the edge to remove.
* @param to
* the head node of the edge to remove.
* @return {@code true} if the target edge was in this graph, and thus is
* removed.
*/
public final boolean removeEdge(T from, T to) {
if (hasEdge(from, to)) {
graphData.get(from).remove(to);
removeOppositeEdge(to, from);
return true;
} else {
return false;
}
}
abstract void removeOppositeEdge(T to, T from);
/**
* Returns a boolean value indicating whether this graph contains an edge
* from {@code tailNodeId} to {@code headNodeId}.
*
* @param tailNodeId
* the tail node of the query edge.
* @param headNodeId
* the head node of the query edge.
* @return {@code true} only if the query edge is in this graph.
*/
public final boolean hasEdge(T from, T to) {
Map<T, Double> fromAdj = graphData.get(from);
return fromAdj != null && fromAdj.get(to) != null;
}
/**
* Removes all nodes and edges from this graph.
*/
public final void clear() {
graphData.clear();
}
public final String dfs() {
if (graphData.isEmpty()) {
return "[]";
} else {
StringBuilder builder = new StringBuilder();
dfsInternal(graphData.keySet().iterator().next(), builder);
colorMap.clear();
return builder.toString();
}
}
private void dfsInternal(T node, StringBuilder builder) {
colorMap.put(node, Color.GREY);
builder.append(node + " ");
Map<T, Double> adj = graphData.get(node);
if (adj != null) {
for (T adjNode : adj.keySet()) {
// Not visited yet
if (colorMap.get(adjNode) == null) {
dfsInternal(adjNode, builder);
}
}
}
colorMap.put(node, Color.BLACK);
}
public final String bfs() {
if (graphData.isEmpty()) {
return "[]";
}
StringBuilder builder = new StringBuilder();
Queue<T> queue = new ArrayDeque<>();
queue.add(graphData.keySet().iterator().next());
while (!queue.isEmpty()) {
T node = queue.remove();
builder.append(node + " ");
colorMap.put(node, Color.GREY);
Map<T, Double> adjNodes = graphData.get(node);
if (adjNodes != null) {
for (T adjNode : adjNodes.keySet()) {
if (colorMap.get(adjNode) == null) {
queue.add(adjNode);
}
}
}
colorMap.put(node, Color.BLACK);
}
colorMap.clear();
return builder.toString();
}
private boolean hasCyclesInternal(T current, T source) {
colorMap.put(current, Color.GREY);
Map<T, Double> adjMap = graphData.get(current);
if (adjMap == null) {
colorMap.put(current, Color.BLACK);
return false;
} else {
Set<T> adjNodes = adjMap.keySet();
for (T adj : adjNodes) {
if (!colorMap.containsKey(adj)) {
if (hasCyclesInternal(adj, current)) {
return true;
}
} else if (!isSource(adj, source) && colorMap.get(adj) == Color.GREY) {
return true;
}
}
colorMap.put(current, Color.BLACK);
}
return false;
}
abstract boolean isSource(T adj, T source);
public final boolean hasCycles() {
if (graphData.isEmpty()) {
return false;
} else {
try {
return hasCyclesInternal(graphData.keySet().iterator().next(), null);
} finally {
colorMap.clear();
}
}
}
@Override
public String toString() {
return graphData.isEmpty() ? "[]" : graphData.toString();
}
private enum Color {
GREY, BLACK
}
}
UndirectedGraph
package api;
public class UndirectedGraph<T> extends AbstractGraph<T> {
public UndirectedGraph() {}
public UndirectedGraph(UndirectedGraph<T> graph) {
super(graph);
}
@Override
void addOppositeEdge(T to, T from, double weight) {
graphData.get(to).put(from, weight);
}
@Override
void removeOppositeEdge(T to, T from) {
graphData.get(to).remove(from);
}
@Override
boolean isSource(T node, T source) {
return node == source;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof UndirectedGraph)) {
return false;
}
UndirectedGraph<?> other = (UndirectedGraph<?>) obj;
return graphData.equals(other.graphData);
}
@Override
public int hashCode() {
return graphData.hashCode();
}
}
DirectedGraph
package api;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class DirectedGraph<T> extends AbstractGraph<T> {
public DirectedGraph() {}
public DirectedGraph(DirectedGraph<T> graph) {
super(graph);
}
public final Set<T> inDegreeOf(T nodeId) {
if (hasNode(nodeId)) {
Set<T> inDegree = new HashSet<>();
Iterator<T> fromIter = graphData.keySet().iterator();
while (fromIter.hasNext()) {
T from = fromIter.next();
if (graphData.get(from).containsKey(nodeId)) {
inDegree.add(from);
}
}
return inDegree;
} else {
return null;
}
}
@Override
void addOppositeEdge(T to, T from, double weight) {
}
@Override
void removeOppositeEdge(T to, T from) {
}
public final Set<T> outDegreeOf(T nodeId) {
if (graphData.containsKey(nodeId)) {
return graphData.get(nodeId).keySet();
} else {
return null;
}
}
@Override
boolean isSource(T node, T source) {
return false;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DirectedGraph)) {
return false;
}
DirectedGraph<?> other = (DirectedGraph<?>) obj;
return graphData.equals(other.graphData);
}
@Override
public int hashCode() {
return graphData.hashCode();
}
}
DirectedGraphTest
package api.test;
import java.util.HashSet;
import java.util.Set;
import org.junit.Assert;
import org.junit.Test;
import api.DirectedGraph;
import api.UndirectedGraph;
public class DirectedGraphTest {
@Test
public void inDegree() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(1, 2);
directedGraph.addEdge(2, 4);
directedGraph.addEdge(3, 4);
directedGraph.addEdge(5, 4);
directedGraph.addEdge(6, 4);
Set<Integer> expected = new HashSet<>();
expected.add(2);
expected.add(3);
expected.add(5);
expected.add(6);
Assert.assertEquals(expected, directedGraph.inDegreeOf(4));
}
@Test
public void outDegree() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(1, 2);
directedGraph.addEdge(2, 4);
directedGraph.addEdge(3, 4);
directedGraph.addEdge(5, 4);
directedGraph.addEdge(6, 4);
directedGraph.addEdge(2, 8);
directedGraph.addEdge(2, 7);
directedGraph.addEdge(2, 10);
Set<Integer> expected = new HashSet<>();
expected.add(4);
expected.add(7);
expected.add(8);
expected.add(10);
Assert.assertEquals(expected, directedGraph.outDegreeOf(2));
}
@Test
public void hasCycles() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(1, 2);
directedGraph.addEdge(2, 3);
directedGraph.addEdge(3, 1);
Assert.assertTrue(directedGraph.hasCycles());
}
@Test
public void hasNoCycles() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(1, 2);
directedGraph.addEdge(2, 3);
directedGraph.addEdge(1, 3);
Assert.assertFalse(directedGraph.hasCycles());
}
@Test
public void equalsSymmetric() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(12, 56);
directedGraph.addEdge(56, 100);
DirectedGraph<Integer> directedGraph2 = directedGraph;
Assert.assertTrue(directedGraph.equals(directedGraph2));
}
@Test
public void equalsReflecsive() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(12, 56);
directedGraph.addEdge(56, 100);
Assert.assertTrue(directedGraph.equals(directedGraph));
}
@Test
public void equalsTransitive() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(12, 56);
directedGraph.addEdge(56, 100);
DirectedGraph<Integer> directedGraph2 = new DirectedGraph<>();
directedGraph2.addEdge(12, 56);
directedGraph2.addEdge(56, 100);
DirectedGraph<Integer> directedGraph3 = new DirectedGraph<>();
directedGraph3.addEdge(12, 56);
directedGraph3.addEdge(56, 100);
Assert.assertTrue(directedGraph.equals(directedGraph2));
Assert.assertTrue(directedGraph2.equals(directedGraph3));
Assert.assertTrue(directedGraph.equals(directedGraph3));
}
@Test
public void equalsOtherWrongType() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(12, 56);
directedGraph.addEdge(56, 100);
UndirectedGraph<Integer> undirectedGraph = new UndirectedGraph<>();
undirectedGraph.addEdge(12, 56);
undirectedGraph.addEdge(56, 100);
Assert.assertFalse(directedGraph.equals(undirectedGraph));
}
@Test
public void equalsNotEqualGraphs() {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addEdge(12, 56);
directedGraph.addEdge(56, 100);
DirectedGraph<Integer> directedGraph2 = new DirectedGraph<>();
directedGraph2.addEdge(12, 57);
directedGraph2.addEdge(56, 100);
Assert.assertFalse(directedGraph.equals(directedGraph2));
}
}
UndirectedGraphTest
package api.test;
import org.junit.Assert;
import org.junit.Test;
import api.UndirectedGraph;
public class UndirectedGraphTest {
private static final double EPSILON = 0.000001;
@Test
public void addFirstNode() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
Assert.assertTrue(graph.addNode(1));
}
@Test
public void checkFirstNodeAdded() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1000);
Assert.assertTrue(graph.hasNode(1000));
}
@Test
public void checkNotExistingNodeAbsent() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1000);
Assert.assertFalse(graph.hasNode(2000));
}
@Test
public void addNode() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1);
Assert.assertTrue(graph.addNode(2));
}
@Test
public void addAlreadyAddedNode() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1);
Assert.assertFalse(graph.addNode(1));
}
@Test
public void addEdgeToEmptyGraph() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
Assert.assertTrue(graph.addEdge(1, 2));
}
@Test
public void checkEdgeAddedToEmptyGraph() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 2);
Assert.assertTrue(graph.hasEdge(1, 2));
}
@Test
public void checkOppositeEdgeAddedToEmptyGraph() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 2);
Assert.assertTrue(graph.hasEdge(2, 1));
}
@Test
public void connectExistingNodes() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1);
graph.addNode(2);
Assert.assertTrue(graph.addEdge(1, 2));
}
@Test
public void checkExistingNodesConnected() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1);
graph.addNode(2);
graph.addEdge(1, 2);
Assert.assertTrue(graph.hasEdge(1, 2));
}
@Test
public void checkExistingOppositeNodesConnected() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1);
graph.addNode(2);
graph.addEdge(1, 2);
Assert.assertTrue(graph.hasEdge(2, 1));
}
@Test
public void updateEdgeWeight() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 3);
graph.addEdge(1, 3, 2.0);
Assert.assertTrue(Double.compare(2.0, graph.getEdgeWeight(1, 3)) == 0);
}
@Test
public void checkWeightUpdated() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 3);
graph.addEdge(1, 3, 2.0);
final double expected = 2.0;
Assert.assertEquals(expected, graph.getEdgeWeight(1, 3), EPSILON);
}
@Test
public void checkOppositeWeightUpdated() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 3);
graph.addEdge(1, 3, 3.6);
final double expected = 3.6;
Assert.assertEquals(expected, graph.getEdgeWeight(1, 3), EPSILON);
}
@Test
public void connectAlreadyConnectedNodes() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 3);
Assert.assertFalse(graph.addEdge(1, 3));
}
@Test
public void connectOppositeOfAlreadyConnectedNodes() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 3);
Assert.assertFalse(graph.addEdge(3, 1));
}
@Test
public void hasNoCyclesTwoNodes() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 3);
Assert.assertFalse(graph.hasCycles());
}
@Test
public void hasSelfLoop() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 1);
Assert.assertTrue(graph.hasCycles());
}
@Test
public void hasNoLoopsSingleNode() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(12);
Assert.assertFalse(graph.hasCycles());
}
@Test
public void hasSelfLoop2() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 2);
graph.addEdge(2, 3);
graph.addEdge(3, 3);
Assert.assertTrue(graph.hasCycles());
}
@Test
public void hasCycles() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 3);
graph.addEdge(1, 2);
graph.addEdge(2, 3);
Assert.assertTrue(graph.hasCycles());
}
@Test(expected = IllegalArgumentException.class)
public void addNonEdge() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 2, Double.NaN);
}
@Test(expected = IllegalArgumentException.class)
public void addNegativeInfinityEdge() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 2, Double.NEGATIVE_INFINITY);
}
@Test
public void addPositiveInfinityEdge() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 2, Double.POSITIVE_INFINITY);
Assert.assertTrue(graph.getEdgeWeight(1, 2) == Double.POSITIVE_INFINITY);
}
@Test
public void nanWeightWhenNoEdge() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1);
graph.addNode(34);
Assert.assertTrue(Double.isNaN(graph.getEdgeWeight(1, 34)));
}
@Test
public void nanWeightWhenNoNodes() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addNode(1);
graph.addNode(34);
Assert.assertTrue(Double.isNaN(graph.getEdgeWeight(2, 67)));
}
@Test
public void hasCyclesEmpty() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
Assert.assertFalse(graph.hasCycles());
}
@Test
public void removeEdge() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 5);
Assert.assertTrue(graph.removeEdge(1, 2));
Assert.assertFalse(graph.hasEdge(2, 2));
Assert.assertTrue(graph.hasEdge(1, 3));
Assert.assertTrue(graph.hasEdge(2, 5));
}
@Test
public void removeNode() {
UndirectedGraph<Integer> graph = new UndirectedGraph<>();
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(1, 4);
graph.addEdge(1, 5);
graph.addEdge(3, 4);
graph.addEdge(3, 5);
graph.addEdge(4, 5);
graph.addEdge(4, 6);
graph.addEdge(2, 4);
graph.addEdge(5, 6);
Assert.assertTrue(graph.removeNode(3));
Assert.assertFalse(graph.hasEdge(1, 3));
Assert.assertFalse(graph.hasEdge(5, 3));
Assert.assertFalse(graph.hasEdge(4, 3));
}
}