Как я могу рекурсивно перебирать дерево, которое меняется во время обхода?

Я пытаюсь пройти по дереву DOM, заменяя и удаляя узлы, используя AngleSharp анализатор HTML. Эта проблема не уникальна для этой библиотеки, это скорее общий вопрос о том, как рекурсивно изменять дерево и гарантировать, что я все еще просматриваю все дерево.

Возьмите этот список myCollection, где каждая запись является объектом узла, потенциально с дочерними элементами. Это также живая коллекция:

-A
-B
-C
 --D
 --E
 --F
-G

Я начинаю повторять рекурсивную функцию:

private void LoopRecursively(Node element) {
   //either do nothing, remove, or replace with children
   //e.g. element.Replace(element.ChildNodes);
   for (var x = 0; x < element.ChildNodes.Length; x++) {
      LoopRecursively(element.ChildNodes[x]);

   }
}

Допустим, мы решили заменить узел C его дочерними элементами, поэтому список становится следующим:

-A
-B
-D
-E
-F
-G

Проблема в том, что рекурсия будет неправильной. Теперь количество узлов больше, чем Length в учтенном цикле for, поэтому не все элементы будут повторяться. Точно так же удаление узла будет означать, что узел, который переместился вверх в списке, будет пропущен.

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


person Grant H.    schedule 13.08.2015    source источник
comment
Вам нужно учитывать свои собственные изменения в дереве, или вы можете создать список изменений, чтобы добавить и добавить их, когда вы закончите?   -  person tbddeveloper    schedule 13.08.2015
comment
Я бы, вероятно, добавил в свою функцию обхода проверку, чтобы вычислить контрольную сумму узлов в дереве, прежде чем выполнять какие-либо операции с деревом. Например, чтобы начать вычисление контрольной суммы узлов (подсчитать узлы на каждой глубине и умножить на глубину), затем при каждом вызове, если контрольная сумма изменилась, прервите и перезапустите обход. Вы будете повторять этот процесс до тех пор, пока не будет выполнен полный обход без изменения контрольной суммы.   -  person mjw    schedule 13.08.2015
comment
@mjw, вот чего я боялся, кажется неуклюжим просто повторять рекурсию в целом несколько раз, но я полагаю, это имеет смысл.   -  person Grant H.    schedule 13.08.2015
comment
@Hammerstein, поскольку объекты на самом деле являются просто указателями на базовый поток данных (что-то вроде StreamReader), создание копии или ведение списка на самом деле не сработает, по крайней мере, не так хорошо, как я могу сказать.   -  person Grant H.    schedule 13.08.2015
comment
Я просто прочитал ваш вопрос еще раз и теперь вижу, что вы будете рекурсивно с прицелом на внесение ваших СОБСТВЕННЫХ изменений в дерево во время обхода. В этом случае вы можете рассмотреть возможность фиксации состояния дерева до обхода вместе с контрольной суммой ... затем собрать любые изменения, которые вы хотите внести, в порядке обнаружения во время обхода, а затем обработать дерево после проверки. контрольная сумма совпадает (никакие другие процессы не изменили дерево, пока вы выполняли предварительную обработку).   -  person mjw    schedule 13.08.2015


Ответы (2)


Безопасный способ: используйте рекурсивную функцию для создания нового дерева вместо изменения старого, а затем замените старое на новое.

Менее безопасный способ: пусть ваша функция LoopRecursively возвращает целое число, представляющее количество добавленных или удаленных узлов, а затем обновите переменные цикла этим новым числом. (обновить как индекс цикла, так и переменную в условном цикле)

person Dave C    schedule 13.08.2015

Теперь количество узлов больше, чем учитывается длина в цикле for, поэтому не все элементы будут повторяться.

Я не думаю, что это правда. Вы оцениваете element.ChildNodes.Length не один раз, а на каждой итерации. Следовательно, если список активен, его длина будет меняться вместе с вашими изменениями.

