Jak spłaszczyć drzewo przez LINQ?

Więc mam proste drzewo:

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

Mam IEnumerable<MyNode>. Chcę otrzymać listę wszystkich MyNode (w tym inner node objects (Elements)) jako jedną płaską listę Where group == 1. Jak to zrobić przez LINQ?

Author: myWallJSON, 2012-08-06

14 answers

Możesz spłaszczyć drzewo tak:

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

Możesz następnie filtrować według group za pomocą Where(...).

Aby zdobyć "punkty za styl", przekonwertuj Flatten na funkcję rozszerzenia w klasie statycznej.

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

Aby zdobyć więcej punktów za "jeszcze lepszy styl", przekonwertuj Flatten do ogólnej metody rozszerzenia, która pobiera drzewo i funkcję, która tworzy potomków z węzła:

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

Wywołaj tę funkcję w następujący sposób:

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

Jeśli wolisz spłaszczenie w przedsprzedaży zamiast po kolei, przełącz się po bokach Concat(...).

 143
Author: Sergey Kalinichenko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-03-27 11:48:21

Problem z zaakceptowaną odpowiedzią polega na tym, że jest ona nieefektywna, jeśli drzewo jest głębokie. Jeśli drzewo jest Bardzo Głębokie, to wieje stos. Problem można rozwiązać używając jawnego stosu:

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

Zakładając n węzłów w drzewie o wysokości h i współczynniku rozgałęzienia znacznie mniejszym niż n, metoda ta wynosi O(1) w przestrzeni stosu, O(h) w przestrzeni stosu i O(N) w czasie. Innym podanym algorytmem jest O (h) w stosie, o(1) w stercie i o(nh) w czasie. Jeśli współczynnik rozgałęzienia jest mały w porównaniu do n, h znajduje się pomiędzy O (lg n) I O (N), co ilustruje, że naiwny algorytm może użyć niebezpiecznej ilości stosu i dużej ilości czasu, jeśli H jest blisko n.

Teraz, gdy mamy Trawers, Twoje zapytanie jest proste:

root.Traverse().Where(item=>item.group == 1);
 127
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-16 06:18:28

Dla kompletności, oto kombinacja odpowiedzi dasblinkenlight i Eric Lippert. Jednostka testowana i w ogóle. :-)

 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);
     }
 }
 27
Author: Konamiman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-17 08:14:49

Update:

Dla osób zainteresowanych poziomem zagnieżdżenia (głębią). Jedną z dobrych rzeczy na temat implementacji stosu jawnego enumeratora jest to, że w dowolnym momencie (a w szczególności podczas otrzymywania elementu) stack.Count reprezentuje aktualnie przetwarzaną głębokość. Biorąc to pod uwagę i używając krotek wartości C#7.0, możemy po prostu zmienić deklarację metody w następujący sposób:

public static IEnumerable<(T Item, int Level)> ExpandWithLevel<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)

I yield wypowiedź:

yield return (item, stack.Count);

Wtedy możemy zaimplementować oryginał metoda poprzez zastosowanie prostej Select na powyższym:

public static IEnumerable<T> Expand<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector) =>
    source.ExpandWithLevel(elementSelector).Select(e => e.Item);

Oryginalny:

O dziwo nikt (nawet Eric) nie pokazał" naturalnego " portu iteracyjnego rekurencyjnego pre-order DFT, więc oto on:]}
    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();
        }
    }
 23
Author: Ivan Stoev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-30 19:22:52

Znalazłem kilka małych problemów z odpowiedziami podanymi tutaj:

  • co jeśli początkowa lista elementów jest null?
  • co jeśli na liście dzieci znajduje się wartość null?

Zbudowany na poprzednich odpowiedziach i wymyślił:

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

I testy jednostkowe:

[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; }
    }
}
 8
Author: Doug Clutter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-29 17:19:25

Jeśli ktoś inny to znajdzie, ale musi znać poziom po spłaszczeniu drzewa, rozszerza się to na kombinację rozwiązań Konamimana dasblinkenlight i Erica Lipperta:]}

    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));
        }
    }
 4
