it-swarm.com.de

Wie kann man einen Baum über LINQ reduzieren?

Ich habe also einen einfachen Baum:

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

Ich habe ein IEnumerable<MyNode>. Ich möchte eine Liste aller MyNode (einschließlich innerer Knotenobjekte (Elements)) als eine flache Liste Wheregroup == 1. Wie mache ich so etwas über LINQ?

82
myWallJSON

Sie können einen Baum wie folgt platt machen:

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

Sie können dann mit Where(...) nach group filtern.

Um einige "Punkte für Stil" zu erhalten, konvertieren Sie Flatten in eine Erweiterungsfunktion in einer statischen Klasse.

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

Um einige Punkte für "noch besseren Stil" zu erhalten, konvertieren Sie Flatten in eine generische Erweiterungsmethode, die einen Baum und eine Funktion verwendet, die Nachkommen hervorbringt:

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

Rufen Sie diese Funktion folgendermaßen auf:

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

Wenn Sie das Reduzieren lieber in der Vorbestellung als in der Nachbestellung bevorzugen, wechseln Sie die Seiten des Concat(...).

129
dasblinkenlight

Das Problem mit der akzeptierten Antwort ist, dass es ineffizient ist, wenn der Baum tief ist. Wenn der Baum sehr tief ist, sprengt er den Stapel. Sie können das Problem mit einem expliziten Stack lösen:

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

Unter der Annahme, dass n Knoten in einem Baum der Höhe h und ein Verzweigungsfaktor erheblich kleiner als n sind, ist diese Methode O(1) im Stapelspeicher, O(h) = in Heap Space und O(n) in Time. Der andere angegebene Algorithmus ist O(h) in Stack, O(1) in Heap und O(nh) in der Zeit. Wenn der Verzweigungsfaktor im Vergleich zu n klein ist, dann liegt h zwischen O (lg n) und O (n), was veranschaulicht dass der naive Algorithmus eine gefährliche Menge Stapel und eine große Menge Zeit verwenden kann, wenn h in der Nähe von n liegt.

Jetzt, da wir eine Durchquerung haben, ist Ihre Anfrage unkompliziert:

root.Traverse().Where(item=>item.group == 1);
117
Eric Lippert

Der Vollständigkeit halber hier die Kombination der Antworten von dasblinkenlight und Eric Lippert. Einheit getestet und alles. :-)

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

pdate:

Für Menschen mit Interesse an der Verschachtelung (Tiefe). Eines der guten Dinge bei der expliziten Implementierung des Enumerator-Stacks ist, dass zu jedem Zeitpunkt (und insbesondere bei der Ausgabe des Elements) das stack.Count repräsentiert die aktuelle Bearbeitungstiefe. Wenn Sie dies berücksichtigen und die C # 7.0-Wertetupel verwenden, können Sie die Methodendeklaration einfach wie folgt ändern:

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

und yield Anweisung:

yield return (item, stack.Count);

Dann können wir die ursprüngliche Methode implementieren, indem wir einfaches Select auf das Obige anwenden:

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

Original:

Überraschenderweise zeigte niemand (auch Eric) den "natürlichen" iterativen Port einer rekursiven DFT vor der Bestellung. Hier ist es also:

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

Ich habe einige kleine Probleme mit den hier gegebenen Antworten gefunden:

  • Was ist, wenn die anfängliche Liste der Elemente null ist?
  • Was ist, wenn die Liste der untergeordneten Elemente einen Nullwert enthält?

Basierte auf den vorherigen Antworten und lieferte Folgendes:

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

Und die Unit-Tests:

[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
Doug Clutter

Falls dies jemand anderes findet, aber auch den Level nach dem Abflachen des Baums kennen muss, erweitert dies die Konamiman-Kombination von dasblinkenlight und Eric Lipperts Lösungen:

    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
Dave

Eine wirklich andere Möglichkeit ist ein ordentliches OO - Design.

z.B. Bitten Sie das MyNode, alle Ebenen zurückzugeben.

So was:

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

    public IEnumerable<MyNode> GetAllNodes()
    {
        if (Elements == null)
        {
            return new List<MyNode>(); 
        }

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

Jetzt können Sie die oberste Ebene von MyNode auffordern, alle Knoten abzurufen.

var flatten = topNode.GetAllNodes();

Wenn Sie die Klasse nicht bearbeiten können, ist dies keine Option. Aber ansonsten denke ich, dass dies einer separaten (rekursiven) LINQ-Methode vorgezogen werden könnte.

Dies verwendet LINQ, also denke ich, dass diese Antwort hier anwendbar ist;)

1
Julian

Unten ist Ivan Stoevs Code mit der zusätzlichen Funktion, den Index jedes Objekts im Pfad zu ermitteln. Z.B. suche nach "Item_120":

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

würde das Element und ein int-Array [1,2,0] zurückgeben. Natürlich ist auch die Verschachtelungsebene als Länge des Arrays verfügbar.

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
lisz
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
Tom Brothers

Kombinieren Sie die Antwort von Dave und Ivan Stoev, falls Sie die Verschachtelungsebene und die Liste "in der richtigen Reihenfolge" und nicht umgekehrt wie in der Antwort von Konamiman benötigen.

 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
Corcus

Aufbauend auf der Antwort von Konamiman und der Bemerkung, dass die Reihenfolge unerwartet ist, ist hier eine Version mit einem expliziten Sortierparameter:

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

Und eine Beispielnutzung:

var flattened = doc.TraverseAndFlatten(x => x.DependentDocuments, y => y.Document.DocDated).ToList();
0
rothschild86