用一个语句展平一棵树(列表列表)?
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<T>
,因为插入是 O(1) 操作,而对 List<T>
的插入是 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 到底想要什么......以上是关于用一个语句展平一棵树(列表列表)?的主要内容,如果未能解决你的问题,请参考以下文章