C# 如何通过LINQ展平树?

C# 如何通过LINQ展平树?,c#,.net,linq,.net-4.0,tree,C#,.net,Linq,.net 4.0,Tree,所以我有一个简单的树: class MyNode { public MyNode Parent; public IEnumerable<MyNode> Elements; int group = 1; } 我有一本数不清的书。我想得到一个包含内部节点对象元素的所有MyNode的列表,作为一个平面列表,其中group==1。如何通过LINQ做这样的事情?你可以像这样压平一棵树: IEnumerable<MyNode> Flatten(IEnumerable<M

所以我有一个简单的树:

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

我有一本数不清的书。我想得到一个包含内部节点对象元素的所有MyNode的列表,作为一个平面列表,其中group==1。如何通过LINQ做这样的事情?

你可以像这样压平一棵树:

IEnumerable<MyNode> Flatten(IEnumerable<MyNode> e) =>
    e.SelectMany(c => Flatten(c.Elements)).Concat(new[] { e });
IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);
要为更好的样式赢得更多分数,请将“展平”转换为通用扩展方法,该方法采用树和从节点生成子体的函数:

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> Flatten(IEnumerable<MyNode> e) =>
    e.SelectMany(c => Flatten(c.Elements)).Concat(new[] { e });
IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);

如果您更喜欢按预排序而不是按后排序进行展平,请切换混凝土的侧面……

公认答案的问题是,如果树很深,则效率低下。如果这棵树很深,它就会把它吹倒。您可以使用显式堆栈解决此问题:

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

为了完整起见,这里是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);
     }
 }

如果其他人发现了这一点,但在压平树后还需要知道其级别,这将扩展到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));
        }
    }
更新:

适合对嵌套深度感兴趣的人。显式枚举器堆栈实现的优点之一是,在任何时刻,尤其是在生成元素时,stack.Count表示当前的处理深度。因此,考虑到这一点并利用C7.0值元组,我们可以简单地更改方法声明,如下所示:

public static IEnumerable<(T Item, int Level)> ExpandWithLevel<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
然后,我们可以通过在上面应用简单选择来实现原始方法:

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

结合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();
            }
        }
    }

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

下面是Ivan Stoev的代码,它还具有告诉路径中每个对象的索引的附加功能。例如,搜索项目_120:

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120
将返回项和整数数组[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 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; }
    }
}

另一个选择是采用适当的OO设计

e、 g.要求MyNode返回所有展平

像这样:

类MyNode { 公共MyNode父节点; 公共可数元素; int组=1; 公共IEnumerable GetAllNodes { 如果元素==null { 返回可枚举的。空; } return Elements.SelectManye=>e.GetAllNodes; } } 现在,您可以要求顶级MyNode获取所有节点

var flatten = topNode.GetAllNodes();
如果您无法编辑该类,则这不是一个选项。但除此之外,我认为这可能是一个单独的递归LINQ方法的首选


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

这里有一些现成的实现,使用队列并首先返回扁平树给我,然后返回我的孩子

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

    }

每隔一段时间,我都会尝试解决这个问题,并设计出自己的解决方案,该解决方案支持任意深度的结构—无递归,执行广度优先遍历,并且不会滥用太多LINQ查询,也不会在子级上先发制人地执行递归。在深入研究和尝试了许多解决方案之后,我终于想出了这个解决方案。它最终非常接近Ian Stoev的答案,我刚才才看到他的答案,但是我的答案并没有使用无限循环,也没有不寻常的代码流

公共静态IEnumerable遍历 这是一个数不清的来源, 函数递归 { 如果源!=null { 堆栈枚举数=新堆栈; 尝试 { enumerators.Pushsource.GetEnumerator; 而枚举数。计数>0 { var top=枚举数; 上一步,下一步 { 收益率返回顶部。当前; var children=fnRecursetop.Current; 如果子项!=null { top=children.GetEnumerator; 枚举器; } } enumerators.Pop.Dispose; } } 最后 { 而枚举数。计数>0 enumerators.Pop.Dispose; } } }
可以找到一个工作示例。

这里给出的大多数答案都是生成序列或锯齿序列。例如,从下面的树开始:

1 2 / \ / \ / \ / \ / \ / \ / \ / \ 11 12 21 22 / \ / \ / \ / \ / \ / \ / \ / \ 111 112 121 122 211 212 221 222 dasblinkenlight的生成此展平序列:

111, 112, 121, 122, 11, 12, 211, 212, 221, 222, 21, 22, 1, 2 推广Eric Lippert的Konamiman产生了这个平坦序列:

2, 22, 222, 221, 21, 212, 211, 1, 12, 122, 121, 11, 112, 111 Ivan Stoev的制作了这个展平序列:

