From 32f2d770f7117015385083ec6e755336ddf18f21 Mon Sep 17 00:00:00 2001 From: benyu Date: Mon, 13 Jul 2020 16:56:16 -0700 Subject: [PATCH] Make breadth-first iterators consume the successor iterators lazily, and refactor to cut about 150 lines of code. RELNOTES=Lazier Traverser#breadthFirst(). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=321062203 --- .../google/common/graph/TraverserTest.java | 37 ++ .../com/google/common/graph/Traverser.java | 350 ++++++------------ .../google/common/graph/TraverserTest.java | 37 ++ .../com/google/common/graph/Traverser.java | 350 ++++++------------ 4 files changed, 284 insertions(+), 490 deletions(-) diff --git a/android/guava-tests/test/com/google/common/graph/TraverserTest.java b/android/guava-tests/test/com/google/common/graph/TraverserTest.java index 497c43bf33b7..fa446dee5cb3 100644 --- a/android/guava-tests/test/com/google/common/graph/TraverserTest.java +++ b/android/guava-tests/test/com/google/common/graph/TraverserTest.java @@ -184,6 +184,13 @@ public void forGraph_breadthFirstIterable_javadocExample_canBeIteratedMultipleTi assertEqualCharNodes(result, "bfaecd"); } + @Test + public void forGraph_breadthFirst_infinite() { + Iterable result = + Traverser.forGraph(fixedSuccessors(Iterables.cycle(1, 2, 3))).breadthFirst(0); + assertThat(Iterables.limit(result, 4)).containsExactly(0, 1, 2, 3).inOrder(); + } + @Test public void forGraph_breadthFirst_diamond() { Traverser traverser = Traverser.forGraph(DIAMOND_GRAPH); @@ -373,6 +380,13 @@ public void forGraph_depthFirstPreOrderIterable_javadocExample_canBeIteratedMult assertEqualCharNodes(result, "bacefd"); } + @Test + public void forGraph_depthFirstPreOrder_infinite() { + Iterable result = + Traverser.forGraph(fixedSuccessors(Iterables.cycle(1, 2, 3))).breadthFirst(0); + assertThat(Iterables.limit(result, 2)).containsExactly(0, 1).inOrder(); + } + @Test public void forGraph_depthFirstPreOrder_diamond() { Traverser traverser = Traverser.forGraph(DIAMOND_GRAPH); @@ -784,6 +798,13 @@ public void forTree_withUndirectedNetwork_throws() throws Exception { } } + @Test + public void forTree_breadthFirst_infinite() { + Iterable result = + Traverser.forTree(fixedSuccessors(Iterables.cycle(1, 2, 3))).breadthFirst(0); + assertThat(Iterables.limit(result, 8)).containsExactly(0, 1, 2, 3, 1, 2, 3, 1).inOrder(); + } + @Test public void forTree_breadthFirst_tree() throws Exception { Traverser traverser = Traverser.forTree(TREE); @@ -912,6 +933,13 @@ public void forTree_breadthFirstIterable_iterableIsLazy() { assertThat(graph.requestedNodes).containsExactly('a', 'a', 'd', 'd', 'd', 'g', 'g', 'g'); } + @Test + public void forTree_depthFirstPreOrder_infinite() { + Iterable result = + Traverser.forTree(fixedSuccessors(Iterables.cycle(1, 2, 3))).depthFirstPreOrder(0); + assertThat(Iterables.limit(result, 3)).containsExactly(0, 1, 1).inOrder(); + } + @Test public void forTree_depthFirstPreOrderIterable_tree() throws Exception { Traverser traverser = Traverser.forTree(TREE); @@ -1238,4 +1266,13 @@ public Iterable successors(Character node) { return delegate.successors(node); } } + + private static SuccessorsFunction fixedSuccessors(final Iterable successors) { + return new SuccessorsFunction() { + @Override + public Iterable successors(N n) { + return successors; + } + }; + } } diff --git a/android/guava/src/com/google/common/graph/Traverser.java b/android/guava/src/com/google/common/graph/Traverser.java index 40c3d2a68ccd..4257eb828023 100644 --- a/android/guava/src/com/google/common/graph/Traverser.java +++ b/android/guava/src/com/google/common/graph/Traverser.java @@ -22,13 +22,10 @@ import com.google.common.annotations.Beta; import com.google.common.collect.AbstractIterator; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.common.collect.UnmodifiableIterator; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashSet; import java.util.Iterator; -import java.util.Queue; import java.util.Set; import org.checkerframework.checker.nullness.compatqual.NullableDecl; @@ -63,6 +60,11 @@ */ @Beta public abstract class Traverser { + private final SuccessorsFunction successorFunction; + + private Traverser(SuccessorsFunction successorFunction) { + this.successorFunction = checkNotNull(successorFunction); + } /** * Creates a new traverser for the given general {@code graph}. @@ -88,9 +90,13 @@ public abstract class Traverser { * * @param graph {@link SuccessorsFunction} representing a general graph that may have cycles. */ - public static Traverser forGraph(SuccessorsFunction graph) { - checkNotNull(graph); - return new GraphTraverser<>(graph); + public static Traverser forGraph(final SuccessorsFunction graph) { + return new Traverser(graph) { + @Override + Traversal newTraversal() { + return Traversal.inGraph(graph); + } + }; } /** @@ -166,15 +172,19 @@ public static Traverser forGraph(SuccessorsFunction graph) { * @param tree {@link SuccessorsFunction} representing a directed acyclic graph that has at most * one path between any two nodes */ - public static Traverser forTree(SuccessorsFunction tree) { - checkNotNull(tree); + public static Traverser forTree(final SuccessorsFunction tree) { if (tree instanceof BaseGraph) { checkArgument(((BaseGraph) tree).isDirected(), "Undirected graphs can never be trees."); } if (tree instanceof Network) { checkArgument(((Network) tree).isDirected(), "Undirected networks can never be trees."); } - return new TreeTraverser<>(tree); + return new Traverser(tree) { + @Override + Traversal newTraversal() { + return Traversal.inTree(tree); + } + }; } /** @@ -208,7 +218,9 @@ public static Traverser forTree(SuccessorsFunction tree) { * * @throws IllegalArgumentException if {@code startNode} is not an element of the graph */ - public abstract Iterable breadthFirst(N startNode); + public final Iterable breadthFirst(N startNode) { + return breadthFirst(ImmutableSet.of(startNode)); + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from any of the {@code @@ -220,7 +232,15 @@ public static Traverser forTree(SuccessorsFunction tree) { * @see #breadthFirst(Object) * @since 24.1 */ - public abstract Iterable breadthFirst(Iterable startNodes); + public final Iterable breadthFirst(Iterable startNodes) { + final ImmutableSet validated = validate(startNodes); + return new Iterable() { + @Override + public Iterator iterator() { + return newTraversal().breadthFirst(validated.iterator()); + } + }; + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from {@code startNode}, in @@ -253,7 +273,9 @@ public static Traverser forTree(SuccessorsFunction tree) { * * @throws IllegalArgumentException if {@code startNode} is not an element of the graph */ - public abstract Iterable depthFirstPreOrder(N startNode); + public final Iterable depthFirstPreOrder(N startNode) { + return depthFirstPreOrder(ImmutableSet.of(startNode)); + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from any of the {@code @@ -265,7 +287,15 @@ public static Traverser forTree(SuccessorsFunction tree) { * @see #depthFirstPreOrder(Object) * @since 24.1 */ - public abstract Iterable depthFirstPreOrder(Iterable startNodes); + public final Iterable depthFirstPreOrder(Iterable startNodes) { + final ImmutableSet validated = validate(startNodes); + return new Iterable() { + @Override + public Iterator iterator() { + return newTraversal().preOrder(validated.iterator()); + } + }; + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from {@code startNode}, in @@ -298,7 +328,9 @@ public static Traverser forTree(SuccessorsFunction tree) { * * @throws IllegalArgumentException if {@code startNode} is not an element of the graph */ - public abstract Iterable depthFirstPostOrder(N startNode); + public final Iterable depthFirstPostOrder(N startNode) { + return depthFirstPostOrder(ImmutableSet.of(startNode)); + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from any of the {@code @@ -310,229 +342,25 @@ public static Traverser forTree(SuccessorsFunction tree) { * @see #depthFirstPostOrder(Object) * @since 24.1 */ - public abstract Iterable depthFirstPostOrder(Iterable startNodes); - - // Avoid subclasses outside of this class - private Traverser() {} - - private static final class GraphTraverser extends Traverser { - private final SuccessorsFunction graph; - - GraphTraverser(SuccessorsFunction graph) { - this.graph = checkNotNull(graph); - } - - @Override - public Iterable breadthFirst(final N startNode) { - checkNotNull(startNode); - return breadthFirst(ImmutableSet.of(startNode)); - } - - @Override - public Iterable breadthFirst(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInGraph(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return new BreadthFirstIterator(startNodes); - } - }; - } - - @Override - public Iterable depthFirstPreOrder(final N startNode) { - checkNotNull(startNode); - return depthFirstPreOrder(ImmutableSet.of(startNode)); - } - - @Override - public Iterable depthFirstPreOrder(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInGraph(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return Walker.inGraph(graph).preOrder(startNodes.iterator()); - } - }; - } - - @Override - public Iterable depthFirstPostOrder(final N startNode) { - checkNotNull(startNode); - return depthFirstPostOrder(ImmutableSet.of(startNode)); - } - - @Override - public Iterable depthFirstPostOrder(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInGraph(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return Walker.inGraph(graph).postOrder(startNodes.iterator()); - } - }; - } - - @SuppressWarnings("CheckReturnValue") - private void checkThatNodeIsInGraph(N startNode) { - // successors() throws an IllegalArgumentException for nodes that are not an element of the - // graph. - graph.successors(startNode); - } - - private final class BreadthFirstIterator extends UnmodifiableIterator { - private final Queue queue = new ArrayDeque<>(); - private final Set visited = new HashSet<>(); - - BreadthFirstIterator(Iterable roots) { - for (N root : roots) { - // add all roots to the queue, skipping duplicates - if (visited.add(root)) { - queue.add(root); - } - } - } - + public final Iterable depthFirstPostOrder(Iterable startNodes) { + final ImmutableSet validated = validate(startNodes); + return new Iterable() { @Override - public boolean hasNext() { - return !queue.isEmpty(); + public Iterator iterator() { + return newTraversal().postOrder(validated.iterator()); } - - @Override - public N next() { - N current = queue.remove(); - for (N neighbor : graph.successors(current)) { - if (visited.add(neighbor)) { - queue.add(neighbor); - } - } - return current; - } - } + }; } - private static final class TreeTraverser extends Traverser { - private final SuccessorsFunction tree; - - TreeTraverser(SuccessorsFunction tree) { - this.tree = checkNotNull(tree); - } - - @Override - public Iterable breadthFirst(final N startNode) { - checkNotNull(startNode); - return breadthFirst(ImmutableSet.of(startNode)); - } - - @Override - public Iterable breadthFirst(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInTree(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return new BreadthFirstIterator(startNodes); - } - }; - } - - @Override - public Iterable depthFirstPreOrder(final N startNode) { - checkNotNull(startNode); - return depthFirstPreOrder(ImmutableSet.of(startNode)); - } + abstract Traversal newTraversal(); - @Override - public Iterable depthFirstPreOrder(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N node : startNodes) { - checkThatNodeIsInTree(node); - } - return new Iterable() { - @Override - public Iterator iterator() { - return Walker.inTree(tree).preOrder(startNodes.iterator()); - } - }; - } - - @Override - public Iterable depthFirstPostOrder(final N startNode) { - checkNotNull(startNode); - return depthFirstPostOrder(ImmutableSet.of(startNode)); - } - - @Override - public Iterable depthFirstPostOrder(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInTree(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return Walker.inTree(tree).postOrder(startNodes.iterator()); - } - }; - } - - @SuppressWarnings("CheckReturnValue") - private void checkThatNodeIsInTree(N startNode) { - // successors() throws an IllegalArgumentException for nodes that are not an element of the - // graph. - tree.successors(startNode); - } - - private final class BreadthFirstIterator extends UnmodifiableIterator { - private final Queue queue = new ArrayDeque<>(); - - BreadthFirstIterator(Iterable roots) { - for (N root : roots) { - queue.add(root); - } - } - - @Override - public boolean hasNext() { - return !queue.isEmpty(); - } - - @Override - public N next() { - N current = queue.remove(); - Iterables.addAll(queue, tree.successors(current)); - return current; - } + @SuppressWarnings("CheckReturnValue") + private ImmutableSet validate(Iterable startNodes) { + ImmutableSet copy = ImmutableSet.copyOf(startNodes); + for (N node : copy) { + successorFunction.successors(node); // Will throw if node doesn't exist } + return copy; } /** @@ -540,16 +368,16 @@ public N next() { * the next element from the next non-empty iterator; for graph, we need to loop through the next * non-empty iterator to find first unvisited node. */ - private abstract static class Walker { + private abstract static class Traversal { final SuccessorsFunction successorFunction; - Walker(SuccessorsFunction successorFunction) { - this.successorFunction = checkNotNull(successorFunction); + Traversal(SuccessorsFunction successorFunction) { + this.successorFunction = successorFunction; } - static Walker inGraph(SuccessorsFunction graph) { + static Traversal inGraph(SuccessorsFunction graph) { final Set visited = new HashSet<>(); - return new Walker(graph) { + return new Traversal(graph) { @Override N visitNext(Deque> horizon) { Iterator top = horizon.getFirst(); @@ -565,8 +393,8 @@ N visitNext(Deque> horizon) { }; } - static Walker inTree(SuccessorsFunction tree) { - return new Walker(tree) { + static Traversal inTree(SuccessorsFunction tree) { + return new Traversal(tree) { @Override N visitNext(Deque> horizon) { Iterator top = horizon.getFirst(); @@ -579,9 +407,23 @@ N visitNext(Deque> horizon) { }; } + final Iterator breadthFirst(Iterator startNodes) { + return topDown(startNodes, InsertionOrder.BACK); + } + final Iterator preOrder(Iterator startNodes) { + return topDown(startNodes, InsertionOrder.FRONT); + } + + /** + * In top-down traversal, an ancestor node is always traversed before any of its descendant + * nodes. The traversal order among descendant nodes (particularly aunts and nieces) are + * determined by the {@code InsertionOrder} parameter: nieces are placed at the FRONT before + * aunts for pre-order; while in BFS they are placed at the BACK after aunts. + */ + private Iterator topDown(Iterator startNodes, final InsertionOrder order) { final Deque> horizon = new ArrayDeque<>(); - horizon.addFirst(startNodes); + horizon.add(startNodes); return new AbstractIterator() { @Override protected N computeNext() { @@ -590,7 +432,9 @@ protected N computeNext() { if (next != null) { Iterator successors = successorFunction.successors(next).iterator(); if (successors.hasNext()) { - horizon.addFirst(successors); + // BFS: horizon.addLast(successors) + // Pre-order: horizon.addFirst(successors) + order.insertInto(horizon, successors); } return next; } @@ -601,9 +445,9 @@ protected N computeNext() { } final Iterator postOrder(Iterator startNodes) { - final Deque> horizon = new ArrayDeque<>(); - horizon.addFirst(startNodes); final Deque ancestorStack = new ArrayDeque<>(); + final Deque> horizon = new ArrayDeque<>(); + horizon.add(startNodes); return new AbstractIterator() { @Override protected N computeNext() { @@ -622,9 +466,7 @@ protected N computeNext() { /** * Visits the next node from the top iterator of {@code horizon} and returns the visited node. - * Null is returned to indicate reaching the end of the top iterator, which can be used by the - * traversal strategies to decide what to return in such case: in pre-order, continue to poll - * the next top iterator with {@code visitNext()}; in post-order, return the parent node. + * Null is returned to indicate reaching the end of the top iterator. * *

