如何通过LINQ展平树?


95

所以我有简单的树:

class MyNode
{
 public MyNode Parent;
 public IEnumerable<MyNode> Elements;
 int group = 1;
}

我有一个IEnumerable<MyNode>。我想将所有列表MyNode(包括内部节点对象(Elements))作为一个平面列表Where group == 1。如何通过LINQ做这样的事情?


1
您希望平展列表按什么顺序?
菲利普(Philip)

1
节点何时停止拥有子节点?我想Elements是什么时候为null或为空?
亚当·霍尔兹沃思


解决此问题的最简单/最清晰的方法是使用递归LINQ查询。这个问题:stackoverflow.com/questions/732281/expressing-recursion-in-linq对此进行了很多讨论,这个特定答案对如何实现进行了详细说明。
Alvaro Rodriguez

Answers:


137

您可以像这样压扁一棵树:

IEnumerable<MyNode> Flatten(IEnumerable<MyNode> e) =>
    e.SelectMany(c => Flatten(c.Elements)).Concat(new[] { e });

然后,您可以group使用过滤Where(...)

要获得一些“风格要点”,请转换Flatten为静态类中的扩展功能。

public static IEnumerable<MyNode> Flatten(this IEnumerable<MyNode> e) =>
    e.SelectMany(c => c.Elements.Flatten()).Concat(e);

要为“甚至更好的样式”赢得更多积分,请转换Flatten为采用一棵树和一个从节点生成后代的函数的通用扩展方法:

public static IEnumerable<T> Flatten<T>(
    this IEnumerable<T> e
,   Func<T,IEnumerable<T>> f
) => e.SelectMany(c => f(c).Flatten(f)).Concat(e);

像这样调用此函数:

IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);

如果您希望按顺序而不是按顺序展平,请在的两侧进行切换Concat(...)


@AdamHouldsworth感谢您的编辑!调用中的元素Concat应该为new[] {e},而不是new[] {c}(它甚至不会在c那里编译)。
dasblinkenlight 2012年

我不同意:进行了编译,测试和使用c。使用e不会编译。您还可以添加if (e == null) return Enumerable.Empty<T>();以处理空子列表。
亚当·霍尔兹沃思

1
更像`public static IEnumerable <T> Flatten <T>(此IEnumerable <T>源,Func <T,IEnumerable <T >> f){如果(source == null)返回Enumerable.Empty <T>(); 返回source.SelectMany(c => f(c).Flatten(f))。Concat(source); }`
myWallJSON

10
请注意,此解为O(nh),其中n是树中的项目数,h是树的平均深度。由于h可以介于O(1)和O(n)之间,因此它介于O(n)和O(n平方)算法之间。有更好的算法。
埃里克·利珀特

1
我注意到,如果列表为IEnumerable <baseType>,则该函数不会将元素添加到扁平化列表中。您可以通过调用以下函数来解决此问题:var res = tree.Flatten(node => node.Elements.OfType <DerivedType>)
Frank Horemans

125

可接受答案的问题在于,如果树很深,效率低下。如果树是非常深的,然后它吹堆栈。您可以使用显式堆栈解决问题:

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);
    }
}

假设树中的n个节点的高度为h,分支因子小于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);

3
@johnnycardy:如果您要提出一个观点,那么代码可能显然不正确。有什么可以使它更清楚地正确呢?
Eric Lippert 2014年

3
@ebramtharwat:正确。您可以调用Traverse所有元素。或者,您可以进行修改Traverse以采用一个序列,并将其将序列的所有元素压入stack。记住,stack是“我尚未遍历的元素”。或者,您可以在序列为子序列的情况下创建“虚拟”根,然后遍历虚拟根。
Eric Lippert 2014年

2
如果这样做,foreach (var child in current.Elements.Reverse())您将获得更加预期的展平。特别是,子级将以出现的顺序显示,而不是最后一个子级。在大多数情况下,这无关紧要,但是在我的情况下,我需要将展平以可预测和预期的顺序进行。
米卡·佐尔图

