Skip to content

Commit 63734b9

Browse files
cpovirkGoogle Java Core Libraries
authored and
Google Java Core Libraries
committed
Use iteration instead of recursion in Graphs.hasCycle.
This can avoid stack overflows for graphs with long paths. RELNOTES=`graph`: Improved `Graphs.hasCycle` to avoid causing `StackOverflowError` for long paths. PiperOrigin-RevId: 653681462
1 parent 61bfd84 commit 63734b9

File tree

4 files changed

+158
-42
lines changed

4 files changed

+158
-42
lines changed

android/guava-tests/test/com/google/common/graph/GraphPropertiesTest.java

+22
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,17 @@ public void hasCycle_multipleCycles() {
155155
assertThat(hasCycle(undirectedGraph)).isTrue();
156156
}
157157

158+
@Test
159+
public void hasCycle_deepPathGraph() {
160+
for (MutableGraph<Integer> graph : graphsToTest) {
161+
for (int i = 0; i < 100000; i++) {
162+
graph.putEdge(i, i + 1);
163+
}
164+
}
165+
assertThat(hasCycle(directedNetwork)).isFalse();
166+
assertThat(hasCycle(undirectedNetwork)).isFalse();
167+
}
168+
158169
@Test
159170
public void hasCycle_twoParallelEdges() {
160171
for (MutableNetwork<Integer, String> network : networksToTest) {
@@ -176,4 +187,15 @@ public void hasCycle_cyclicMultigraph() {
176187
assertThat(hasCycle(directedNetwork)).isTrue();
177188
assertThat(hasCycle(undirectedNetwork)).isTrue();
178189
}
190+
191+
@Test
192+
public void hasCycle_deepPathNetwork() {
193+
for (MutableNetwork<Integer, String> network : networksToTest) {
194+
for (int i = 0; i < 100000; i++) {
195+
network.addEdge(i, i + 1, Integer.toString(i));
196+
}
197+
}
198+
assertThat(hasCycle(directedNetwork)).isFalse();
199+
assertThat(hasCycle(undirectedNetwork)).isFalse();
200+
}
179201
}

android/guava/src/com/google/common/graph/Graphs.java

+57-21
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@
2727
import com.google.common.collect.Iterators;
2828
import com.google.common.collect.Maps;
2929
import com.google.errorprone.annotations.CanIgnoreReturnValue;
30+
import java.util.ArrayDeque;
3031
import java.util.Collection;
32+
import java.util.Deque;
3133
import java.util.HashSet;
3234
import java.util.Iterator;
3335
import java.util.Map;
36+
import java.util.Queue;
3437
import java.util.Set;
3538
import javax.annotation.CheckForNull;
3639

@@ -68,7 +71,7 @@ public static <N> boolean hasCycle(Graph<N> graph) {
6871
Map<Object, NodeVisitState> visitedNodes =
6972
Maps.newHashMapWithExpectedSize(graph.nodes().size());
7073
for (N node : graph.nodes()) {
71-
if (subgraphHasCycle(graph, visitedNodes, node, null)) {
74+
if (subgraphHasCycle(graph, visitedNodes, node)) {
7275
return true;
7376
}
7477
}
@@ -94,34 +97,67 @@ public static boolean hasCycle(Network<?, ?> network) {
9497
}
9598

9699
/**
97-
* Performs a traversal of the nodes reachable from {@code node}. If we ever reach a node we've
98-
* already visited (following only outgoing edges and without reusing edges), we know there's a
99-
* cycle in the graph.
100+
* Performs a traversal of the nodes reachable from {@code startNode}. If we ever reach a node
101+
* we've already visited (following only outgoing edges and without reusing edges), we know
102+
* there's a cycle in the graph.
100103
*/
101104
private static <N> boolean subgraphHasCycle(
102-
Graph<N> graph,
103-
Map<Object, NodeVisitState> visitedNodes,
104-
N node,
105-
@CheckForNull N previousNode) {
106-
NodeVisitState state = visitedNodes.get(node);
107-
if (state == NodeVisitState.COMPLETE) {
108-
return false;
109-
}
110-
if (state == NodeVisitState.PENDING) {
111-
return true;
112-
}
105+
Graph<N> graph, Map<Object, NodeVisitState> visitedNodes, N startNode) {
106+
Deque<NodeAndRemainingSuccessors<N>> stack = new ArrayDeque<>();
107+
stack.addLast(new NodeAndRemainingSuccessors<>(startNode));
108+
109+
while (!stack.isEmpty()) {
110+
// To peek at the top two items, we need to temporarily remove one.
111+
NodeAndRemainingSuccessors<N> top = stack.removeLast();
112+
NodeAndRemainingSuccessors<N> prev = stack.peekLast();
113+
stack.addLast(top);
114+
115+
N node = top.node;
116+
N previousNode = prev == null ? null : prev.node;
117+
if (top.remainingSuccessors == null) {
118+
NodeVisitState state = visitedNodes.get(node);
119+
if (state == NodeVisitState.COMPLETE) {
120+
stack.removeLast();
121+
continue;
122+
}
123+
if (state == NodeVisitState.PENDING) {
124+
return true;
125+
}
113126

114-
visitedNodes.put(node, NodeVisitState.PENDING);
115-
for (N nextNode : graph.successors(node)) {
116-
if (canTraverseWithoutReusingEdge(graph, nextNode, previousNode)
117-
&& subgraphHasCycle(graph, visitedNodes, nextNode, node)) {
118-
return true;
127+
visitedNodes.put(node, NodeVisitState.PENDING);
128+
top.remainingSuccessors = new ArrayDeque<>(graph.successors(node));
129+
}
130+
131+
if (!top.remainingSuccessors.isEmpty()) {
132+
N nextNode = top.remainingSuccessors.remove();
133+
if (canTraverseWithoutReusingEdge(graph, nextNode, previousNode)) {
134+
stack.addLast(new NodeAndRemainingSuccessors<>(nextNode));
135+
continue;
136+
}
119137
}
138+
139+
stack.removeLast();
140+
visitedNodes.put(node, NodeVisitState.COMPLETE);
120141
}
121-
visitedNodes.put(node, NodeVisitState.COMPLETE);
122142
return false;
123143
}
124144

145+
private static final class NodeAndRemainingSuccessors<N> {
146+
final N node;
147+
148+
/**
149+
* The successors left to be visited, or {@code null} if we just added this {@code
150+
* NodeAndRemainingSuccessors} instance to the stack. In the latter case, we'll compute the
151+
* successors if we determine that we need them after we've performed the initial processing of
152+
* the node.
153+
*/
154+
@CheckForNull Queue<N> remainingSuccessors;
155+
156+
NodeAndRemainingSuccessors(N node) {
157+
this.node = node;
158+
}
159+
}
160+
125161
/**
126162
* Determines whether an edge has already been used during traversal. In the directed case a cycle
127163
* is always detected before reusing an edge, so no special logic is required. In the undirected

guava-tests/test/com/google/common/graph/GraphPropertiesTest.java

+22
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,17 @@ public void hasCycle_multipleCycles() {
155155
assertThat(hasCycle(undirectedGraph)).isTrue();
156156
}
157157

158+
@Test
159+
public void hasCycle_deepPathGraph() {
160+
for (MutableGraph<Integer> graph : graphsToTest) {
161+
for (int i = 0; i < 100000; i++) {
162+
graph.putEdge(i, i + 1);
163+
}
164+
}
165+
assertThat(hasCycle(directedNetwork)).isFalse();
166+
assertThat(hasCycle(undirectedNetwork)).isFalse();
167+
}
168+
158169
@Test
159170
public void hasCycle_twoParallelEdges() {
160171
for (MutableNetwork<Integer, String> network : networksToTest) {
@@ -176,4 +187,15 @@ public void hasCycle_cyclicMultigraph() {
176187
assertThat(hasCycle(directedNetwork)).isTrue();
177188
assertThat(hasCycle(undirectedNetwork)).isTrue();
178189
}
190+
191+
@Test
192+
public void hasCycle_deepPathNetwork() {
193+
for (MutableNetwork<Integer, String> network : networksToTest) {
194+
for (int i = 0; i < 100000; i++) {
195+
network.addEdge(i, i + 1, Integer.toString(i));
196+
}
197+
}
198+
assertThat(hasCycle(directedNetwork)).isFalse();
199+
assertThat(hasCycle(undirectedNetwork)).isFalse();
200+
}
179201
}

guava/src/com/google/common/graph/Graphs.java

+57-21
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@
2727
import com.google.common.collect.Iterators;
2828
import com.google.common.collect.Maps;
2929
import com.google.errorprone.annotations.CanIgnoreReturnValue;
30+
import java.util.ArrayDeque;
3031
import java.util.Collection;
32+
import java.util.Deque;
3133
import java.util.HashSet;
3234
import java.util.Iterator;
3335
import java.util.Map;
3436
import java.util.Optional;
37+
import java.util.Queue;
3538
import java.util.Set;
3639
import javax.annotation.CheckForNull;
3740

@@ -69,7 +72,7 @@ public static <N> boolean hasCycle(Graph<N> graph) {
6972
Map<Object, NodeVisitState> visitedNodes =
7073
Maps.newHashMapWithExpectedSize(graph.nodes().size());
7174
for (N node : graph.nodes()) {
72-
if (subgraphHasCycle(graph, visitedNodes, node, null)) {
75+
if (subgraphHasCycle(graph, visitedNodes, node)) {
7376
return true;
7477
}
7578
}
@@ -95,34 +98,67 @@ public static boolean hasCycle(Network<?, ?> network) {
9598
}
9699

97100
/**
98-
* Performs a traversal of the nodes reachable from {@code node}. If we ever reach a node we've
99-
* already visited (following only outgoing edges and without reusing edges), we know there's a
100-
* cycle in the graph.
101+
* Performs a traversal of the nodes reachable from {@code startNode}. If we ever reach a node
102+
* we've already visited (following only outgoing edges and without reusing edges), we know
103+
* there's a cycle in the graph.
101104
*/
102105
private static <N> boolean subgraphHasCycle(
103-
Graph<N> graph,
104-
Map<Object, NodeVisitState> visitedNodes,
105-
N node,
106-
@CheckForNull N previousNode) {
107-
NodeVisitState state = visitedNodes.get(node);
108-
if (state == NodeVisitState.COMPLETE) {
109-
return false;
110-
}
111-
if (state == NodeVisitState.PENDING) {
112-
return true;
113-
}
106+
Graph<N> graph, Map<Object, NodeVisitState> visitedNodes, N startNode) {
107+
Deque<NodeAndRemainingSuccessors<N>> stack = new ArrayDeque<>();
108+
stack.addLast(new NodeAndRemainingSuccessors<>(startNode));
109+
110+
while (!stack.isEmpty()) {
111+
// To peek at the top two items, we need to temporarily remove one.
112+
NodeAndRemainingSuccessors<N> top = stack.removeLast();
113+
NodeAndRemainingSuccessors<N> prev = stack.peekLast();
114+
stack.addLast(top);
115+
116+
N node = top.node;
117+
N previousNode = prev == null ? null : prev.node;
118+
if (top.remainingSuccessors == null) {
119+
NodeVisitState state = visitedNodes.get(node);
120+
if (state == NodeVisitState.COMPLETE) {
121+
stack.removeLast();
122+
continue;
123+
}
124+
if (state == NodeVisitState.PENDING) {
125+
return true;
126+
}
114127

115-
visitedNodes.put(node, NodeVisitState.PENDING);
116-
for (N nextNode : graph.successors(node)) {
117-
if (canTraverseWithoutReusingEdge(graph, nextNode, previousNode)
118-
&& subgraphHasCycle(graph, visitedNodes, nextNode, node)) {
119-
return true;
128+
visitedNodes.put(node, NodeVisitState.PENDING);
129+
top.remainingSuccessors = new ArrayDeque<>(graph.successors(node));
130+
}
131+
132+
if (!top.remainingSuccessors.isEmpty()) {
133+
N nextNode = top.remainingSuccessors.remove();
134+
if (canTraverseWithoutReusingEdge(graph, nextNode, previousNode)) {
135+
stack.addLast(new NodeAndRemainingSuccessors<>(nextNode));
136+
continue;
137+
}
120138
}
139+
140+
stack.removeLast();
141+
visitedNodes.put(node, NodeVisitState.COMPLETE);
121142
}
122-
visitedNodes.put(node, NodeVisitState.COMPLETE);
123143
return false;
124144
}
125145

146+
private static final class NodeAndRemainingSuccessors<N> {
147+
final N node;
148+
149+
/**
150+
* The successors left to be visited, or {@code null} if we just added this {@code
151+
* NodeAndRemainingSuccessors} instance to the stack. In the latter case, we'll compute the
152+
* successors if we determine that we need them after we've performed the initial processing of
153+
* the node.
154+
*/
155+
@CheckForNull Queue<N> remainingSuccessors;
156+
157+
NodeAndRemainingSuccessors(N node) {
158+
this.node = node;
159+
}
160+
}
161+
126162
/**
127163
* Determines whether an edge has already been used during traversal. In the directed case a cycle
128164
* is always detected before reusing an edge, so no special logic is required. In the undirected

0 commit comments

Comments
 (0)