如何通过LINQ压扁树?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何通过LINQ压扁树?相关的知识,希望对你有一定的参考价值。
所以我有简单的树:
class MyNode
{
public MyNode Parent;
public IEnumerable<MyNode> Elements;
int group = 1;
}
我有一个IEnumerable<MyNode>
。我想获得所有MyNode
(包括内部节点对象(Elements
))的列表作为一个平面列表Where
group == 1
。如何通过LINQ做这样的事情?
你可以像这样展平一棵树:
IEnumerable<MyNode> Flatten(IEnumerable<MyNode> e) {
return e.SelectMany(c => Flatten(c.Elements)).Concat(new[] {e});
}
然后你可以使用group
过滤Where(...)
。
要获得一些“风格点”,请将Flatten
转换为静态类中的扩展函数。
public static IEnumerable<MyNode> Flatten(this IEnumerable<MyNode> e) {
return e.SelectMany(c => c.Elements.Flatten()).Concat(e);
}
要获得“更好的风格”的一些积分,请将Flatten
转换为通用的扩展方法,该方法采用树和生成后代的函数:
public static IEnumerable<T> Flatten<T>(
this IEnumerable<T> e,
Func<T,IEnumerable<T>> f)
{
return e.SelectMany(c => f(c).Flatten(f)).Concat(e);
}
像这样调用这个函数:
IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);
如果您希望在预订中而不是在后期订购中进行扁平化,请切换Concat(...)
的两侧。
基于Konamiman的答案以及排序意外的评论,这里是一个带有显式排序参数的版本:
public static IEnumerable<T> TraverseAndFlatten<T, V>(this IEnumerable<T> items, Func<T, IEnumerable<T>> nested, Func<T, V> orderBy)
{
var stack = new Stack<T>();
foreach (var item in items.OrderBy(orderBy))
stack.Push(item);
while (stack.Count > 0)
{
var current = stack.Pop();
yield return current;
var children = nested(current).OrderBy(orderBy);
if (children == null) continue;
foreach (var child in children)
stack.Push(child);
}
}
以及样本用法:
var flattened = doc.TraverseAndFlatten(x => x.DependentDocuments, y => y.Document.DocDated).ToList();
下面是Ivan Stoev的代码,它具有告诉路径中每个对象的索引的附加功能。例如。搜索“Item_120”:
Item_0--Item_00
Item_01
Item_1--Item_10
Item_11
Item_12--Item_120
将返回该项和一个int数组[1,2,0]。显然,嵌套级别也可用,作为数组的长度。
public static IEnumerable<(T, int[])> Expand<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> getChildren) {
var stack = new Stack<IEnumerator<T>>();
var e = source.GetEnumerator();
List<int> indexes = new List<int>() { -1 };
try {
while (true) {
while (e.MoveNext()) {
var item = e.Current;
indexes[stack.Count]++;
yield return (item, indexes.Take(stack.Count + 1).ToArray());
var elements = getChildren(item);
if (elements == null) continue;
stack.Push(e);
e = elements.GetEnumerator();
if (indexes.Count == stack.Count)
indexes.Add(-1);
}
if (stack.Count == 0) break;
e.Dispose();
indexes[stack.Count] = -1;
e = stack.Pop();
}
} finally {
e.Dispose();
while (stack.Count != 0) stack.Pop().Dispose();
}
}
接受的答案的问题是,如果树很深,效率很低。如果树很深,那么它会吹掉堆栈。您可以使用显式堆栈来解决问题:
public static IEnumerable<MyNode> Traverse(this MyNode root)
{
var stack = new Stack<MyNode>();
stack.Push(root);
while(stack.Count > 0)
{
var current = stack.Pop();
yield return current;
foreach(var child in current.Elements)
stack.Push(child);
}
}
假设高度为h的树中的n个节点和分支因子远小于n,则该方法在堆栈空间中为O(1),在堆空间中为O(h),在时间上为O(n)。给出的另一算法是堆栈中的O(h),堆中的O(1)和时间上的O(nh)。如果分支因子与n相比较小则则h在O(lg n)和O(n)之间,这说明如果h接近n,则天真算法可以使用危险量的堆栈和大量时间。
现在我们进行了遍历,您的查询很简单:
root.Traverse().Where(item=>item.group == 1);
为了完整起见,这里是dasblinkenlight和Eric Lippert的答案的组合。单元测试和一切。 :-)
public static IEnumerable<T> Flatten<T>(
this IEnumerable<T> items,
Func<T, IEnumerable<T>> getChildren)
{
var stack = new Stack<T>();
foreach(var item in items)
stack.Push(item);
while(stack.Count > 0)
{
var current = stack.Pop();
yield return current;
var children = getChildren(current);
if (children == null) continue;
foreach (var child in children)
stack.Push(child);
}
}
更新:
对于对嵌套水平感兴趣的人(深度)。显式枚举器堆栈实现的一个好处是,在任何时刻(特别是在产生元素时)stack.Count
表示当前的处理深度。因此,考虑到这一点并使用C#7.0值元组,我们可以简单地更改方法声明,如下所示:
public static IEnumerable<(T Item, int Level)> ExpandWithLevel<T>(
this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
和yield
声明:
yield return (item, stack.Count);
然后我们可以通过在上面应用简单的Select
来实现原始方法:
public static IEnumerable<T> Expand<T>(
this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector) =>
source.ExpandWithLevel(elementSelector).Select(e => e.Item);
原版的:
令人惊讶的是,没有人(甚至是Eric)展示了递归预订DFT的“自然”迭代端口,所以这里是:
public static IEnumerable<T> Expand<T>(
this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
{
var stack = new Stack<IEnumerator<T>>();
var e = source.GetEnumerator();
try
{
while (true)
{
while (e.MoveNext())
{
var item = e.Current;
yield return item;
var elements = elementSelector(item);
if (elements == null) continue;
stack.Push(e);
e = elements.GetEnumerator();
}
if (stack.Count == 0) break;
e.Dispose();
e = stack.Pop();
}
}
finally
{
e.Dispose();
while (stack.Count != 0) stack.Pop().Dispose();
}
}
如果其他人发现了这个,但是在他们弄平了树之后也需要知道它的水平,这扩展了Konamiman的dasblinkenlight和Eric Lippert的解决方案的组合:
public static IEnumerable<Tuple<T, int>> FlattenWithLevel<T>(
this IEnumerable<T> items,
Func<T, IEnumerable<T>> getChilds)
{
var stack = new Stack<Tuple<T, int>>();
foreach (var item in items)
stack.Push(new Tuple<T, int>(item, 1));
while (stack.Count > 0)
{
var current = stack.Pop();
yield return current;
foreach (var child in getChilds(current.Item1))
stack.Push(new Tuple<T, int>(child, current.Item2 + 1));
}
}
我在这里给出了答案,我发现了一些小问题:
- 如果项目的初始列表为空,该怎么办?
- 如果孩子列表中有空值怎么办?
基于之前的答案,并提出以下建议:
public static class IEnumerableExtensions
{
public static IEnumerable<T> Flatten<T>(
this IEnumerable<T> items,
Func<T, IEnumerable<T>> getChildren)
{
if (items == null)
yield break;
var stack = new Stack<T>(items);
while (stack.Count > 0)
{
var current = stack.Pop();
yield return current;
if (current == null) continue;
var children = getChildren(current);
if (children == null) continue;
foreach (var child in children)
stack.Push(child);
}
}
}
单元测试:<
以上是关于如何通过LINQ压扁树?的主要内容,如果未能解决你的问题,请参考以下文章