How to flatten tree via LINQ?

后端 未结 14 2199
抹茶落季
抹茶落季 2020-11-22 04:56

So I have simple tree:

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

I have a I

相关标签:
14条回答
  • 2020-11-22 05:34

    Most of the answers presented here are producing depth-first or zig-zag sequences. For example starting with the tree below:

            1                   2 
           / \                 / \
          /   \               /   \
         /     \             /     \
        /       \           /       \
       11       12         21       22
      / \       / \       / \       / \
     /   \     /   \     /   \     /   \
    111 112   121 122   211 212   221 222
    

    dasblinkenlight's answer produces this flattened sequence:

    111, 112, 121, 122, 11, 12, 211, 212, 221, 222, 21, 22, 1, 2
    

    Konamiman's answer (that generalizes Eric Lippert's answer) produces this flattened sequence:

    2, 22, 222, 221, 21, 212, 211, 1, 12, 122, 121, 11, 112, 111
    

    Ivan Stoev's answer produces this flattened sequence:

    1, 11, 111, 112, 12, 121, 122, 2, 21, 211, 212, 22, 221, 222
    

    If you are interested in a breadth-first sequence like this:

    1, 2, 11, 12, 21, 22, 111, 112, 121, 122, 211, 212, 221, 222
    

    ...then this is the solution for you:

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

    The difference in the implementation is basically using a Queue instead of a Stack. No actual sorting is taking place.


    Caution: this implementation is far from optimal regarding memory-efficiency, since a large percentage of the total number of elements will end up being stored in the internal queue during the enumeration. Stack-based tree-traversals are much more efficient regarding memory usage than Queue-based implementations.

    0 讨论(0)
  • 2020-11-22 05:35

    Building on Konamiman's answer, and the comment that the ordering is unexpected, here's a version with an explicit sort param:

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

    And a sample usage:

    var flattened = doc.TraverseAndFlatten(x => x.DependentDocuments, y => y.Document.DocDated).ToList();
    
    0 讨论(0)
  • 2020-11-22 05:38

    Below is Ivan Stoev's code with the additonal feature of telling the index of every object in the path. E.g. search for "Item_120":

    Item_0--Item_00
            Item_01
    
    Item_1--Item_10
            Item_11
            Item_12--Item_120
    

    would return the item and an int array [1,2,0]. Obviously, nesting level is also available, as length of the array.

    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();
        }
    }
    
    0 讨论(0)
  • 2020-11-22 05:39

    You can flatten a tree like this:

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

    You can then filter by group using Where(...).

    To earn some "points for style", convert Flatten to an extension function in a static class.

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

    To earn more points for "even better style", convert Flatten to a generic extension method that takes a tree and a function that produces descendants from a node:

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

    Call this function like this:

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

    If you would prefer flattening in pre-order rather than in post-order, switch around the sides of the Concat(...).

    0 讨论(0)
  • 2020-11-22 05:39

    Every once in awhile I try to scratch at this problem and devise my own solution that supports arbitrarily deep structures (no recursion), performs breadth first traversal, and doesn't abuse too many LINQ queries or preemptively execute recursion on the children. After digging around in the .NET source and trying many solutions, I've finally come up with this solution. It ended up being very close to Ian Stoev's answer (whose answer I only saw just now), however mine doesn't utilize infinite loops or have unusual code flow.

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

    A working example can be found here.

    0 讨论(0)
  • 2020-11-22 05:46

    Combining Dave's and Ivan Stoev's answer in case you need the level of nesting and the list flattened "in order" and not reversed like in the answer given by 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 讨论(0)
提交回复
热议问题