mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-12-19 07:00:35 +08:00
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:
committed by
GitHub
parent
4858ec9af0
commit
6b7d201657
@@ -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 Hierholzer’s 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 Hierholzer’s 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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user