欧拉回路是图论中最经典的问题之一:给定无向图或有向图,找到一条经过每条边恰好一次并最终回到起点的闭合路径。该问题源于哥尼斯堡七桥问题,由欧拉在1736年解决并开创了图论学科。本文用Java实现两种经典算法——Fleury算法与Hierholzer算法,包含欧拉路存在性判定、完整代码实现与复杂度分析。
一、欧拉回路的判定定理
在寻找欧拉回路之前,必须判定其是否存在。欧拉给出了如下经典定理:
无向图:连通图存在欧拉回路,当且仅当每个顶点的度数均为偶数;存在欧拉通路(非回路),当且仅当恰好有两个顶点度数为奇数。
有向图:弱连通的有向图存在欧拉回路,当且仅当每个顶点的入度等于出度;存在欧拉通路,当且仅当恰好一个顶点出度比入度大1(起点),一个顶点入度比出度大1(终点),其余顶点入度等于出度。
import java.util.*;
/**
* 图结构:支持有向/无向图,邻接表存储
*/
public class Graph {
public int n; // 顶点数
public List<List<Edge>> adj; // 邻接表
public boolean directed; // 是否是有向图
private int edgeCount = 0;
public Graph(int n, boolean directed) {
this.n = n;
this.directed = directed;
adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
}
/** 添加边,返回边的唯一ID */
public void addEdge(int u, int v) {
int eid = edgeCount++;
adj.get(u).add(new Edge(u, v, eid));
if (!directed) {
adj.get(v).add(new Edge(v, u, eid));
}
}
/** 获取所有边的数量 */
public int edgeCount() { return edgeCount; }
/**
* 判定欧拉回路/通路是否存在
* @return [是否存在欧拉回路, 是否存在欧拉通路, 起点(若存在通路)]
*/
public boolean[] checkEuler() {
if (directed) return checkDirected();
else return checkUndirected();
}
private boolean[] checkUndirected() {
// 检查连通性(忽略孤立点)
if (!isConnected()) return new boolean[]{false, false, false};
int oddCount = 0;
int start = 0;
for (int i = 0; i < n; i++) {
if (adj.get(i).size() % 2 == 1) {
oddCount++;
start = i;
}
}
// 所有偶度 -> 欧拉回路
// 恰好两个奇度 -> 欧拉通路
boolean hasCircuit = (oddCount == 0);
boolean hasPath = (oddCount == 0 || oddCount == 2);
return new boolean[]{hasCircuit, hasPath, hasPath};
}
private boolean[] checkDirected() {
if (!isWeaklyConnected()) return new boolean[]{false, false, false};
int[] inDegree = new int[n];
int[] outDegree = new int[n];
for (int u = 0; u < n; u++) {
outDegree[u] = adj.get(u).size();
for (Edge e : adj.get(u)) {
inDegree[e.to]++;
}
}
int startPlus = 0, endPlus = 0;
int start = 0;
for (int i = 0; i < n; i++) {
int diff = outDegree[i] - inDegree[i];
if (Math.abs(diff) > 1) return new boolean[]{false, false, false};
if (diff == 1) { startPlus++; start = i; }
else if (diff == -1) endPlus++;
}
boolean hasCircuit = (startPlus == 0 && endPlus == 0);
boolean hasPath = (startPlus == 1 && endPlus == 1) || hasCircuit;
return new boolean[]{hasCircuit, hasPath, hasPath};
}
/** BFS检查无向图连通性 */
private boolean isConnected() {
boolean[] vis = new boolean[n];
int start = -1;
for (int i = 0; i < n; i++) {
if (!adj.get(i).isEmpty()) { start = i; break; }
}
if (start == -1) return true; // 空图
Queue<Integer> q = new LinkedList<>();
q.add(start);
vis[start] = true;
while (!q.isEmpty()) {
int u = q.poll();
for (Edge e : adj.get(u)) {
if (!vis[e.to]) { vis[e.to] = true; q.add(e.to); }
}
}
for (int i = 0; i < n; i++) {
if (!adj.get(i).isEmpty() && !vis[i]) return false;
}
return true;
}
/** BFS检查有向图弱连通性 */
private boolean isWeaklyConnected() {
// 构建无向版本的邻接表
List<Set<Integer>> undirected = new ArrayList<>();
for (int i = 0; i < n; i++) undirected.add(new HashSet<>());
for (int u = 0; u < n; u++) {
for (Edge e : adj.get(u)) {
undirected.get(u).add(e.to);
undirected.get(e.to).add(u);
}
}
boolean[] vis = new boolean[n];
int start = -1;
for (int i = 0; i < n; i++) {
if (!adj.get(i).isEmpty()) { start = i; break; }
}
if (start == -1) return true;
Queue<Integer> q = new LinkedList<>();
q.add(start); vis[start] = true;
while (!q.isEmpty()) {
int u = q.poll();
for (int v : undirected.get(u)) {
if (!vis[v]) { vis[v] = true; q.add(v); }
}
}
for (int i = 0; i < n; i++) {
if (!adj.get(i).isEmpty() && !vis[i]) return false;
}
return true;
}
}
/** 边结构 */
public class Edge {
public int from, to;
public int id;
public boolean used = false;
public Edge(int from, int to, int id) {
this.from = from;
this.to = to;
this.id = id;
}
}
二、Fleury算法——桥边优先策略
Fleury算法的核心思想是:除非别无选择,否则不走桥边。桥边(割边)是指删除后会使图不连通的边,如果提前走了桥边,可能会被困在某个连通分量中无法返回。
/**
* Fleury算法实现
* 时间复杂度:O(E^2),因为每次都要判断桥边
*/
public class FleuryAlgorithm {
/**
* 寻找欧拉回路/通路
* @param g 图
* @param start 起点
* @return 顶点访问序列,若无解返回空列表
*/
public static List<Integer> findEulerPath(Graph g, int start) {
// 复制图结构用于算法(因为会修改边状态)
Graph graph = cloneGraph(g);
List<Integer> path = new ArrayList<>();
int current = start;
path.add(current);
while (true) {
List<Edge> edges = graph.adj.get(current);
Edge nextEdge = null;
// 寻找非桥边
for (Edge e : edges) {
if (e.used) continue;
if (!isBridge(graph, e)) {
nextEdge = e;
break;
}
}
// 若没有非桥边,选择任意可用边(必须是最后一条)
if (nextEdge == null) {
for (Edge e : edges) {
if (!e.used) {
nextEdge = e;
break;
}
}
}
// 无可用边,算法结束
if (nextEdge == null) break;
// 标记边为已使用
nextEdge.used = true;
// 无向图需标记反向边
if (!graph.directed) {
for (Edge rev : graph.adj.get(nextEdge.to)) {
if (rev.to == current && rev.id == nextEdge.id) {
rev.used = true;
break;
}
}
}
current = nextEdge.to;
path.add(current);
}
// 校验是否用完所有边
int usedCount = 0;
for (List<Edge> list : graph.adj) {
for (Edge e : list) if (e.used) usedCount++;
}
int totalEdges = graph.directed ? graph.edgeCount() : graph.edgeCount() * 2;
if (usedCount != totalEdges) {
return new ArrayList<>(); // 无解
}
return path;
}
/** 判断边是否为桥边(Tarjan思想简化版) */
private static boolean isBridge(Graph g, Edge edge) {
if (edge.used) return false;
// 若该顶点的可用边仅有一条,则必走
int available = 0;
for (Edge e : g.adj.get(edge.from)) {
if (!e.used) available++;
}
if (available == 1) return false;
// DFS检查删除该边后是否仍然连通
boolean[] vis = new boolean[g.n];
// 临时标记该边和反向边为已用
edge.used = true;
Edge reverse = null;
if (!g.directed) {
for (Edge e : g.adj.get(edge.to)) {
if (e.to == edge.from && e.id == edge.id) {
reverse = e;
e.used = true;
break;
}
}
}
// 从edge.from出发DFS看能否到达edge.to
boolean reachable = dfsReachable(g, edge.from, edge.to, vis);
// 恢复边状态
edge.used = false;
if (reverse != null) reverse.used = false;
// 若不可达,则是桥边
return !reachable;
}
private static boolean dfsReachable(Graph g, int u, int target, boolean[] vis) {
if (u == target) return true;
vis[u] = true;
for (Edge e : g.adj.get(u)) {
if (!e.used && !vis[e.to]) {
if (dfsReachable(g, e.to, target, vis)) return true;
}
}
return false;
}
/** 深拷贝图(仅复制边结构) */
private static Graph cloneGraph(Graph g) {
Graph ng = new Graph(g.n, g.directed);
// 通过重新添加边来复制
Set<String> added = new HashSet<>();
for (int u = 0; u < g.n; u++) {
for (Edge e : g.adj.get(u)) {
String key = Math.min(e.from, e.to) + "-" + Math.max(e.from, e.to) + "-" + e.id;
if (!g.directed && added.contains(key)) continue;
added.add(key);
ng.addEdge(e.from, e.to);
}
}
return ng;
}
}
三、Hierholzer算法——栈与回退策略
Hierholzer算法效率更高,核心思想是:沿着未访问的边随意走,直到无法前进时回溯,将回溯路径拼接。该算法时间复杂度为 O(E),是求解欧拉回路的工业标准方法。
/**
* Hierholzer算法实现
* 时间复杂度:O(E),空间复杂度:O(V + E)
*/
public class HierholzerAlgorithm {
/**
* 寻找欧拉回路/通路(Hierholzer算法)
*/
public static List<Integer> findEulerPath(Graph g, int start) {
Graph graph = cloneGraph(g);
Stack<Integer> stack = new Stack<>();
List<Integer> path = new ArrayList<>();
stack.push(start);
while (!stack.isEmpty()) {
int u = stack.peek();
boolean hasUnusedEdge = false;
// 找到一条未使用的出边
for (Edge e : graph.adj.get(u)) {
if (!e.used) {
e.used = true;
// 无向图标记反向边
if (!graph.directed) {
for (Edge rev : graph.adj.get(e.to)) {
if (rev.to == u && rev.id == e.id) {
rev.used = true;
break;
}
}
}
stack.push(e.to);
hasUnusedEdge = true;
break;
}
}
// 若没有未使用的出边,回退并记录路径
if (!hasUnusedEdge) {
path.add(stack.pop());
}
}
// path是逆序的,需要反转
Collections.reverse(path);
// 校验
int usedCount = 0;
for (List<Edge> list : graph.adj) {
for (Edge e : list) if (e.used) usedCount++;
}
int totalEdges = graph.directed ? graph.edgeCount() : graph.edgeCount() * 2;
if (usedCount != totalEdges) {
return new ArrayList<>();
}
return path;
}
/** 迭代版:避免递归栈溢出,适合大规模图 */
public static List<Integer> findEulerPathIterative(Graph g, int start) {
Graph graph = cloneGraph(g);
List<Integer> circuit = new ArrayList<>();
// 为每个顶点维护当前遍历到的边索引
int[] edgePtr = new int[graph.n];
Deque<Integer> stack = new ArrayDeque<>();
stack.push(start);
while (!stack.isEmpty()) {
int u = stack.peek();
if (edgePtr[u] < graph.adj.get(u).size()) {
Edge e = graph.adj.get(u).get(edgePtr[u]++);
if (e.used) continue;
e.used = true;
if (!graph.directed) {
for (Edge rev : graph.adj.get(e.to)) {
if (rev.to == u && rev.id == e.id) {
rev.used = true;
break;
}
}
}
stack.push(e.to);
} else {
circuit.add(stack.pop());
}
}
Collections.reverse(circuit);
return circuit;
}
private static Graph cloneGraph(Graph g) {
Graph ng = new Graph(g.n, g.directed);
Set<String> added = new HashSet<>();
for (int u = 0; u < g.n; u++) {
for (Edge e : g.adj.get(u)) {
String key = Math.min(e.from, e.to) + "-" + Math.max(e.from, e.to) + "-" + e.id;
if (!g.directed && added.contains(key)) continue;
added.add(key);
ng.addEdge(e.from, e.to);
}
}
return ng;
}
}
四、完整测试与演示
public class EulerPathDemo {
public static void main(String[] args) {
// 示例1:经典的无向图欧拉回路(七桥问题变体)
System.out.println("=== 无向图欧拉回路 ===");
Graph g1 = new Graph(5, false);
g1.addEdge(0, 1);
g1.addEdge(0, 2);
g1.addEdge(1, 2);
g1.addEdge(1, 3);
g1.addEdge(2, 3);
g1.addEdge(2, 4);
g1.addEdge(3, 4);
boolean[] check = g1.checkEuler();
System.out.println("Has Euler Circuit: " + check[0]);
System.out.println("Has Euler Path: " + check[1]);
if (check[1]) {
List<Integer> fleury = FleuryAlgorithm.findEulerPath(g1, 0);
System.out.println("Fleury Path: " + fleury);
List<Integer> hierholzer = HierholzerAlgorithm.findEulerPath(g1, 0);
System.out.println("Hierholzer Path: " + hierholzer);
}
// 示例2:有向图欧拉回路
System.out.println("\n=== 有向图欧拉回路 ===");
Graph g2 = new Graph(4, true);
g2.addEdge(0, 1);
g2.addEdge(1, 2);
g2.addEdge(2, 3);
g2.addEdge(3, 0);
g2.addEdge(0, 2);
g2.addEdge(2, 0);
boolean[] check2 = g2.checkEuler();
System.out.println("Has Euler Circuit: " + check2[0]);
if (check2[0]) {
List<Integer> path = HierholzerAlgorithm.findEulerPath(g2, 0);
System.out.println("Hierholzer Path: " + path);
}
// 示例3:大规模图性能测试
System.out.println("\n=== 性能测试 ===");
int n = 10000;
Graph big = new Graph(n, false);
Random rand = new Random(42);
for (int i = 0; i < n; i++) {
big.addEdge(i, (i + 1) % n);
big.addEdge(i, (i + 2) % n);
}
long t1 = System.currentTimeMillis();
List<Integer> p1 = HierholzerAlgorithm.findEulerPath(big, 0);
System.out.println("Hierholzer on " + n + " nodes: " + (System.currentTimeMillis() - t1) + "ms, path length=" + p1.size());
}
}
五、算法对比与复杂度分析
| 指标 | Fleury算法 | Hierholzer算法 |
|---|---|---|
| 时间复杂度 | O(E²) | O(E) |
| 空间复杂度 | O(V + E) | O(V + E) |
| 核心思想 | 避免桥边 | 随意走+回溯拼接 |
| 适用规模 | 小规模图(E < 10⁴) | 大规模图 |
| 实现难度 | 需要判断桥边 | 简单栈操作 |
桥边判断优化:若使用Tarjan算法预处理桥边,可将Fleury优化至 O(E),但代码复杂度显著增加,不如直接使用Hierholzer算法。
六、实际应用场景
- 路线规划:邮递员问题(中国邮路问题)的基础子问题
- DNA序列拼接:De Bruijn图中寻找欧拉通路进行基因组组装
- 电路板钻孔:优化钻孔路径,使钻头移动距离最短
- 密码学:De Bruijn序列生成基于欧拉回路
七、总结
本文完整实现了欧拉回路的判定与求解算法:
- 欧拉定理判定:基于顶点度数的奇偶性/出入度差,快速判定解的存在性
- Fleury算法:以”不走桥边”为核心策略,直观但效率较低
- Hierholzer算法:利用栈的回退机制,线性时间高效求解,是工业首选
两种算法各有适用场景:Fleury适合教学理解,Hierholzer适合实际工程部署。