2
@MicahZoltu,您可以.Reverse通过将Stack<T>a 换成Queue<T>
Rubens Farias

2
@MicahZoltu您对顺序是正确的,但是问题Reverse在于它会创建其他迭代器,这就是这种方法要避免的迭代器。@RubensFarias代Queue用于Stack在广度优先遍历结果。
杰克

25

仅出于完整性考虑,这里结合了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);
     }
 }

3
为了避免NullReferenceException var children = getChildren(current); if(children!= null){foreach(child中的var child)stack.Push(child); }
serg 2015年

2
我想指出的是,即使这确实使列表变平,它也会以相反的顺序返回它。最后一个元素变为第一个,等等
。– Corcus

21

更新:

对于对嵌套级别(深度)感兴趣的人。关于显式枚举器堆栈实现的好处之一是,在任何时候(尤其是在产生元素时),它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();
        }
    }

我假设您在e每次调用时都elementSelector进行切换以保持预订状态-如果订购没有关系,是否可以更改功能以e在启动后处理所有功能?
NetMage

我要特别预定@NetMage。只需少量更改,即可处理后期订单。但是要点是,这是深度优先遍历。对于“ 呼吸优先遍历”,我将使用Queue<T>。无论如何,这里的想法是与枚举器保持一个小的堆栈,这与递归实现中发生的情况非常相似。
伊万·斯托耶夫

@IvanStoev我在想代码会简化。我猜想使用Stack它将导致曲折宽度优先遍历。
NetMage

7

我发现此处给出的答案有一些小问题:

  • 如果初始项目列表为空怎么办?
  • 如果子列表中的值为空,该怎么办?

基于先前的答案,并提出以下内容:

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);
        }
    }
}

和单元测试:

[TestClass]
public class IEnumerableExtensionsTests
{
    [TestMethod]
    public void NullList()
    {
        IEnumerable<Test> items = null;
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(0, flattened.Count());
    }
    [TestMethod]
    public void EmptyList()
    {
        var items = new Test[0];
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(0, flattened.Count());
    }
    [TestMethod]
    public void OneItem()
    {
        var items = new[] { new Test() };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(1, flattened.Count());
    }
    [TestMethod]
    public void OneItemWithChild()
    {
        var items = new[] { new Test { Id = 1, Children = new[] { new Test { Id = 2 } } } };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(2, flattened.Count());
        Assert.IsTrue(flattened.Any(i => i.Id == 1));
        Assert.IsTrue(flattened.Any(i => i.Id == 2));
    }
    [TestMethod]
    public void OneItemWithNullChild()
    {
        var items = new[] { new Test { Id = 1, Children = new Test[] { null } } };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(2, flattened.Count());
        Assert.IsTrue(flattened.Any(i => i.Id == 1));
        Assert.IsTrue(flattened.Any(i => i == null));
    }
    class Test
    {
        public int Id { get; set; }
        public IEnumerable<Test> Children { get; set; }
    }
}

4

万一其他人发现了这个问题,但又需要弄平树后的水平,可以扩展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));
        }
    }

2

另一个真正的选择是具有适当的OO设计。

例如,要求MyNode退还所有拼合。

像这样:

class MyNode
{
    public MyNode Parent;
    public IEnumerable<MyNode> Elements;
    int group = 1;

    public IEnumerable<MyNode> GetAllNodes()
    {
        if (Elements == null)
        {
            return Enumerable.Empty<MyNode>(); 
        }

        return Elements.SelectMany(e => e.GetAllNodes());
    }
}

现在,您可以要求顶级MyNode获取所有节点。

var flatten = topNode.GetAllNodes();

如果您无法编辑课程,那么这不是一个选择。但是否则,我认为这可能是单独的(递归)LINQ方法的首选。

这是使用LINQ,所以我认为此答案适用于这里;)


也许Enumerabl.Empty比新List好吗?
弗兰克

1
确实!更新!
朱利安

0
void Main()
{
    var allNodes = GetTreeNodes().Flatten(x => x.Elements);

    allNodes.Dump();
}

