如何收集递归方法的结果

时间:2009-07-17 09:27:41

标签: java recursion

我遍历树结构以收集叶节点的路径。您希望以哪种方式收集操作结果:

a)合并子项的结果并返回此

private Collection<String> extractPaths(final Element element, final IPath parentPath) {
    final IPath path = parentPath.append(element.getLabel());
    final Collection<Element> children = getElementChildren(element);
    if (children.isEmpty())
        return Collections.singletonList(path.toString());

    final Set<String> result = new TreeSet<String>();
    for (final Element child : children)
        result.addAll(extractPaths(child, path));
    return result;
}

b)提供结果集合作为参数,并在每个递归步骤中添加新元素

private void extractPaths(final Element element, final IPath parentPath, final Set<String> result) {
    final IPath path = parentPath.append(element.getLabel());
    final Collection<Element> children = getElementChildren(element);
    if (children.isEmpty())
        result.add(path.toString());

    for (final Element child : children)
       extractPaths(child, path, result);
}

9 个答案:

答案 0 :(得分:6)

两者都可以毫无问题地使用。虽然,以前的解决方案更干净,因为它不会改变输入参数。函数式编程的本质没有任何副作用。

答案 1 :(得分:6)

我认为后者是打电话给extractPaths(child, path, result)

后一种形式将更有效,因为它不需要在每个递归级别复制项目。正如Boris所说,它在功能上不那么干净 - 但是Java并没有真正为不可变集合提供适当的方法来有效地创建基于它们的新集合。

为了使调用愉快,您可以提供第一个选项样式的包装器,它只创建一个新集并调用第二个选项。这可能就是我要做的事情:

private Collection<String> extractPaths(Element element, IPath parentPath) {
    Set<String> ret = new HashSet<String>();
    extractPaths(element, parentPath, ret);
    return ret;
}

另一种方法是将第三个参数从Set<String>更改为某种“收集器”界面:您告诉它您已找到结果,而没有指定如何处理它。实际上,收集器可以返回一个 new 收集器,从那时开始使用 - 将其留给实现来决定是否制作功能上干净的“创建新集”版本,或隐藏副作用在收集器中,它将再次返回以供重用。

答案 2 :(得分:3)

为客户提供最方便灵活的界面,请将其编写为实现Iterator<E>的类。

这意味着客户端可以循环遍历递归期间找到的项目,但是他们不必将“for each”代码实现为回调(Java没有很好的方法可以做到这一点),并且他们甚至可以“暂停”操作并在以后继续操作,超出他们开始操作的范围(或在任何时候放弃它)。

尽管这是最棘手的。如果您正在遍历的数据结构是一个树状结构,每个节点中都有父指针,那么您不需要除当前节点之外的其他数据。要进入下一个节点,请寻找第一个孩子。如果有,那就是下一个节点。否则尝试下一个兄弟。如果没有,请获取父级并尝试获取its下一个兄弟,依此类推,直到您点击null,在这种情况下不再有项目。

作为一个快速而肮脏的例子,这里是一个类似treenode的类,打破了所有关于封装的规则,以节省一些空间:

class SimpleNode
{
    String name;
    public SimpleNode parent, firstChild, nextSibling;

    public SimpleNode(String n) { name = n; }

    public void add(SimpleNode c)
    {
        c.parent = this;
        c.nextSibling = firstChild;
        firstChild = c;
    }

    public String getIndent()
    {
        StringBuffer i = new StringBuffer();

        for (SimpleNode n = this; n != null; n = n.parent)
            i.append("  ");

        return i.toString();
    }
}

现在让我们从中创建一个树:

SimpleNode root = new SimpleNode("root");

SimpleNode fruit = new SimpleNode("fruit");
root.add(fruit);
fruit.add(new SimpleNode("pear"));
fruit.add(new SimpleNode("banana"));
fruit.add(new SimpleNode("apple"));

SimpleNode companies = new SimpleNode("companies");
root.add(companies);
companies.add(new SimpleNode("apple"));
companies.add(new SimpleNode("sun"));
companies.add(new SimpleNode("microsoft"));

SimpleNode colours = new SimpleNode("colours");
root.add(colours);
colours.add(new SimpleNode("orange"));
colours.add(new SimpleNode("red"));
colours.add(new SimpleNode("blue"));

现在,为了这个想法的新手拼出这个,我们希望能够做到的是:

for (final SimpleNode n : new SimpleNodeIterator(root))
    System.out.println(n.getIndent() + "- " + n.name);

得到这个(我已经使上面的代码在SO中生成了类似于分层子弹列表的内容):

    • 颜色
      • 蓝色
      • 红色
    • 公司
      • 微软
      • 太阳
      • 苹果
      • 苹果
      • 香蕉