Author: Dave,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-08 21:01:28

Inną opcją jest posiadanie odpowiedniego projektu OO.

Np. poproś {[2] } o zwrócenie wszystkich spłaszczonych.

Tak:

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

Teraz możesz poprosić MyNode najwyższego poziomu o uzyskanie wszystkich węzłów.

var flatten = topNode.GetAllNodes();

Jeśli nie możesz edytować klasy, to nie jest to opcja. Ale poza tym, myślę, że może być to preferowana oddzielna (rekurencyjna) metoda LINQ.

To używa LINQ, więc myślę, że ta odpowiedź ma zastosowanie tutaj;)

 2
Author: Julian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-04-20 15:47:45

Połączenie odpowiedzi Dave 'a i Ivana Stoeva w przypadku, gdy potrzebujesz poziomu zagnieżdżenia i listy spłaszczonej "w porządku", a nie odwróconej, jak w odpowiedzi udzielonej przez Konamimana.

 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();
            }
        }
    }
 1
Author: Corcus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-11 17:07:19

Oto kilka gotowych do użycia implementacji za pomocą kolejki i zwracania drzewa spłaszczonego najpierw mnie, a potem moje dzieci.

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

    }
 1
Author: Ruben Alfonso,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-02-12 09:14:40

Większość prezentowanych tutaj odpowiedzi tworzy depth-first lub sekwencje zig-zag. Na przykład zaczynając od drzewa poniżej:

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

Dasblinkenlight ' s odpowiedź daje tę spłaszczoną sekwencję:

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

Odpowiedź Konamimana (która uogólnia odpowiedź Erica Lipperta } ) tworzy tę spłaszczoną sekwencję:

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

Odpowiedź Iwana Stojewa daje tę spłaszczoną sekwencję:

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

Jeśli jesteś zainteresowany szerokość-pierwsza Sekwencja jak ta:

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

...to jest rozwiązanie dla Ciebie:

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

Różnica w implementacji polega zasadniczo na użyciu Queue zamiast Stack. Nie ma faktycznego sortowania.


Uwaga: Ta implementacja nie jest optymalna pod względem wydajności pamięci, ponieważ duży procent całkowitej liczby elementów zostanie zapisany w wewnętrznej kolejce podczas wyliczania. Stack-na podstawie implementacje tree-traversals są znacznie bardziej wydajne pod względem wykorzystania pamięci niż implementacje oparte na Queue.

 1
Author: Theodor Zoulias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-19 22:24:58
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;
}
 0
Author: Tom Brothers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-06 14:38:11

Bazując na odpowiedzi Konamimana i komentarzu, że zamówienie jest nieoczekiwane, oto wersja z wyraźnym param sortowania:

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

I przykładowe użycie:

var flattened = doc.TraverseAndFlatten(x => x.DependentDocuments, y => y.Document.DocDated).ToList();
 0
Author: rothschild86,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-23 22:19:45

Poniżej znajduje się kod Iwana Stojewa z dodatkową funkcją informującą o indeksie każdego obiektu na ścieżce. Np. wyszukaj "Item_120":

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

Zwróci element i tablicę int [1,2,0]. Oczywiście, poziom zagnieżdżania jest również dostępny, jako długość tablicy.

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
Author: lisz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-15 10:47:57

Od czasu do czasu staram się zarysować ten problem i opracować własne rozwiązanie, które obsługuje arbitralnie głębokie struktury (bez rekurencji), wykonuje szerokość pierwszego przejścia i nie nadużywa zbyt wielu zapytań LINQ lub prewencyjnie wykonuje rekurencję na dzieciach. Po grzebaniu w źródle . NET i wypróbowaniu wielu rozwiązań, w końcu wymyśliłem to rozwiązanie. Skończyło się na tym, że jest bardzo blisko odpowiedzi Iana Stoeva (którego odpowiedź widziałem dopiero teraz), jednak moja Nie używaj nieskończonych pętli lub mieć nietypowy przepływ kodu.

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

Przykład pracy można znaleźć tutaj .

 0
Author: Chris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-07-25 20:59:39