【问题标题】:How to "unroll" a "recursive" structure如何“展开”“递归”结构
【发布时间】:2011-01-02 00:10:05
【问题描述】:

不知道怎么称呼它,但是说你有一个看起来像这样的类:

class Person
{
    public string Name;
    public IEnumerable<Person> Friends;
}

然后你有一个人,你想递归地“展开”这个结构,所以你最终得到一个没有重复的人的单一列表。

你会怎么做?我已经做了一些似乎可以工作的东西,但我很想看看其他人会怎么做,特别是如果 Linq 有内置的东西,你可以用一种聪明的方式来解决这个小问题:)


这是我的解决方案:

public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects, Func<T, IEnumerable<T>> selector)
{
    // Stop if subjects are null or empty
    if(subjects == null)
        yield break;

    // For each subject
    foreach(var subject in subjects)
    {
        // Yield it
        yield return subject;

        // Then yield all its decendants
        foreach (var decendant in SelectRecursive(selector(subject), selector))
            yield return decendant;
    }
}

会像这样使用:

var people = somePerson.SelectRecursive(x => x.Friends);

【问题讨论】:

  • 我遗漏了一些东西......如果你在那里有循环,它会停止吗?
  • @Kobi:这是由if(!subjects.Any()) yield break;完成的
  • @Oliver:不,不会。只有在主题列表为空时才会停止。所以我想我实际上可以完全跳过那部分,因为它不会有任何区别......
  • @Kobi:不,你没有遗漏任何东西。它永远不会停止 :p 当我制作它时我正在使用的东西永远不会有任何循环,所以没有费心做任何事情。如果需要,我可能会使用 HashSet 来跟踪我已经访问过的主题。
  • 删除了 !subjects.Any() 部分,因为它并没有真正起到任何作用,只是让人困惑:p

标签: c# recursion ienumerable


【解决方案1】:

虽然在可能存在大量数据时使用 IEnumerable 非常棒,但值得记住递归添加到列表的经典方法。

可以这么简单(我省略了选择器;只是演示递归添加到输出列表):

class Node
{
    public readonly List<Node> Children = new List<Node>();

    public List<Node> Flatten()
    {
        var all = new List<Node>();
        Flatten(ref all);
        return all;
    }

    public void Flatten(ref List<Node> all)
    {
        all.Add(this);

        foreach (var child in Children)
            child.Flatten(ref all);
    }
}

用法:

Node rootNode = ...;
...
var all = rootNode.Flatten();

