package org.codefx.libfx.collection.tree.stream; import java.util.Objects; import java.util.Optional; import org.codefx.libfx.collection.tree.navigate.TreeNavigator; /** * A {@link TreeIterationStrategy} which uses <a href="https://en.wikipedia.org/wiki/Depth-first_search">depth-first * search</a> to iterate a tree's nodes. * <p> * This implementation is only guaranteed to work on trees, i.e. a connected, directed, acyclic graph. Using it on other * graphs can lead to unexpected behavior including infinite loops. * * @param <E> * the type of elements contained in the tree */ final class DfsTreeIterationStrategy<E> implements TreeIterationStrategy<E> { // #begin FIELDS private final TreeNavigator<E> navigator; /** * The path from the root to the last node returned by {@link #goToNextNode()} * <p> * The path will also contain at least one node before the first call to {@link #goToNextNode()}. These are the * nodes specified during construction and the one at the end of the path must be returned by the first call to * {@code goToNextNode()}. Otherwise the end of the path (e.g. the root specified during construction) would never * be returned by the strategy. Whether {@code goToNextNode()} must return this node or find a new one is indicated * by {@link #beforeFirst}. * <p> * If the path is empty, no more nodes will be returned. */ private final TreePath<TreeNode<E>> path; /** * Indicates whether {@link #goToNextNode()} was not already called at least once. */ private boolean beforeFirst; // #end FIELDS // #begin CONSTRUCTION /** * Creates a new depth-first search strategy starting with the specified initial path. * <p> * The iteration will begin with the node at the end of the initial path. Note that this does not make the node the * root of the (sub-)tree over which this strategy iterates. Instead it will at some point try to find right * siblings or uncles of this node. It will only stop when backtracking to the first node in that path, i.e. the * subtree's root. * <p> * The specified path must correspond to the navigator's view on the tree, i.e. each element in the path must be the * parent (in the tree) of the next one. * <p> * See {@link TreePathFactory} for easy ways to create an initial path. * * @param navigator * the navigator used to navigate the tree * @param initialPath * the initial path from the root of the (sub-)tree iterated by this strategy; must contain at least one * element; the last element in the path will be returned by the first call to {@link #goToNextNode()} */ public DfsTreeIterationStrategy(TreeNavigator<E> navigator, TreePath<TreeNode<E>> initialPath) { Objects.requireNonNull(navigator, "The argument 'navigator' must not be null."); Objects.requireNonNull(initialPath, "The argument 'initialPath' must not be null."); if (initialPath.isEmpty()) throw new IllegalArgumentException("The 'initialPath' must not be empty."); this.navigator = navigator; this.path = initialPath; this.beforeFirst = true; } // #end CONSTRUCTION // #begin GO TO NEXT NODE @Override public Optional<E> goToNextNode() { // if the path is empty, iteration ended and no more nodes will be returned if (path.isEmpty()) return Optional.empty(); // if this is the first call, do not move to the next node if (beforeFirst) beforeFirst = false; else movePathEndToNextNode(); return path.getEnd().map(TreeNode::getElement); } private void movePathEndToNextNode() { Optional<TreeNode<E>> leftmostChild = getLeftmostChild(); if (leftmostChild.isPresent()) goToLeftmostChild(leftmostChild.get()); else goToRightSiblingOrUncle(); } private Optional<TreeNode<E>> getLeftmostChild() { return path .getEnd() .flatMap(node -> navigator.getChild(node.getElement(), 0)) .map(child -> SimpleTreeNode.innerNode(child, 0)); } private void goToLeftmostChild(TreeNode<E> leftmostChild) { path.append(leftmostChild); } private void goToRightSiblingOrUncle() { Optional<TreeNode<E>> siblingOrUncle = Optional.empty(); while (!siblingOrUncle.isPresent() && !path.isEmpty()) { TreeNode<E> currentNode = path.removeEnd(); Optional<TreeNode<E>> parentNode = path.getEnd(); if (parentNode.isPresent()) { E parentElement = parentNode.get().getElement(); int rightSiblingIndex = currentNode.getChildIndex().getAsInt() + 1; siblingOrUncle = navigator .getChild(parentElement, rightSiblingIndex) .map(sibling -> SimpleTreeNode.innerNode(sibling, rightSiblingIndex)); } } siblingOrUncle.ifPresent(path::append); } // #end GO TO NEXT NODE }