如何递归迭代在遍历期间发生变化的树?

时间:2015-08-13 18:45:41

标签: c# loops recursion anglesharp

我尝试遍历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

这个问题是递归是错误的。现在有比for循环中的Length更多的节点,因此并非所有项都会被递归。同样,删除节点意味着跳过列表中向上移动的节点。

如何递归因递归处理而可能发生变化的树? 我一遍又一遍地递归我的列表,直到我确定没有做出任何改变,或者我是否错误地接近了这个问题?

2 个答案:

答案 0 :(得分:1)

安全方式:使用递归函数创建一个全新的树而不是更改旧树,然后用新树替换旧树。

安全性较低:让LoopRecursively函数返回一个表示添加或删除的节点数的整数,然后用这个新数字更新循环变量。 (更新循环索引和循环条件中的变量)

答案 1 :(得分:1)

  

现在有比for循环中的Length更多的节点,因此并不是所有的项都会被递归。

我不认为这是真的。您没有评估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

希望这个答案对你有所帮助。