【问题标题】:How can I recursively iterate over a tree that is changing during the traversal?如何递归迭代在遍历过程中发生变化的树?
【发布时间】:2015-11-06 21:17:15
【问题描述】:

我正在尝试遍历 DOM 树,使用 HTML 解析器 AngleSharp 替换和删除节点。这个问题不是这个库独有的,而是一个关于如何递归地改变树并确保我仍在遍历整个树的一般问题。

获取此列表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 更多的节点,因此并非所有项目都会被递归。同样,删除一个节点意味着在列表中向上移动的节点将被跳过。

如何递归由于我的递归处理而可能发生变化的树? 是否一遍又一遍地重复我的列表,直到我确定唯一的方法没有进行任何更改,或者我是否错误地解决了问题?

【问题讨论】:

  • 您是否需要考虑自己对树所做的更改,或者您可以创建一个更改列表以添加并在完成后添加它们?
  • 在对树执行任何操作之前,我可能会在遍历函数中添加一个检查来计算树中节点的校验和。例如,开始计算节点的校验和(计算每个深度的节点并乘以深度),然后在每次调用时,如果校验和已更改,则中断并重新开始遍历。您将重复此过程,直到执行完整的遍历而不更改校验和。
  • @mjw,这就是我所害怕的,整体上重复执行递归似乎有点笨拙,但我认为这是有道理的。
  • @Hammerstein,因为对象实际上只是指向底层数据流的指针(有点像StreamReader),因此构建副本或保留列表不会真正起作用,至少不像我能说的最好。
  • 我刚刚再次阅读了您的问题,现在看到您将递归着眼于在遍历期间对树进行 OWN 修改。在这种情况下,您可能会考虑捕获树的预遍历状态以及校验和......然后收集您想要在遍历期间按照发现顺序进行的任何更改,然后在您验证后对树进行后处理校验和匹配(预处理时没有其他进程修改树)。

标签: c# loops recursion anglesharp


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    现在有比 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&lt;Node&gt;?好吧,我们可以在每个调用中创建一个列表,然后将其与调用者的列表合并,但是,上面的版本更有效一些。我们还使用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
    

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

    【讨论】:

      猜你喜欢
      • 2019-12-01
      • 2020-04-25
      • 2014-08-21
      • 2012-03-21
      • 2011-11-24
      • 2020-01-24
      • 2016-12-15
      • 2015-10-22
      • 2018-07-12
      相关资源
      最近更新 更多