为此,我们必须将一些标准操作映射到我们的SimpleNode类:

class SimpleNodeIterator extends TreeIterator<SimpleNode>
{
    public SimpleNodeIterator(SimpleNode root)
        { super(root); }

    protected SimpleNode getFirstChild(SimpleNode of)
        { return of.firstChild; }

    protected SimpleNode getNextSibling(SimpleNode of)
        { return of.nextSibling; }

    protected SimpleNode getParent(SimpleNode of)
        { return of.parent; }
}

最后,在我们设计的最底层,TreeIterator<TNode>是一个非常可重用的抽象基类,完成其余的工作,现在我们已经告诉它如何导航我们的节点类:

abstract class TreeIterator<TNode> implements Iterator<TNode>,
                                              Iterable<TNode>
{
    private TNode _next;

    protected TreeIterator(TNode root)
        { _next = root; }

    public Iterator<TNode> iterator()
        { return this; }

    public void remove()
        { throw new UnsupportedOperationException(); }

    public boolean hasNext()
        { return (_next != null); }

    public TNode next()
    {
        if (_next == null)
            throw new NoSuchElementException();

        TNode current = _next;

        _next = getFirstChild(current);

        for (TNode ancestor = current;
             (ancestor != null) && (_next == null);
             ancestor = getParent(ancestor))
        {
            _next = getNextSibling(ancestor);
        }

        return current;
    }

    protected abstract TNode getFirstChild(TNode of);
    protected abstract TNode getNextSibling(TNode of);
    protected abstract TNode getParent(TNode of);
}

(它有点顽皮,因为它在同一个对象上实现Iterator<E>Iterable<E>。这只是意味着你必须新建一个新对象才能再次迭代;不要尝试重用相同的对象)。

这意味着如果您的层次结构由可以定义这三个简单导航操作的节点组成,那么您所要做的就是派生自己的等效SimpleNodeIterator。这使得在任何树实现上启用此功能变得非常容易。

如果您正在迭代的内容无法获取父级,则需要在迭代期间保留堆栈。每次下降一级时,都会将当前级别的状态推送到堆栈。当您完成当前级别的迭代时,您将从堆栈中弹出最后一个状态并继续它。当堆栈为空时,您就完成了。这意味着你有一些中间存储,但它的最大大小与递归的深度成比例而不是项目的数量,所以假设数据大致平衡,那么它应该比将所有项目复制到更高的存储效率。返回之前的清单。

答案 3 :(得分:1)

我通常更愿意返回结果,因为我认为

$result = extractPaths($arg,$arg2);

更清晰
extractPaths($arg,$arg2,$result);

但它完全基于品味。

答案 4 :(得分:1)

我会选择选项b,因为它会创建更少的对象,从而提高效率。解决方案a更像是在函数式语言中使用它的方式,但它依赖于Java中没有的假设。

答案 5 :(得分:1)

如果你传入要构建的对象,如果你有一个异常,你在一个你引用该对象的地方捕获,那么你至少会得到你建立的数据,直到抛出异常。

当多个方法将“构建”它时,我亲自传递Builders作为参数,包括递归。这样,您只构建了一个对象,并且错过了大量的Set,Map或List复制。

答案 6 :(得分:1)

在这种特殊情况下,我更喜欢后一种解决方案,因为:

  1. 它避免了创建一次性收藏
  2. 以这种方式实现的算法无法从“功能”中获得任何收益
  3. imho 如果没有一个非常好的理由,就没有真正的功能 f(例如使用线程)。

答案 7 :(得分:1)

将集合作为此方法的参数传递

答案 8 :(得分:1)

我在重构之后找到的最终解决方案是实现变体b)但是传递一个Visitor而不是结果集合:

private void traverse(final Element element, final Visitor... visitors) {
   for (final Visitor visitor : visitors)
       // push e.g. the parent path to the stack
       visitor.push(visitor.visit(element)); 

   for (final Element child: getElementChildren(element))
       traverse(child, visitors);

   for (final Visitor visitor : visitors)
       visitor.pop();
}

访问者还提供一个堆栈来携带有关父路径的信息。这个解决方案允许我将遍历逻辑与集合逻辑分开,而不需要更复杂的TreeIterator实现。

private class CollectPathsVisitor extends ElementVisitor {
    public final Set<String> paths = new TreeSet<String>();
    public Object visit(Element element) {
        final IPath parentPath = (IPath) peek();
        final IPath path = parentPath.append(element.getLabel());
        if (!hasChildren(element))
            paths.add(path);
        return path;
    }
}