1, 11, 111, 112, 12, 121, 122, 2, 21, 211, 212, 22, 221, 222 如果您对这样的序列感兴趣:

IEnumerable<MyNode> Flatten(IEnumerable<MyNode> e) =>
    e.SelectMany(c => Flatten(c.Elements)).Concat(new[] { e });
IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);
1, 2, 11, 12, 21, 22, 111, 112, 121, 122, 211, 212, 221, 222 …那么这就是您的解决方案:

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source,
    Func<T, IEnumerable<T>> childrenSelector)
{
    var queue = new Queue<T>(source);
    while (queue.Count > 0)
    {
        var current = queue.Dequeue();
        yield return current;
        var children = childrenSelector(current);
        if (children == null) continue;
        foreach (var child in children) queue.Enqueue(child);
    }
}
实现中的差异基本上是使用队列而不是堆栈。没有进行实际的分拣



注意:这种实现在内存效率方面远远不是最优的,因为在枚举期间,元素总数的很大一部分最终将存储在内部队列中。与基于队列的实现相比,基于堆栈的树遍历在内存使用方面效率更高。

您希望扁平化列表的顺序是什么?节点何时停止具有子节点?我假设它是在元素为null或空时出现的?可能是重复的,最简单/最清楚的解决方法是使用递归LINQ查询。这个问题:对此有很多讨论,具体的答案是关于如何实现它的细节。@AdamHouldsworth感谢您的编辑!对Concat的调用中的元素应该是新的[]{e},而不是新的[]{c},它甚至不会在那里用c进行编译。使用e不会编译。您还可以添加if e==null返回Enumerable.Empty;处理空的子列表。更像“public static IEnumerable flattthis IEnumerable source,Func f{if source==null返回可枚举的。Empty;return source.SelectManyc=>fc.flattf.Concatsource;}`注意,这个解决方案是Onh,其中n是树中的项数,h是树的平均深度。因为h可以在O1和On之间,所以这是在On和On平方算法之间。还有更好的算法。我注意到,如果列表是IEnumerable,函数将不会向展平列表添加元素。您可以通过如下方式调用函数来解决此问题:var res=tree.flattNode=>node.Elements.oftype在扩展中使用foreach意味着它不再是“延迟执行”,当然,除非您使用yield return。@johnnycardy:如果您要争论一点,那么代码可能并不明显正确。什么能让它更清楚地正确?@ebramtharwat:正确。可以对所有元素调用Traverse。或者您可以修改Traverse以获取一个序列,并让它将序列的所有元素推送到堆栈上。请记住,堆栈是我尚未遍历的元素。或者,您可以在序列是其子项的位置创建一个伪根,然后遍历该伪根。如果在current.Elements.Reverse中为每个var子项创建一个伪根,您将获得更期望的平坦化。特别是,子项将按其出现的顺序出现,而不是先出现最后一个子项。这在大多数情况下都不重要,但在我的情况下,我需要按可预测和预期的顺序进行展平。@MicahZoltu,您可以通过将堆栈替换为Queue@MicahZoltu关于顺序,您是正确的,但是反向的问题是它会创建额外的迭代器,这就是这种方法要避免的@RubensFarias用队列代替堆栈会导致广度优先遍历;如果是孩子null{foreach var child in childs stack.Pushchild;}我想指出的是,尽管这确实会使列表变平,但它会以相反的顺序返回列表。最后一个元素变为第一个等。能够指定深度优先或广度优先也很好……我假设每次调用elementSelector时您都会切换e以保持预排序-如果顺序不重要,您能否更改函数以在启动后处理所有e?@NetMage我特别想要预排序。只需很少的零钱,它就可以处理邮政订单。但主要的一点是,这是深度优先遍历。对于呼吸优先遍历,我将使用队列。无论如何,这里的想法是保留一个带有枚举数的小堆栈,非常类似于递归实现中发生的情况。@IvanStoev我认为代码会简化。我猜使用堆栈将导致一个之字形的宽度优先遍历。维护堆栈而不是堆栈有什么意义?枚举数通常是可变值类型,通常作为状态机实现。所以我期待一个堆栈解决方案
Ivan仔细检查后发现,这两点都是正确的。装箱是不可避免的,存储子项的枚举数肯定比存储所有子项更可取。向上投票:-嗨,@lisz,你把代码贴在哪里?我发现一些错误,例如修饰符“public”对此项无效,修饰符“static”对此项无效可能是Enumerable。空比新列表更好?确实!更新!
public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source,
    Func<T, IEnumerable<T>> childrenSelector)
{
    var queue = new Queue<T>(source);
    while (queue.Count > 0)
    {
        var current = queue.Dequeue();
        yield return current;
        var children = childrenSelector(current);
        if (children == null) continue;
        foreach (var child in children) queue.Enqueue(child);
    }
}