用一个语句展平一棵树(列表列表)?

Posted

技术标签:

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

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

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

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

【问题讨论】:

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

以上是关于用一个语句展平一棵树(列表列表)?的主要内容,如果未能解决你的问题,请参考以下文章

是否有展平嵌套元素列表的功能?

用嵌套列表和嵌套字典列表展平一个非常大的 Json

展平 Python 列表而不创建任何对象的副本?

一日一技:如何把多层嵌套的列表展平

我怎样才能完全展平一个列表(列表(列表)......)

我怎样才能完全展平一个列表(列表(列表)......)