public static class ExtensionMethods
{
    public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> childrenSelector = null)
    {
        if (source == null)
        {
            return new List<T>();
        }

        var list = source;

        if (childrenSelector != null)
        {
            foreach (var item in source)
            {
                list = list.Concat(childrenSelector(item).Flatten(childrenSelector));
            }
        }

        return list;
    }
}

IEnumerable<MyNode> GetTreeNodes() {
    return new[] { 
        new MyNode { Elements = new[] { new MyNode() }},
        new MyNode { Elements = new[] { new MyNode(), new MyNode(), new MyNode() }}
    };
}

class MyNode
{
    public MyNode Parent;
    public IEnumerable<MyNode> Elements;
    int group = 1;
}

1
在扩展中使用foreach意味着它不再是“延迟执行”(除非您当然使用yield return)。
Tri Q Tran

0

如果需要嵌套级别,则将Dave和Ivan Stoev的答案结合起来,列表“按顺序”变平并且不会像Konamiman给出的答案那样颠倒。

 public static class HierarchicalEnumerableUtils
    {
        private static IEnumerable<Tuple<T, int>> ToLeveled<T>(this IEnumerable<T> source, int level)
        {
            if (source == null)
            {
                return null;
            }
            else
            {
                return source.Select(item => new Tuple<T, int>(item, level));
            }
        }

        public static IEnumerable<Tuple<T, int>> FlattenWithLevel<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
        {
            var stack = new Stack<IEnumerator<Tuple<T, int>>>();
            var leveledSource = source.ToLeveled(0);
            var e = leveledSource.GetEnumerator();
            try
            {
                while (true)
                {
                    while (e.MoveNext())
                    {
                        var item = e.Current;
                        yield return item;
                        var elements = elementSelector(item.Item1).ToLeveled(item.Item2 + 1);
                        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();
            }
        }
    }

能够先指定深度还是先宽度还不错……

0

以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();

0

以下是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();
    }
}

嗨,@ lisz,您将此代码粘贴到哪里?我收到诸如“修饰符'public'对此项目无效”,“修饰符'static'对此项目无效”之类的错误
Kynao

0

这里有一些可以使用Queue的现成的实现,并且先返回Flatten树,然后返回我的孩子。

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> items, 
    Func<T,IEnumerable<T>> getChildren)
    {
        if (items == null)
            yield break;

        var queue = new Queue<T>();

        foreach (var item in items) {
            if (item == null)
                continue;

            queue.Enqueue(item);

            while (queue.Count > 0) {
                var current = queue.Dequeue();
                yield return current;

                if (current == null)
                    continue;

                var children = getChildren(current);
                if (children == null)
                    continue;

                foreach (var child in children)
                    queue.Enqueue(child);
            }
        }

    }

0

我不时尝试解决这个问题,并设计自己的解决方案,以支持任意深度的结构(不进行递归),执行广度优先遍历,不滥用太多LINQ查询或对子级执行递归。在探究.NET源代码并尝试了许多解决方案之后,我终于提出了该解决方案。最终它与Ian Stoev的答案非常接近(我刚刚才看到它的答案),但是我的代码没有利用无限循环或异常的代码流。

public static IEnumerable<T> Traverse<T>(
    this IEnumerable<T> source,
    Func<T, IEnumerable<T>> fnRecurse)
{
    if (source != null)
    {
        Stack<IEnumerator<T>> enumerators = new Stack<IEnumerator<T>>();
        try
        {
            enumerators.Push(source.GetEnumerator());
            while (enumerators.Count > 0)
            {
                var top = enumerators.Peek();
                while (top.MoveNext())
                {
                    yield return top.Current;

                    var children = fnRecurse(top.Current);
                    if (children != null)
                    {
                        top = children.GetEnumerator();
                        enumerators.Push(top);
                    }
                }

                enumerators.Pop().Dispose();
            }
        }
        finally
        {
            while (enumerators.Count > 0)
                enumerators.Pop().Dispose();
        }
    }
}

一个有效的例子可以在这里找到。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.