Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: Add more test cases to improve code coverage
  • Loading branch information
sairamsharan committed Oct 11, 2025
commit 568913c6a62308d8f3b055c9b7629f590a9c3b96
119 changes: 82 additions & 37 deletions src/main/java/com/thealgorithms/graph/HierholzerAlgorithm.java
Original file line number Diff line number Diff line change
@@ -1,90 +1,118 @@
package com.thealgorithms.graph;

import java.util.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

/**
* An implementation of Hierholzer's Algorithm to find an Eulerian Path or Circuit in an undirected graph.
* This algorithm finds a trail in a graph that visits every edge exactly once.
* Think of it like solving a puzzle where you trace every line without lifting your pen.
* An Eulerian path is a trail in a graph that visits every edge exactly once.
* An Eulerian circuit is an Eulerian path that starts and ends at the same vertex.
*
* Wikipedia: https://en.wikipedia.org/wiki/Eulerian_path#Hierholzer's_algorithm
*/
public class HierholzerAlgorithm {

private final int numVertices;
private final Map<Integer, LinkedList<Integer>> graph;

/**
* Sets up the algorithm with the graph we want to solve.
* Constructor for the algorithm.
* @param graph The graph represented as an adjacency list.
* Using a LinkedList for neighbors is efficient for edge removal.
*/
public HierholzerAlgorithm(Map<Integer, LinkedList<Integer>> graph) {
this.graph = (graph == null) ? new HashMap<>() : graph;
if (graph == null) {
this.graph = new HashMap<>();
this.numVertices = 0;
return;
}
this.graph = graph;
this.numVertices = graph.size();
}

/**
* Before starting, we have to ask: can this puzzle even be solved?
* This method checks the two essential rules for an undirected graph.
* @return true if a circuit is possible, false otherwise.
* Checks if an Eulerian circuit exists in the undirected graph.
* Condition: All vertices with a non-zero degree must be in a single connected component,
* and all vertices must have an even degree.
* @return true if a circuit exists, false otherwise.
*/
public boolean hasEulerianCircuit() {
if (graph.isEmpty()) {
return true; // An empty puzzle is trivially solved.
return true; // An empty graph has an empty circuit.
}

// Rule 1: Every point must have an even number of lines connected to it.
// This ensures for every way in, there's a way out.
// Check 1: All vertices must have an even degree.
for (int vertex : graph.keySet()) {
if (graph.get(vertex).size() % 2 != 0) {
return false; // Found a point with an odd number of lines.
return false; // Found a vertex with an odd degree.
}
}

// Rule 2: The drawing must be one single, connected piece.
// You can't have a separate, floating part of the puzzle.
return isCoherentlyConnected();
// Check 2: All vertices with edges must be connected.
if (!isCoherentlyConnected()) {
return false;
}

return true;
}

/**
* This is the main event—finding the actual path.
* @return A list of points (vertices) that make up the complete circuit.
* Finds the Eulerian circuit.
* @return A list of vertices representing the circuit, or an empty list if none exists.
*/
public List<Integer> findEulerianCircuit() {
if (!hasEulerianCircuit()) {
// If the puzzle can't be solved, return an empty path.
return Collections.emptyList();
}

// We'll work on a copy of the graph so we don't destroy the original.
// Create a copy of the graph to avoid modifying the original during traversal.
Map<Integer, LinkedList<Integer>> tempGraph = new HashMap<>();
for (Map.Entry<Integer, LinkedList<Integer>> entry : graph.entrySet()) {
tempGraph.put(entry.getKey(), new LinkedList<>(entry.getValue()));
}

// 'currentPath' is our breadcrumb trail as we explore.
// Data structures for the algorithm.
Stack<Integer> currentPath = new Stack<>();
// 'circuit' is where we'll lay out the final, complete path.
List<Integer> circuit = new LinkedList<>();

// Find any point to start from.
int startVertex = graph.keySet().stream().findFirst().orElse(-1);
if (startVertex == -1) return Collections.emptyList();
// Find a starting vertex (any vertex with edges).
int startVertex = -1;
for (int vertex : tempGraph.keySet()) {
if (!tempGraph.get(vertex).isEmpty()) {
startVertex = vertex;
break;
}
}

if (startVertex == -1) {
if (graph.isEmpty()) {
return Collections.emptyList();
}
return Collections.singletonList(graph.keySet().iterator().next()); // Graph with one isolated vertex.
}

currentPath.push(startVertex);

while (!currentPath.isEmpty()) {
int currentVertex = currentPath.peek();

// If there's an unexplored hallway from our current location...
// If the current vertex has unvisited edges
if (tempGraph.containsKey(currentVertex) && !tempGraph.get(currentVertex).isEmpty()) {
// ...let's go down it.
int nextVertex = tempGraph.get(currentVertex).pollFirst();
// Erase the hallway behind us so we don't use it again.
int nextVertex = tempGraph.get(currentVertex).pollFirst(); // Get a neighbor

// Remove the reverse edge as well (for undirected graph)
tempGraph.get(nextVertex).remove(Integer.valueOf(currentVertex));
// Add the new location to our breadcrumb trail.

// Push the neighbor to the stack to continue the tour
currentPath.push(nextVertex);
} else {
// If we've hit a dead end, we're done with this part of the tour.
// We add our location to the final path and backtrack.
// If "stuck" (no more edges), backtrack and add to the final circuit.
circuit.add(0, currentPath.pop());
}
}
Expand All @@ -93,20 +121,37 @@ public List<Integer> findEulerianCircuit() {
}

/**
* A helper to check if the graph is one single piece.
* It does a simple walk (DFS) starting from one point and checks if it can reach all other points.
* Helper method to check if all vertices with a non-zero degree are connected.
* Uses a simple traversal (DFS).
*/
private boolean isCoherentlyConnected() {
if (graph.isEmpty()) return true;
if (graph.isEmpty()) {
return true;
}

Set<Integer> visited = new HashSet<>();
int startNode = graph.keySet().stream().findFirst().orElse(-1);
if (startNode == -1) return true;
int startNode = -1;

// Find the first vertex with a degree greater than 0
for (int vertex : graph.keySet()) {
if (!graph.get(vertex).isEmpty()) {
startNode = vertex;
break;
}
}

// If no edges in the graph, it's connected.
if (startNode == -1) {
return true;
}

// Perform DFS from the start node
dfs(startNode, visited);

// Check if all vertices with edges were visited
for (int vertex : graph.keySet()) {
if (!graph.get(vertex).isEmpty() && !visited.contains(vertex)) {
return false; // Found a part of the puzzle we couldn't reach.
return false; // Found a vertex with edges that wasn't visited
}
}
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.thealgorithms.graph;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;

public class HierholzerAlgorithmTest {
Expand Down Expand Up @@ -31,16 +37,42 @@ public void testFindsEulerianCircuitInSimpleTriangleGraph() {
}

@Test
public void testHandlesGraphWithNoEulerianCircuit() {
// Create a graph where a vertex has an odd degree
public void testFailsForGraphWithOddDegreeVertices() {
// Create a graph where vertices 0 and 1 have an odd degree (1)
Map<Integer, LinkedList<Integer>> graph = new HashMap<>();
graph.put(0, new LinkedList<>(Collections.singletonList(1)));
graph.put(1, new LinkedList<>(Collections.singletonList(0)));
graph.put(2, new LinkedList<>(Collections.emptyList())); // Vertex 2 is isolated

HierholzerAlgorithm algorithm = new HierholzerAlgorithm(graph);

// The algorithm should correctly identify that no circuit exists
assertEquals(false, algorithm.hasEulerianCircuit());
assertFalse(algorithm.hasEulerianCircuit());
// The find method should return an empty list
assertTrue(algorithm.findEulerianCircuit().isEmpty());
}

@Test
public void testFailsForDisconnectedGraph() {
// Create a graph with two separate triangles (0-1-2 and 3-4-5)
Map<Integer, LinkedList<Integer>> graph = new HashMap<>();
graph.put(0, new LinkedList<>(Arrays.asList(1, 2)));
graph.put(1, new LinkedList<>(Arrays.asList(0, 2)));
graph.put(2, new LinkedList<>(Arrays.asList(0, 1)));
graph.put(3, new LinkedList<>(Arrays.asList(4, 5)));
graph.put(4, new LinkedList<>(Arrays.asList(3, 5)));
graph.put(5, new LinkedList<>(Arrays.asList(3, 4)));

HierholzerAlgorithm algorithm = new HierholzerAlgorithm(graph);

// All degrees are even, but the graph is not connected, so no circuit exists
assertFalse(algorithm.hasEulerianCircuit());
}

@Test
public void testHandlesEmptyGraph() {
Map<Integer, LinkedList<Integer>> graph = new HashMap<>();
HierholzerAlgorithm algorithm = new HierholzerAlgorithm(graph);
assertTrue(algorithm.hasEulerianCircuit());
assertTrue(algorithm.findEulerianCircuit().isEmpty());
}
}
Loading