【问题标题】:Flatten a tree (list of lists) with one statement?用一个语句展平一棵树(列表列表)?
【发布时间】:2011-04-23 20:56:29
【问题描述】:

感谢 nHibernate,我使用的一些数据结构是列表中列表中的列表。因此,例如,我有一个名为“category”的数据对象,它有一个 .Children 属性,该属性解析为一个类别列表......每个类别都可以有孩子......等等。

我需要找到一种方法,从该结构中的顶级类别开始,并获取整个结构中所有子级的列表或数组或类似的东西 - 所以所有子级的所有子级等,扁平化到一个列表中。

我确信它可以通过递归来完成,但我发现递归代码很难完成,而且我相信在 .Net 4 中使用 Linq 或类似的东西必须有一种更直接的方法 - 有什么建议吗?

【问题讨论】:

  • 扁平化一棵树似乎本质上是递归的。即使使用 LINQ,我也不相信有一种单一的语句方式可以使树变平。你会接受递归答案吗?
  • 当然可以。这只是似乎应该有一个“简单”答案的事情之一。 Linq "selectMany" 将展平树的两个级别,但问题是我无法知道当我开始时我的对象中有多少级别。所以我想递归是唯一的方法。

标签: .net tree flatten


【解决方案1】:

如果广度优先遍历没问题,并且您只想使用一些简短的非递归内联代码(实际上是 3 行),请创建一个使用您的根元素初始化的列表,然后将其扩展为一个简单的 for-循环:

// your data object class looks like:
public class DataObject
{
  public List<DataObject> Children { get; set; }
  ...
}

...

// given are some root elements
IEnumerable<DataObject> rootElements = ...

// initialize the flattened list with the root elements
var result = new List<DataObject>(rootElements);
// extend the flattened list by one simple for-loop, 
// please note that result.Count may increase after every iteration!
for (int index = 0; index < result.Count; index++)
  result.AddRange(result[index].Children);

【讨论】:

  • 如果你的树的深度超过 1 怎么办?这行不通。
  • @rolls 列表在循环时会自行扩展。这样它将遍历所有级别的所有元素。
  • 这很聪明,我错过了。我相信如果您使用链表,AddRange 的性能也会更高。
  • @rolls 感谢您的提示。我添加了一条评论以表明这里有一个小技巧。不幸的是,LinkedList 没有 AddRange 方法,所以我使用 List 类来保持简单。
【解决方案2】:

鉴于@E.Z.Hart 提到的类,您还可以使用辅助方法对其进行扩展,我认为在这种情况下更简单。

public class Category
{
    public string Name { get; set; }

    public List<Category> Children { get; set; }

    public IEnumerable<Category> AllChildren()
    {
        yield return this;
        foreach (var child in Children)
        foreach (var granChild in child.AllChildren())
        {
            yield return granChild;
        }
    }   
}

【讨论】:

    【解决方案3】:

    这是一个完成这项工作的扩展方法:

    // Depth-first traversal, recursive
    public static IEnumerable<T> Flatten<T>(
            this IEnumerable<T> source,
            Func<T, IEnumerable<T>> childrenSelector)
    {
        foreach (var item in source)
        {
            yield return item;
            foreach (var child in childrenSelector(item).Flatten(childrenSelector))
            {
                yield return child;
            }
        }
    }
    

    你可以这样使用它:

    foreach(var category in categories.Flatten(c => c.Children))
    {
        ...
    }
    

    上面的解决方案是深度优先遍历,如果你想要广度优先遍历,你可以这样做:

    // Breadth-first traversal, non-recursive
    public static IEnumerable<T> Flatten2<T>(
            this IEnumerable<T> source,
            Func<T, IEnumerable<T>> childrenSelector)
    {
        var queue = new Queue<T>(source);
        while (queue.Count > 0)
        {
            var item = queue.Dequeue();
            yield return item;
            foreach (var child in childrenSelector(item))
            {
                queue.Enqueue(child);
            }
        }
    }
    

    它还具有非递归的好处......


    更新:实际上,我只是想到了一种使深度优先遍历非递归的方法......这里是:

    // Depth-first traversal, non-recursive
    public static IEnumerable<T> Flatten3<T>(
            this IEnumerable<T> source,
            Func<T, IEnumerable<T>> childrenSelector)
    {
        LinkedList<T> list = new LinkedList<T>(source);
        while (list.Count > 0)
        {
            var item = list.First.Value;
            yield return item;
            list.RemoveFirst();
            var node = list.First;
            foreach (var child in childrenSelector(item))
            {
                if (node != null)
                    list.AddBefore(node, child);
                else
                    list.AddLast(child);
            }
        }
    }
    

    我使用的是 LinkedList&lt;T&gt;,因为插入是 O(1) 操作,而对 List&lt;T&gt; 的插入是 O(n)。

    【讨论】:

    • +1 表示基于队列的解决方案的聪明之处。我不知道我会称之为“简单”,但我想这是一个见仁见智的问题。广度与深度优先问题是一个重要的问题;我倾向于假设深度优先,这是一个不好的习惯。无论如何,你的答案更好。
    • 关于深度优先:添加“if (source == null) yield break;”适合大多数用例
    【解决方案4】:

    假设您的 Category 类看起来像:

     public class Category
     {
       public string Name { get; set; }
       public List<Category> Children { get; set; }
     }
    

    我认为没有一种“简单”的非递归方式可以做到这一点;如果您只是在寻找单个方法调用来处理它,那么“简单”的方法是将递归版本写入单个方法调用。可能有一种迭代方法可以做到这一点,但我猜它实际上非常复杂。这就像在不使用微积分的情况下询问“简单”的方法来找到曲线的切线。

    无论如何,这可能会做到:

    public static List<Category> Flatten(Category root) {
    
        var flattened = new List<Category> {root};
    
        var children = root.Children;
    
        if(children != null)
        {
            foreach (var child in children)
            {
                flattened.AddRange(Flatten(child));
            }
        }
    
        return flattened;
    }
    

    【讨论】:

    • is 是一个简单的非递归解决方案,但它执行广度优先遍历(请参阅我的答案)。这可能不是一个大问题,但这取决于 OP 到底想要什么......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-05-23
    • 2015-01-01
    • 2021-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-04
    相关资源
    最近更新 更多