【讨论】:

    【解决方案2】:

    我在寻找和考虑类似的解决方案时发现了这个问题——在我的例子中,我为 ASP.NET UI 控件创建了一个高效的IEnumerable&lt;Control&gt;。我拥有的递归yield 很快,但我知道这可能会产生额外的成本,因为控制结构越深,所需的时间就越长。现在我知道这是 O(n log n)。

    此处给出的解决方案提供了一些答案,但正如 cmets 中所讨论的,它确实改变了顺序(OP 并不关心)。我意识到,要保留 OP 给出的顺序以及我需要的顺序,简单的 Queue (如 Jon 使用的)和 Stack 都不会起作用,因为所有父对象都将首先产生,然后是它们之后的任何子对象(反之亦然)。

    为了解决这个问题并保持顺序,我意识到解决方案只需将Enumerator 本身放在Stack 上。要使用 OPs 原始问题,它看起来像这样:

    public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects, Func<T, IEnumerable<T>> selector)
    {
        if (subjects == null)
            yield break;
    
        var stack = new Stack<IEnumerator<T>>();
    
        stack.Push(subjects.GetEnumerator());
    
        while (stack.Count > 0)
        {
            var en = stack.Peek();
            if (en.MoveNext())
            {
                var subject = en.Current;
                yield return subject;
    
                stack.Push(selector(subject).GetEnumerator());
            }
            else 
            {
                stack.Pop().Dispose();
            }
        }
    }
    

    我在这里使用stack.Peek 来避免将同一个枚举器推回堆栈,因为这可能是更频繁的操作,期望该枚举器提供多个项目。

    这将创建与递归版本相同数量的枚举器,但与将所有主题放在队列或堆栈中并继续添加任何后代主题相比,新对象可能会更少。这是 O(n) 时间,因为每个枚举器都独立存在(在递归版本中,隐式调用一个 MoveNext 在子枚举器上执行 MoveNext 到递归堆栈中的当前深度)。

    【讨论】:

    • 你应该在从堆栈中弹出枚举数后将其丢弃。
    • 注意:“将枚举器本身放在堆栈上” - 这可行,但这样做的代价是创建大量枚举器,每个递归节点一个。将此与 Jon 的解决方案进行对比,该解决方案不会创建相同的编号顺序,但会避免 GetEnumerator 对所有后代的调用。一种优化是让节点(主题)类实现ICollection 所以可以做到if (node.Count &gt; 0) stack.Push(selector(node).GetEnumerator()); 这避免了在所有“叶”节点上创建枚举器。
    【解决方案3】:

    这是一个实现:

    • 深度优先递归选择,
    • 不需要子集合的双重迭代,
    • 不对选定元素使用中间集合,
    • 不处理循环,
    • 可以倒着做。

      public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> rootItems, Func<T, IEnumerable<T>> selector)
      {
          return new RecursiveEnumerable<T>(rootItems, selector, false);
      }
      
      public static IEnumerable<T> SelectRecursiveReverse<T>(this IEnumerable<T> rootItems, Func<T, IEnumerable<T>> selector)
      {
          return new RecursiveEnumerable<T>(rootItems, selector, true);
      }
      
      class RecursiveEnumerable<T> : IEnumerable<T>
      {
          public RecursiveEnumerable(IEnumerable<T> rootItems, Func<T, IEnumerable<T>> selector, bool reverse)
          {
              _rootItems = rootItems;
              _selector = selector;
              _reverse = reverse;
          }
      
          IEnumerable<T> _rootItems;
          Func<T, IEnumerable<T>> _selector;
          bool _reverse;
      
          public IEnumerator<T> GetEnumerator()
          {
              return new Enumerator(this);
          }
      
          System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
          {
              return GetEnumerator();
          }
      
          class Enumerator : IEnumerator<T>
          {
              public Enumerator(RecursiveEnumerable<T> owner)
              {
                  _owner = owner;
                  Reset();
              }
      
              RecursiveEnumerable<T> _owner;
              T _current;
              Stack<IEnumerator<T>> _stack = new Stack<IEnumerator<T>>();
      
      
              public T Current
              {
                  get 
                  {
                      if (_stack == null || _stack.Count == 0)
                          throw new InvalidOperationException();
                      return _current; 
                  }
              }
      
              public void Dispose()
              {
                  _current = default(T);
                  if (_stack != null)
                  {
                      while (_stack.Count > 0)
                      {
                          _stack.Pop().Dispose();
                      }
                      _stack = null;
                  }
              }
      
              object System.Collections.IEnumerator.Current
              {
                  get { return Current; }
              }
      
              public bool MoveNext()
              {
                  if (_owner._reverse)
                      return MoveReverse();
                  else
                      return MoveForward();
              }
      
              public bool MoveForward()
              {
                  // First time?
                  if (_stack == null)
                  {
                      // Setup stack
                      _stack = new Stack<IEnumerator<T>>();
      
                      // Start with the root items
                      _stack.Push(_owner._rootItems.GetEnumerator());
                  }
      
                  // Process enumerators on the stack
                  while (_stack.Count > 0)
                  {
                      // Get the current one
                      var se = _stack.Peek();
      
                      // Next please...
                      if (se.MoveNext())
                      {
                          // Store it
                          _current = se.Current;
      
                          // Get child items
                          var childItems = _owner._selector(_current);
                          if (childItems != null)
                          {
                              _stack.Push(childItems.GetEnumerator());
                          }
      
                          return true;
                      }
      
                      // Finished with the enumerator
                      se.Dispose();
                      _stack.Pop();
                  }
      
                  // Finished!
                  return false;
              }
      
              public bool MoveReverse()
              {
                  // First time?
                  if (_stack == null)
                  {
                      // Setup stack
                      _stack = new Stack<IEnumerator<T>>();
      
                      // Start with the root items
                      _stack.Push(_owner._rootItems.Reverse().GetEnumerator());
                  }
      
                  // Process enumerators on the stack
                  while (_stack.Count > 0)
                  {
                      // Get the current one
                      var se = _stack.Peek();
      
                      // Next please...
                      if (se.MoveNext())
                      {
                          // Get child items
                          var childItems = _owner._selector(se.Current);
                          if (childItems != null)
                          {
                              _stack.Push(childItems.Reverse().GetEnumerator());
                              continue;
                          }
      
                          // Store it
                          _current = se.Current;
                          return true;
                      }
      
                      // Finished with the enumerator
                      se.Dispose();
                      _stack.Pop();
      
                      if (_stack.Count > 0)
                      {
                          _current = _stack.Peek().Current;
                          return true;
                      }
                  }
      
                  // Finished!
                  return false;
              }
      
              public void Reset()
              {
                  Dispose();
              }
          }
      }
      

    【讨论】:

      【解决方案4】:

      递归总是很有趣。也许您可以将代码简化为:

      public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects, Func<T, IEnumerable<T>> selector) {
          // Stop if subjects are null or empty 
          if (subjects == null || !subjects.Any())
              return Enumerable.Empty<T>();
      
          // Gather a list of all (selected) child elements of all subjects
          var subjectChildren = subjects.SelectMany(selector);
      
          // Jump into the recursion for each of the child elements
          var recursiveChildren = SelectRecursive(subjectChildren, selector);
      
          // Combine the subjects with all of their (recursive child elements).
          // The union will remove any direct parent-child duplicates.
          // Endless loops due to circular references are however still possible.
          return subjects.Union(recursiveChildren);
      }
      

      这将导致比您的原始代码更少的重复。但是它们可能仍然是重复的,导致无限循环,联合只会防止直接的父子重复。

      而且物品的顺序会和你的不一样:)

      编辑:将最后一行代码更改为三个语句并添加了更多文档。

      【讨论】:

      • 有趣...虽然有点不可读,呵呵。顺便说一句,订购并不重要,所以不要担心:p
      • 我已将单个语句拆分为子结果,这可能使其更易于阅读/理解。基本上我已经用 LINQ 替换了你的 for 循环。当然你也可以疯狂,把这个方法简化为单行语句:)
      【解决方案5】:

      使用聚合扩展...

          List<Person> persons = GetPersons(); 
          List<Person> result = new List<Person>(); 
          persons.Aggregate(result,SomeFunc);
      
          private static List<Person> SomeFunc(List<Person> arg1,Person arg2)
          {
          arg1.Add(arg2)
          arg1.AddRange(arg2.Persons);
          return arg1;
          }
      

      【讨论】:

      • 我实际上在考虑这个问题。想制作一些示例代码吗?
      • 有趣。不过,这不会处理循环关系,对吗?
      • 你可以添加一个简单的 if(arg1.Containes(arg2))
      • 除非我读错了代码,否则这只下降了一层——它不会递归到任意深度。我相信它相当于foreach (var person in persons) { result.Add(person); result.AddRange(person.Persons); }
      【解决方案6】:

      您也可以使用这样的非递归方法:

        HashSet<Person> GatherAll (Person p) {
           Stack<Person> todo = new Stack<Person> ();
           HashSet<Person> results = new HashSet<Person> ();
           todo.Add (p); results.Add (p);
           while (todo.Count > 0) {
              Person p = todo.Pop (); 
              foreach (Person f in p.Friends) 
                 if (results.Add (f)) todo.Add (f);
           }
           return results;
        }
      

      这也应该能正确处理循环。我am从一个人开始,但您可以轻松地将其扩展为从人员列表开始。

      【讨论】:

        【解决方案7】:

        我不相信 LINQ 中内置了任何东西来做到这一点。

        像这样递归地执行它有一个问题——你最终会创建大量的迭代器。如果树很深,这可能会非常低效。 Wes DyerEric Lippert 都写过关于这个的博客。

        您可以通过删除直接递归来消除这种低效率。例如:

        public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects,
            Func<T, IEnumerable<T>> selector)
        {
            if (subjects == null)
            {
                yield break;
            }
        
            Queue<T> stillToProcess = new Queue<T>(subjects);
        
            while (stillToProcess.Count > 0)
            {
                T item = stillToProcess.Dequeue();
                yield return item;
                foreach (T child in selector(item))
                {
                    stillToProcess.Enqueue(child);
                }
            }
        }
        

        这也会改变迭代顺序——它变成广度优先而不是深度优先;将其重写为仍然是深度优先是很棘手的。我也将其更改为不使用Any() - 此修订版不会多次评估任何序列,这在某些情况下可能很方便。请注意,这确实有一个问题 - 由于排队,它会占用更多内存。我们可能可以通过存储迭代器队列而不是项目来缓解这种情况,但我不确定临时......它肯定会更复杂。

        需要注意的一点(ChrisW 在我查找博客文章时也指出 :) - 如果您的朋友列表中有任何循环(即如果 A 有 B,B 有 A),那么您将永远递归.

        【讨论】:

        • @Inquisitor:仅当类型可变时。否则,您可以使用 HashSet&lt;T&gt; 来存储您已经访问过的项目。
        • 为什么深度优先很难做到?不就是把队列换成栈吗?
        • @Eric:这很有可能......虽然你首先得到它的深度并且每个集合中的最后一个所以它仍然不符合原始顺序:(再次,我敢肯定它可以通过更多的努力 - 但我的大脑目前无法考虑它。
        • 啊,是的,我明白你的意思。有趣的巧合,我刚刚检查了我们用来确定发出哪些订单类的递归算法,并想知道它是否可以迭代。让这个算法迭代,正好有这个问题;它并不完全是深度优先,因为那样会颠倒给定命名空间中的类的发出顺序。明智地使用 Reverse() 序列运算符应该很容易修复它。
        • 实际上,对于“已访问”标志有更简单的解决方案 - 例如使用 GUID(在每次展开时生成)或使用在每次展开时递增的整数。然后您不检查是否设置了“已访问”标志;您检查它是否设置为此特定展开的值。但是,如果您使用多线程并且两个线程要同时展开,则此解决方案仍然存在问题...
        猜你喜欢
        • 2019-12-25
        • 1970-01-01
        • 2014-11-24
        • 2020-11-09
        • 1970-01-01
        • 1970-01-01
        • 2011-09-12
        • 2016-04-13
        • 1970-01-01
        相关资源
        最近更新 更多