Предположим следующую простую реализацию для вашего дерева:

class Node
{
    readonly List<Node> children;
    readonly String name;

    public Node(String name)
    {
        this.children = new List<Node>();
        this.name = name;
    }

    public Node AddChild(Node node)
    {
        children.Add(node);
        return this;
    }

    public Node InsertChild(int index, Node node)
    {
        children.Insert(index, node);
        return this;
    }

    public Int32 Length
    {
        get { return children.Count; }
    }

    public Node this[Int32 index]
    {
        get { return children[index]; }
    }

    public Int32 IndexOf(Node node)
    {
        return children.IndexOf(node);
    }

    public Node RemoveChild(Node node)
    {
        children.Remove(node);
        return this;
    }

    public IEnumerable<Node> Children
    {
        get { return children.AsEnumerable(); }
    }

    public override String ToString()
    {
        var content = new String[1 + children.Count];
        content[0] = name;

        for (int i = 0; i < children.Count; )
        {
            var childs = children[i].ToString().Split(new [] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
            content[++i] = "+ " + String.Join(Environment.NewLine + "  ", childs);
        }

        return String.Join(Environment.NewLine, content);
    }
}

Данный Node содержит дочерние элементы (но не родительские) и простые методы для добавления, удаления, вставки, ..., дочерних элементов.

Давайте посмотрим, как мы могли бы построить хороший пример с таким Node:

var root = new Node("Root");
root.AddChild(new Node("a")).
     AddChild(new Node("b")).
     AddChild(new Node("c").
        AddChild(new Node("d").
            AddChild(new Node("e")).
            AddChild(new Node("f"))).
        AddChild(new Node("g")).
        AddChild(new Node("h"))).
    AddChild(new Node("i"));

Результат вызова root.ToString() будет выглядеть следующим образом.

Root
+ a
+ b
+ c
  + d
    + e
    + f
  + g
  + h
+ i

Полагаю, вы хотите сплющить дерево? Как уже было сказано, сделать это неизменным способом может быть хорошей идеей. Есть несколько способов сделать это, но с учетом API, описанного выше, мы можем прийти к следующему решению:

void Flatten(Node element, List<Node> nodes)
{
    var before = nodes.Count;

    foreach (var node in element.Children)
    {
        Flatten(node, nodes);
    }

    if (nodes.Count == before)
    {
        nodes.Add(element); 
    }
}

Почему я сдаю List<Node>? Что ж, мы могли бы создать список при каждом вызове, который затем будет объединен со списком вызывающего абонента, однако версия выше немного более эффективна. Также мы используем свойство Count, чтобы определить, были ли замечены какие-либо дочерние элементы. Мы также могли бы использовать метод расширения Any(), но это снова ненужные накладные расходы. Мы в значительной степени просто проверяем, является ли данный узел листом. если да, то мы добавляем его в предоставленный список.

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

void Flatten(Node element, Node parent = null)
{
    for (var i = 0; i < element.Length; i++)
    {
        Flatten(element[i], element);
    }

    if (parent != null && element.Length > 0)
    {
        var children = element.Children.ToArray();
        var index = parent.IndexOf(element);
        parent.RemoveChild(element);

        foreach (var child in children)
        {
            element.RemoveChild(child);
            parent.InsertChild(index++, child);
        }
    }
}

Первая итерация не изменит значение element.Length. Поэтому мы тоже могли спокойно его оценить один раз и все. Однако потенциальная вторая итерация сделает это. Вот почему мы сначала получаем копию element.Children.ToArray(). Есть также другой способ без этой копии, который включает в себя обратный цикл for (переход от Length к -1).

Посмотрим, как будет выглядеть сериализация дерева после вызова Flatten(root).

Root
+ a
+ b
+ e
+ f
+ g
+ h
+ i

Надеюсь, этот ответ вам немного поможет.

person Florian Rappl    schedule 17.09.2015