For example, if horizon is {@code [[a, b], [c, d], [e]]}, {@code visitNext()} will return * {@code [a, b, null, c, d, null, e, null]} sequentially, encoding the topological structure. @@ -635,4 +477,22 @@ protected N computeNext() { @NullableDecl abstract N visitNext(Deque> horizon); } + + /** Poor man's method reference for {@code Deque::addFirst} and {@code Deque::addLast}. */ + private enum InsertionOrder { + FRONT { + @Override + void insertInto(Deque deque, T value) { + deque.addFirst(value); + } + }, + BACK { + @Override + void insertInto(Deque deque, T value) { + deque.addLast(value); + } + }; + + abstract void insertInto(Deque deque, T value); + } } diff --git a/guava-tests/test/com/google/common/graph/TraverserTest.java b/guava-tests/test/com/google/common/graph/TraverserTest.java index 497c43bf33b7..fa446dee5cb3 100644 --- a/guava-tests/test/com/google/common/graph/TraverserTest.java +++ b/guava-tests/test/com/google/common/graph/TraverserTest.java @@ -184,6 +184,13 @@ public void forGraph_breadthFirstIterable_javadocExample_canBeIteratedMultipleTi assertEqualCharNodes(result, "bfaecd"); } + @Test + public void forGraph_breadthFirst_infinite() { + Iterable result = + Traverser.forGraph(fixedSuccessors(Iterables.cycle(1, 2, 3))).breadthFirst(0); + assertThat(Iterables.limit(result, 4)).containsExactly(0, 1, 2, 3).inOrder(); + } + @Test public void forGraph_breadthFirst_diamond() { Traverser traverser = Traverser.forGraph(DIAMOND_GRAPH); @@ -373,6 +380,13 @@ public void forGraph_depthFirstPreOrderIterable_javadocExample_canBeIteratedMult assertEqualCharNodes(result, "bacefd"); } + @Test + public void forGraph_depthFirstPreOrder_infinite() { + Iterable result = + Traverser.forGraph(fixedSuccessors(Iterables.cycle(1, 2, 3))).breadthFirst(0); + assertThat(Iterables.limit(result, 2)).containsExactly(0, 1).inOrder(); + } + @Test public void forGraph_depthFirstPreOrder_diamond() { Traverser traverser = Traverser.forGraph(DIAMOND_GRAPH); @@ -784,6 +798,13 @@ public void forTree_withUndirectedNetwork_throws() throws Exception { } } + @Test + public void forTree_breadthFirst_infinite() { + Iterable result = + Traverser.forTree(fixedSuccessors(Iterables.cycle(1, 2, 3))).breadthFirst(0); + assertThat(Iterables.limit(result, 8)).containsExactly(0, 1, 2, 3, 1, 2, 3, 1).inOrder(); + } + @Test public void forTree_breadthFirst_tree() throws Exception { Traverser traverser = Traverser.forTree(TREE); @@ -912,6 +933,13 @@ public void forTree_breadthFirstIterable_iterableIsLazy() { assertThat(graph.requestedNodes).containsExactly('a', 'a', 'd', 'd', 'd', 'g', 'g', 'g'); } + @Test + public void forTree_depthFirstPreOrder_infinite() { + Iterable result = + Traverser.forTree(fixedSuccessors(Iterables.cycle(1, 2, 3))).depthFirstPreOrder(0); + assertThat(Iterables.limit(result, 3)).containsExactly(0, 1, 1).inOrder(); + } + @Test public void forTree_depthFirstPreOrderIterable_tree() throws Exception { Traverser traverser = Traverser.forTree(TREE); @@ -1238,4 +1266,13 @@ public Iterable successors(Character node) { return delegate.successors(node); } } + + private static SuccessorsFunction fixedSuccessors(final Iterable successors) { + return new SuccessorsFunction() { + @Override + public Iterable successors(N n) { + return successors; + } + }; + } } diff --git a/guava/src/com/google/common/graph/Traverser.java b/guava/src/com/google/common/graph/Traverser.java index 258346020528..7ab5941c0f03 100644 --- a/guava/src/com/google/common/graph/Traverser.java +++ b/guava/src/com/google/common/graph/Traverser.java @@ -22,13 +22,10 @@ import com.google.common.annotations.Beta; import com.google.common.collect.AbstractIterator; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.common.collect.UnmodifiableIterator; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashSet; import java.util.Iterator; -import java.util.Queue; import java.util.Set; import org.checkerframework.checker.nullness.qual.Nullable; @@ -63,6 +60,11 @@ */ @Beta public abstract class Traverser { + private final SuccessorsFunction successorFunction; + + private Traverser(SuccessorsFunction successorFunction) { + this.successorFunction = checkNotNull(successorFunction); + } /** * Creates a new traverser for the given general {@code graph}. @@ -88,9 +90,13 @@ public abstract class Traverser { * * @param graph {@link SuccessorsFunction} representing a general graph that may have cycles. */ - public static Traverser forGraph(SuccessorsFunction graph) { - checkNotNull(graph); - return new GraphTraverser<>(graph); + public static Traverser forGraph(final SuccessorsFunction graph) { + return new Traverser(graph) { + @Override + Traversal newTraversal() { + return Traversal.inGraph(graph); + } + }; } /** @@ -166,15 +172,19 @@ public static Traverser forGraph(SuccessorsFunction graph) { * @param tree {@link SuccessorsFunction} representing a directed acyclic graph that has at most * one path between any two nodes */ - public static Traverser forTree(SuccessorsFunction tree) { - checkNotNull(tree); + public static Traverser forTree(final SuccessorsFunction tree) { if (tree instanceof BaseGraph) { checkArgument(((BaseGraph) tree).isDirected(), "Undirected graphs can never be trees."); } if (tree instanceof Network) { checkArgument(((Network) tree).isDirected(), "Undirected networks can never be trees."); } - return new TreeTraverser<>(tree); + return new Traverser(tree) { + @Override + Traversal newTraversal() { + return Traversal.inTree(tree); + } + }; } /** @@ -208,7 +218,9 @@ public static Traverser forTree(SuccessorsFunction tree) { * * @throws IllegalArgumentException if {@code startNode} is not an element of the graph */ - public abstract Iterable breadthFirst(N startNode); + public final Iterable breadthFirst(N startNode) { + return breadthFirst(ImmutableSet.of(startNode)); + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from any of the {@code @@ -220,7 +232,15 @@ public static Traverser forTree(SuccessorsFunction tree) { * @see #breadthFirst(Object) * @since 24.1 */ - public abstract Iterable breadthFirst(Iterable startNodes); + public final Iterable breadthFirst(Iterable startNodes) { + final ImmutableSet validated = validate(startNodes); + return new Iterable() { + @Override + public Iterator iterator() { + return newTraversal().breadthFirst(validated.iterator()); + } + }; + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from {@code startNode}, in @@ -253,7 +273,9 @@ public static Traverser forTree(SuccessorsFunction tree) { * * @throws IllegalArgumentException if {@code startNode} is not an element of the graph */ - public abstract Iterable depthFirstPreOrder(N startNode); + public final Iterable depthFirstPreOrder(N startNode) { + return depthFirstPreOrder(ImmutableSet.of(startNode)); + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from any of the {@code @@ -265,7 +287,15 @@ public static Traverser forTree(SuccessorsFunction tree) { * @see #depthFirstPreOrder(Object) * @since 24.1 */ - public abstract Iterable depthFirstPreOrder(Iterable startNodes); + public final Iterable depthFirstPreOrder(Iterable startNodes) { + final ImmutableSet validated = validate(startNodes); + return new Iterable() { + @Override + public Iterator iterator() { + return newTraversal().preOrder(validated.iterator()); + } + }; + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from {@code startNode}, in @@ -298,7 +328,9 @@ public static Traverser forTree(SuccessorsFunction tree) { * * @throws IllegalArgumentException if {@code startNode} is not an element of the graph */ - public abstract Iterable depthFirstPostOrder(N startNode); + public final Iterable depthFirstPostOrder(N startNode) { + return depthFirstPostOrder(ImmutableSet.of(startNode)); + } /** * Returns an unmodifiable {@code Iterable} over the nodes reachable from any of the {@code @@ -310,229 +342,25 @@ public static Traverser forTree(SuccessorsFunction tree) { * @see #depthFirstPostOrder(Object) * @since 24.1 */ - public abstract Iterable depthFirstPostOrder(Iterable startNodes); - - // Avoid subclasses outside of this class - private Traverser() {} - - private static final class GraphTraverser extends Traverser { - private final SuccessorsFunction graph; - - GraphTraverser(SuccessorsFunction graph) { - this.graph = checkNotNull(graph); - } - - @Override - public Iterable breadthFirst(final N startNode) { - checkNotNull(startNode); - return breadthFirst(ImmutableSet.of(startNode)); - } - - @Override - public Iterable breadthFirst(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInGraph(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return new BreadthFirstIterator(startNodes); - } - }; - } - - @Override - public Iterable depthFirstPreOrder(final N startNode) { - checkNotNull(startNode); - return depthFirstPreOrder(ImmutableSet.of(startNode)); - } - - @Override - public Iterable depthFirstPreOrder(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInGraph(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return Walker.inGraph(graph).preOrder(startNodes.iterator()); - } - }; - } - - @Override - public Iterable depthFirstPostOrder(final N startNode) { - checkNotNull(startNode); - return depthFirstPostOrder(ImmutableSet.of(startNode)); - } - - @Override - public Iterable depthFirstPostOrder(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInGraph(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return Walker.inGraph(graph).postOrder(startNodes.iterator()); - } - }; - } - - @SuppressWarnings("CheckReturnValue") - private void checkThatNodeIsInGraph(N startNode) { - // successors() throws an IllegalArgumentException for nodes that are not an element of the - // graph. - graph.successors(startNode); - } - - private final class BreadthFirstIterator extends UnmodifiableIterator { - private final Queue queue = new ArrayDeque<>(); - private final Set visited = new HashSet<>(); - - BreadthFirstIterator(Iterable roots) { - for (N root : roots) { - // add all roots to the queue, skipping duplicates - if (visited.add(root)) { - queue.add(root); - } - } - } - + public final Iterable depthFirstPostOrder(Iterable startNodes) { + final ImmutableSet validated = validate(startNodes); + return new Iterable() { @Override - public boolean hasNext() { - return !queue.isEmpty(); + public Iterator iterator() { + return newTraversal().postOrder(validated.iterator()); } - - @Override - public N next() { - N current = queue.remove(); - for (N neighbor : graph.successors(current)) { - if (visited.add(neighbor)) { - queue.add(neighbor); - } - } - return current; - } - } + }; } - private static final class TreeTraverser extends Traverser { - private final SuccessorsFunction tree; - - TreeTraverser(SuccessorsFunction tree) { - this.tree = checkNotNull(tree); - } - - @Override - public Iterable breadthFirst(final N startNode) { - checkNotNull(startNode); - return breadthFirst(ImmutableSet.of(startNode)); - } - - @Override - public Iterable breadthFirst(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInTree(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return new BreadthFirstIterator(startNodes); - } - }; - } - - @Override - public Iterable depthFirstPreOrder(final N startNode) { - checkNotNull(startNode); - return depthFirstPreOrder(ImmutableSet.of(startNode)); - } + abstract Traversal newTraversal(); - @Override - public Iterable depthFirstPreOrder(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N node : startNodes) { - checkThatNodeIsInTree(node); - } - return new Iterable() { - @Override - public Iterator iterator() { - return Walker.inTree(tree).preOrder(startNodes.iterator()); - } - }; - } - - @Override - public Iterable depthFirstPostOrder(final N startNode) { - checkNotNull(startNode); - return depthFirstPostOrder(ImmutableSet.of(startNode)); - } - - @Override - public Iterable depthFirstPostOrder(final Iterable startNodes) { - checkNotNull(startNodes); - if (Iterables.isEmpty(startNodes)) { - return ImmutableSet.of(); - } - for (N startNode : startNodes) { - checkThatNodeIsInTree(startNode); - } - return new Iterable() { - @Override - public Iterator iterator() { - return Walker.inTree(tree).postOrder(startNodes.iterator()); - } - }; - } - - @SuppressWarnings("CheckReturnValue") - private void checkThatNodeIsInTree(N startNode) { - // successors() throws an IllegalArgumentException for nodes that are not an element of the - // graph. - tree.successors(startNode); - } - - private final class BreadthFirstIterator extends UnmodifiableIterator { - private final Queue queue = new ArrayDeque<>(); - - BreadthFirstIterator(Iterable roots) { - for (N root : roots) { - queue.add(root); - } - } - - @Override - public boolean hasNext() { - return !queue.isEmpty(); - } - - @Override - public N next() { - N current = queue.remove(); - Iterables.addAll(queue, tree.successors(current)); - return current; - } + @SuppressWarnings("CheckReturnValue") + private ImmutableSet validate(Iterable startNodes) { + ImmutableSet copy = ImmutableSet.copyOf(startNodes); + for (N node : copy) { + successorFunction.successors(node); // Will throw if node doesn't exist } + return copy; } /** @@ -540,16 +368,16 @@ public N next() { * the next element from the next non-empty iterator; for graph, we need to loop through the next * non-empty iterator to find first unvisited node. */ - private abstract static class Walker { + private abstract static class Traversal { final SuccessorsFunction successorFunction; - Walker(SuccessorsFunction successorFunction) { - this.successorFunction = checkNotNull(successorFunction); + Traversal(SuccessorsFunction successorFunction) { + this.successorFunction = successorFunction; } - static Walker inGraph(SuccessorsFunction graph) { + static Traversal inGraph(SuccessorsFunction graph) { final Set visited = new HashSet<>(); - return new Walker(graph) { + return new Traversal(graph) { @Override N visitNext(Deque> horizon) { Iterator top = horizon.getFirst(); @@ -565,8 +393,8 @@ N visitNext(Deque> horizon) { }; } - static Walker inTree(SuccessorsFunction tree) { - return new Walker(tree) { + static Traversal inTree(SuccessorsFunction tree) { + return new Traversal(tree) { @Override N visitNext(Deque> horizon) { Iterator top = horizon.getFirst(); @@ -579,9 +407,23 @@ N visitNext(Deque> horizon) { }; } + final Iterator breadthFirst(Iterator startNodes) { + return topDown(startNodes, InsertionOrder.BACK); + } + final Iterator preOrder(Iterator startNodes) { + return topDown(startNodes, InsertionOrder.FRONT); + } + + /** + * In top-down traversal, an ancestor node is always traversed before any of its descendant + * nodes. The traversal order among descendant nodes (particularly aunts and nieces) are + * determined by the {@code InsertionOrder} parameter: nieces are placed at the FRONT before + * aunts for pre-order; while in BFS they are placed at the BACK after aunts. + */ + private Iterator topDown(Iterator startNodes, final InsertionOrder order) { final Deque> horizon = new ArrayDeque<>(); - horizon.addFirst(startNodes); + horizon.add(startNodes); return new AbstractIterator() { @Override protected N computeNext() { @@ -590,7 +432,9 @@ protected N computeNext() { if (next != null) { Iterator successors = successorFunction.successors(next).iterator(); if (successors.hasNext()) { - horizon.addFirst(successors); + // BFS: horizon.addLast(successors) + // Pre-order: horizon.addFirst(successors) + order.insertInto(horizon, successors); } return next; } @@ -601,9 +445,9 @@ protected N computeNext() { } final Iterator postOrder(Iterator startNodes) { - final Deque> horizon = new ArrayDeque<>(); - horizon.addFirst(startNodes); final Deque ancestorStack = new ArrayDeque<>(); + final Deque> horizon = new ArrayDeque<>(); + horizon.add(startNodes); return new AbstractIterator() { @Override protected N computeNext() { @@ -622,9 +466,7 @@ protected N computeNext() { /** * Visits the next node from the top iterator of {@code horizon} and returns the visited node. - * Null is returned to indicate reaching the end of the top iterator, which can be used by the - * traversal strategies to decide what to return in such case: in pre-order, continue to poll - * the next top iterator with {@code visitNext()}; in post-order, return the parent node. + * Null is returned to indicate reaching the end of the top iterator. * *

For example, if horizon is {@code [[a, b], [c, d], [e]]}, {@code visitNext()} will return * {@code [a, b, null, c, d, null, e, null]} sequentially, encoding the topological structure. @@ -635,4 +477,22 @@ protected N computeNext() { @Nullable abstract N visitNext(Deque> horizon); } + + /** Poor man's method reference for {@code Deque::addFirst} and {@code Deque::addLast}. */ + private enum InsertionOrder { + FRONT { + @Override + void insertInto(Deque deque, T value) { + deque.addFirst(value); + } + }, + BACK { + @Override + void insertInto(Deque deque, T value) { + deque.addLast(value); + } + }; + + abstract void insertInto(Deque deque, T value); + } }