Как собрать результат рекурсивного метода - PullRequest
4 голосов
/ 17 июля 2009

Я перебираю древовидную структуру, чтобы собрать пути листовых узлов. Каким способом вы предпочитаете собирать результат операции:

а) объединить результаты детей и вернуть это

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);
}

Ответы [ 10 ]

6 голосов
/ 17 июля 2009

Я полагаю, что последний предназначен для вызова extractPaths(child, path, result)?

Последняя форма будет более эффективной, поскольку ей не нужно копировать элементы на каждом уровне рекурсии. Как говорит Борис, он менее функционально чист, но в действительности Java не предоставляет неизменяемых коллекций с соответствующими методами для эффективного создания новых коллекций на их основе.

Чтобы сделать вызов приятным, вы можете предоставить оболочку в стиле первой опции, которая просто создает новый набор и вызывает вторую опцию. Это, вероятно, то, что я бы сделал:

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

Другой альтернативой является изменение третьего параметра с Set<String> на какой-то интерфейс «коллектора»: вы сообщаете ему, что нашли результат, не указывая, что с ним делать. Действительно, сборщик может возвращать новый сборщик для использования с этого момента - оставляя за собой право решать, делать ли функционально-чистую версию "создать новый набор" или скрывать побочные эффекты коллектор, который просто вернул бы себя снова для повторного использования.

6 голосов
/ 17 июля 2009

Оба могут быть использованы без каких-либо проблем. Впрочем, прежнее решение более чисто, поскольку не меняет входные параметры. Никаких побочных эффектов не характерно для функционального программирования.

3 голосов
/ 17 июля 2009

Чтобы предоставить своим клиентам наиболее удобный и гибкий интерфейс, запишите его как класс, реализующий Iterator<E>.

Это означает, что клиент может перебирать элементы, найденные во время рекурсии, но ему не нужно реализовывать свой код «для каждого» в качестве обратного вызова (у Java нет красивого способа сделать это), и они могут даже «приостановить» операцию и продолжить ее позже, вне области, в которой они ее начали (или отказаться от нее в любой момент).

Это самый сложный для реализации, хотя. Если структура данных, которую вы просматриваете, представляет собой древовидную структуру с родительскими указателями в каждом узле, то вам не нужны никакие другие данные, кроме текущего узла. Чтобы перейти к следующему узлу, ищите первого ребенка. Если есть один, это следующий узел. В противном случае попробуйте следующий брат. Если его нет, найдите родителя и попытайтесь получить its следующего родного брата и так далее, пока не достигнете нуля, в этом случае больше нет элементов.

В качестве быстрого и грязного примера приведем класс, подобный 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):

  • корень
    • цвет
      • синий
      • красный
      • оранжевый
    • компании
      • Microsoft
      • ВС
      • яблоко
    • фрукты
      • яблоко
      • банан
      • груша

Для этого нам нужно отобразить некоторые стандартные операции в наш 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. Это позволяет очень легко включить эту возможность в любой древовидной реализации.

Если то, что вы повторяете, не может найти родителя, вам нужно сохранить стек во время итерации. Каждый раз, когда вы опускаете уровень, вы помещаете состояние текущего уровня в стек. Когда вы заканчиваете итерацию на текущем уровне, вы вытаскиваете последнее состояние из стека и продолжаете его. Когда стек пуст, все готово. Это означает, что у вас есть некоторое промежуточное хранилище, но его максимальный размер пропорционален глубине рекурсии, а не количеству элементов, поэтому, если данные примерно сбалансированы, тогда они должны быть намного более эффективными для хранения, чем копирование всех элементов в список, прежде чем вернуть его.

1 голос
/ 21 июля 2009

Последнее решение, которое я нашел после некоторого рефакторинга, заключается в реализации варианта b), но для передачи посетителя вместо набора результатов:

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;
    }
}
1 голос
/ 17 июля 2009

передать коллекцию в качестве параметра для этого метода

1 голос
/ 17 июля 2009

в данном конкретном случае я предпочитаю последнее решение, поскольку:

  1. избегает создания одноразовых коллекций
  2. ваш алгоритм, реализованный таким образом, не может получить никакой выгоды от "функциональности"

imho нет никакой реальной выгоды от функциональности без действительно веской причины f (например, использование потоков).

1 голос
/ 17 июля 2009

Если вы передадите объект, который будет построен, если у вас было исключение, которое вы поймали в месте, где у вас была ссылка на этот объект, то вы, по крайней мере, будете иметь данные, которые вы создали до тех пор, пока исключение не будет выдано.

Я лично передаю в Builders аргументы, когда на нем будут «строить» несколько методов, включая рекурсию. Таким образом, у вас есть только один строящийся объект, и вы пропускаете множество копий Set, Map или List.

1 голос
/ 17 июля 2009

Я бы выбрал вариант b, поскольку он будет создавать меньше объектов и, следовательно, будет более эффективным. Решение a больше похоже на то, как вы бы это делали на функциональном языке, но оно основано на предположениях, которые не выполняются в Java.

1 голос
/ 17 июля 2009

Я обычно предпочитаю возвращать результат, так как думаю

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

яснее, чем

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

но полностью зависит от вкуса.

0 голосов
/ 17 июля 2009

Позже будет создавать меньше объектов в памяти (как уже было сказано), но также будет управлять каждым путем к дереву только один раз: при извлечении и сохранении в результате Set он не добавляется в любой другой набор снова и снова и снова. *

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...