Hierholzer path algorithm (#6822)

* Added HierholzerEulerianPath algorithm

* Added Hierholzer Algorith to find Eulerian Path

* Added Hierholzer Algorith to find Eulerian Path

* Added Hierholzer Algorith to find Eulerian Path

* Added Hierholzer Algorith to find Eulerian Path

* Added Hierholzer Algorith to find Eulerian Path

* Added Hierholzer Algorith to find Eulerian Path

* Added Hierholzer Algorith to find Eulerian Path

---------

Co-authored-by: crashmovies <swarnakarharshendu420@gmail.com>
This commit is contained in:
Harshendu Swarnakar
2025-10-18 03:23:26 +05:30
committed by GitHub
parent 4858ec9af0
commit 6b7d201657
2 changed files with 472 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
package com.thealgorithms.graph;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
/**
* Implementation of Hierholzer's Algorithm for finding an Eulerian Path or Circuit
* in a directed graph.
*
* <p>
* An <b>Eulerian Circuit</b> is a path that starts and ends at the same vertex
* and visits every edge exactly once.
* </p>
*
* <p>
* An <b>Eulerian Path</b> visits every edge exactly once but may start and end
* at different vertices.
* </p>
*
* <p>
* <b>Algorithm Summary:</b><br>
* 1. Compute indegree and outdegree for all vertices.<br>
* 2. Check if the graph satisfies Eulerian path or circuit conditions.<br>
* 3. Verify that all vertices with non-zero degree are weakly connected (undirected connectivity).<br>
* 4. Use Hierholzers algorithm to build the path by exploring unused edges iteratively.
* </p>
*
* <p>
* <b>Time Complexity:</b> O(E + V).<br>
* <b>Space Complexity:</b> O(V + E).
* </p>
*
* @author <a href="https://en.wikipedia.org/wiki/Eulerian_path#Hierholzer's_algorithm">Wikipedia: Hierholzer algorithm</a>
*/
public class HierholzerEulerianPath {
/**
* Simple directed graph represented by adjacency lists.
*/
public static class Graph {
private final List<List<Integer>> adjacencyList;
/**
* Constructs a graph with a given number of vertices.
*
* @param numNodes number of vertices
*/
public Graph(int numNodes) {
adjacencyList = new ArrayList<>();
for (int i = 0; i < numNodes; i++) {
adjacencyList.add(new ArrayList<>());
}
}
/**
* Adds a directed edge from vertex {@code from} to vertex {@code to}.
*
* @param from source vertex
* @param to destination vertex
*/
public void addEdge(int from, int to) {
adjacencyList.get(from).add(to);
}
/**
* Returns a list of outgoing edges from the given vertex.
*
* @param node vertex index
* @return list of destination vertices
*/
public List<Integer> getEdges(int node) {
return adjacencyList.get(node);
}
/**
* Returns the number of vertices in the graph.
*
* @return number of vertices
*/
public int getNumNodes() {
return adjacencyList.size();
}
}
private final Graph graph;
/**
* Creates a Hierholzer solver for the given graph.
*
* @param graph directed graph
*/
public HierholzerEulerianPath(Graph graph) {
this.graph = graph;
}
/**
* Finds an Eulerian Path or Circuit using Hierholzers Algorithm.
*
* @return list of vertices representing the Eulerian Path/Circuit,
* or an empty list if none exists
*/
public List<Integer> findEulerianPath() {
int n = graph.getNumNodes();
// empty graph -> no path
if (n == 0) {
return new ArrayList<>();
}
int[] inDegree = new int[n];
int[] outDegree = new int[n];
int edgeCount = computeDegrees(inDegree, outDegree);
// no edges -> single vertex response requested by tests: [0]
if (edgeCount == 0) {
return Collections.singletonList(0);
}
int startNode = determineStartNode(inDegree, outDegree);
if (startNode == -1) {
return new ArrayList<>();
}
if (!allNonZeroDegreeVerticesWeaklyConnected(startNode, n, outDegree, inDegree)) {
return new ArrayList<>();
}
List<Integer> path = buildHierholzerPath(startNode, n);
if (path.size() != edgeCount + 1) {
return new ArrayList<>();
}
return rotateEulerianCircuitIfNeeded(path, outDegree, inDegree);
}
private int computeDegrees(int[] inDegree, int[] outDegree) {
int edgeCount = 0;
for (int u = 0; u < graph.getNumNodes(); u++) {
for (int v : graph.getEdges(u)) {
outDegree[u]++;
inDegree[v]++;
edgeCount++;
}
}
return edgeCount;
}
private int determineStartNode(int[] inDegree, int[] outDegree) {
int n = graph.getNumNodes();
int startNode = -1;
int startCount = 0;
int endCount = 0;
for (int i = 0; i < n; i++) {
int diff = outDegree[i] - inDegree[i];
if (diff == 1) {
startNode = i;
startCount++;
} else if (diff == -1) {
endCount++;
} else if (Math.abs(diff) > 1) {
return -1;
}
}
if (!((startCount == 1 && endCount == 1) || (startCount == 0 && endCount == 0))) {
return -1;
}
if (startNode == -1) {
for (int i = 0; i < n; i++) {
if (outDegree[i] > 0) {
startNode = i;
break;
}
}
}
return startNode;
}
private List<Integer> buildHierholzerPath(int startNode, int n) {
List<Deque<Integer>> tempAdj = new ArrayList<>();
for (int i = 0; i < n; i++) {
tempAdj.add(new ArrayDeque<>(graph.getEdges(i)));
}
Deque<Integer> stack = new ArrayDeque<>();
List<Integer> path = new ArrayList<>();
stack.push(startNode);
while (!stack.isEmpty()) {
int u = stack.peek();
if (!tempAdj.get(u).isEmpty()) {
stack.push(tempAdj.get(u).pollFirst());
} else {
path.add(stack.pop());
}
}
Collections.reverse(path);
return path;
}
private List<Integer> rotateEulerianCircuitIfNeeded(List<Integer> path, int[] outDegree, int[] inDegree) {
int startCount = 0;
int endCount = 0;
for (int i = 0; i < outDegree.length; i++) {
int diff = outDegree[i] - inDegree[i];
if (diff == 1) {
startCount++;
} else if (diff == -1) {
endCount++;
}
}
if (startCount == 0 && endCount == 0 && !path.isEmpty()) {
int preferredStart = -1;
for (int i = 0; i < outDegree.length; i++) {
if (outDegree[i] > 0) {
preferredStart = i;
break;
}
}
if (preferredStart != -1 && path.get(0) != preferredStart) {
int idx = 0;
for (Integer node : path) { // replaced indexed loop
if (node == preferredStart) {
break;
}
idx++;
}
if (idx > 0) {
List<Integer> rotated = new ArrayList<>();
int currentIndex = 0;
for (Integer node : path) { // replaced indexed loop
if (currentIndex >= idx) {
rotated.add(node);
}
currentIndex++;
}
currentIndex = 0;
for (Integer node : path) { // replaced indexed loop
if (currentIndex < idx) {
rotated.add(node);
}
currentIndex++;
}
path = rotated;
}
}
}
return path;
}
/**
* Checks weak connectivity (undirected) among vertices that have non-zero degree.
*
* @param startNode node to start DFS from (must be a vertex with non-zero degree)
* @param n number of vertices
* @param outDegree out-degree array
* @param inDegree in-degree array
* @return true if all vertices having non-zero degree belong to a single weak component
*/
private boolean allNonZeroDegreeVerticesWeaklyConnected(int startNode, int n, int[] outDegree, int[] inDegree) {
boolean[] visited = new boolean[n];
Deque<Integer> stack = new ArrayDeque<>();
stack.push(startNode);
visited[startNode] = true;
while (!stack.isEmpty()) {
int u = stack.pop();
for (int v : graph.getEdges(u)) {
if (!visited[v]) {
visited[v] = true;
stack.push(v);
}
}
for (int x = 0; x < n; x++) {
if (!visited[x]) {
for (int y : graph.getEdges(x)) {
if (y == u) {
visited[x] = true;
stack.push(x);
break;
}
}
}
}
}
for (int i = 0; i < n; i++) {
if (outDegree[i] + inDegree[i] > 0 && !visited[i]) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,169 @@
package com.thealgorithms.graph;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link HierholzerEulerianPath}.
*
* This test suite validates Hierholzer's Algorithm implementation
* for finding Eulerian Paths and Circuits in directed graphs.
*
* <p>Coverage includes:
* <ul>
* <li>Basic Eulerian Circuit</li>
* <li>Eulerian Path</li>
* <li>Disconnected graphs</li>
* <li>Single-node graphs</li>
* <li>Graphs with no edges</li>
* <li>Graphs that do not have any Eulerian Path/Circuit</li>
* </ul>
* </p>
*/
class HierholzerEulerianPathTest {
@Test
void testSimpleEulerianCircuit() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3);
graph.addEdge(0, 1);
graph.addEdge(1, 2);
graph.addEdge(2, 0);
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Eulerian Circuit: [0, 1, 2, 0]
List<Integer> expected = Arrays.asList(0, 1, 2, 0);
assertEquals(expected, result);
}
@Test
void testEulerianPathDifferentStartEnd() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(4);
graph.addEdge(0, 1);
graph.addEdge(1, 2);
graph.addEdge(2, 3);
graph.addEdge(3, 1);
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Eulerian Path: [0, 1, 2, 3, 1]
List<Integer> expected = Arrays.asList(0, 1, 2, 3, 1);
assertEquals(expected, result);
}
@Test
void testNoEulerianPathExists() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3);
graph.addEdge(0, 1);
graph.addEdge(1, 2);
// Edge 2->0 missing, so it's not Eulerian Circuit
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
assertEquals(result, Arrays.asList(0, 1, 2));
}
@Test
void testDisconnectedGraph() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(4);
graph.addEdge(0, 1);
graph.addEdge(2, 3); // disconnected component
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Disconnected graph cannot have an Eulerian path
assertTrue(result.isEmpty());
}
@Test
void testGraphWithSelfLoop() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3);
graph.addEdge(0, 0); // self loop
graph.addEdge(0, 1);
graph.addEdge(1, 2);
graph.addEdge(2, 0);
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Eulerian Circuit with self-loop included: [0, 0, 1, 2, 0]
assertEquals(Arrays.asList(0, 0, 1, 2, 0), result);
}
@Test
void testSingleNodeNoEdges() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(1);
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Only one vertex and no edges
assertEquals(Collections.singletonList(0), result);
}
@Test
void testSingleNodeWithLoop() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(1);
graph.addEdge(0, 0);
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Eulerian circuit on a single node with a self-loop
assertEquals(Arrays.asList(0, 0), result);
}
@Test
void testComplexEulerianCircuit() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(5);
graph.addEdge(0, 1);
graph.addEdge(1, 2);
graph.addEdge(2, 3);
graph.addEdge(3, 4);
graph.addEdge(4, 0);
graph.addEdge(1, 3);
graph.addEdge(3, 1);
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Verify all edges are used
int totalEdges = 7;
assertEquals(totalEdges + 1, result.size(), "Path must contain all edges + 1 vertices");
assertEquals(result.get(0), result.get(result.size() - 1), "Must form a circuit");
}
@Test
void testMultipleEdgesBetweenSameNodes() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3);
graph.addEdge(0, 1);
graph.addEdge(0, 1);
graph.addEdge(1, 2);
graph.addEdge(2, 0);
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Hava a Eulerian Path but not a Eulerian Circuit
assertEquals(result, Arrays.asList(0, 1, 2, 0, 1));
}
@Test
void testCoverageForEmptyGraph() {
HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(0);
HierholzerEulerianPath solver = new HierholzerEulerianPath(graph);
List<Integer> result = solver.findEulerianPath();
// Empty graph has no vertices or path
assertTrue(result.isEmpty());
}
}