Come appiattire l'albero tramite LINQ?


95

Quindi ho un albero semplice:

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

Ho un IEnumerable<MyNode>. Voglio ottenere un elenco di tutti MyNode(inclusi gli oggetti del nodo interno ( Elements)) come un unico elenco Where group == 1. Come fare una cosa del genere tramite LINQ?


1
In quale ordine vuoi che si trovi l'elenco appiattito?
Philip il

1
Quando i nodi smettono di avere nodi figli? Presumo sia quando Elementsè nullo o vuoto?
Adam Houldsworth

potrebbe essere duplicato con stackoverflow.com/questions/11827569/…
Tamir

Il modo più semplice / chiaro per risolvere questo problema consiste nell'usare una query LINQ ricorsiva. Questa domanda: stackoverflow.com/questions/732281/expressing-recursion-in-linq ha molte discussioni su questo, e questa particolare risposta spiega in dettaglio come implementarla.
Alvaro Rodriguez

Risposte:


137

Puoi appiattire un albero in questo modo:

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

È quindi possibile filtrare grouputilizzando Where(...).

Per guadagnare alcuni "punti per lo stile", converti Flattenin una funzione di estensione in una classe statica.

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

Per guadagnare più punti per "uno stile ancora migliore", converti Flattenin un metodo di estensione generico che accetta un albero e una funzione che produce discendenti da un nodo:

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

Chiama questa funzione in questo modo:

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

Se preferisci l'appiattimento in pre-ordine piuttosto che in post-ordine, cambia i lati del file Concat(...).


@AdamHouldsworth Grazie per la modifica! L'elemento nella chiamata a Concatdovrebbe essere new[] {e}, non new[] {c}(non si compilerebbe nemmeno con clì).
dasblinkenlight

Non sono d'accordo: compilato, testato e funzionante con c. L'utilizzo enon viene compilato. Puoi anche aggiungere if (e == null) return Enumerable.Empty<T>();per far fronte a elenchi di figli nulli.
Adam Houldsworth

1
più come `public static IEnumerable <T> Flatten <T> (this IEnumerable <T> source, Func <T, IEnumerable <T>> f) {if (source == null) return Enumerable.Empty <T> (); return source.SelectMany (c => f (c) .Flatten (f)). Concat (source); } `
myWallJSON

10
Si noti che questa soluzione è O (nh) dove n è il numero di elementi nell'albero eh è la profondità media dell'albero. Poiché h può essere compreso tra O (1) e O (n), questo è compreso tra un algoritmo O (n) e un O (n quadrato). Esistono algoritmi migliori.
Eric Lippert

1
Ho notato che la funzione non aggiungerà elementi all'elenco appiattito se l'elenco è di IEnumerable <baseType>. Puoi risolvere questo problema chiamando la funzione in questo modo: var res = tree.Flatten (node ​​=> node.Elements.OfType <DerivedType>)
Frank Horemans

125

Il problema con la risposta accettata è che è inefficiente se l'albero è profondo. Se l'albero è molto profondo, fa saltare la pila. Puoi risolvere il problema utilizzando uno stack esplicito:

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

Assumendo n nodi in un albero di altezza he un fattore di ramificazione considerevolmente inferiore a n, questo metodo è O (1) nello spazio stack, O (h) nello spazio heap e O (n) nel tempo. L'altro algoritmo fornito è O (h) nello stack, O (1) nell'heap e O (nh) nel tempo. Se il fattore di ramificazione è piccolo rispetto a n, h è compreso tra O (lg n) e O (n), il che illustra che l'algoritmo naïve può utilizzare una quantità pericolosa di stack e una grande quantità di tempo se h è vicino a n.

Ora che abbiamo un attraversamento, la tua query è semplice:

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

3
@johnnycardy: se avessi intenzione di discutere un punto, forse il codice non è ovviamente corretto. Cosa potrebbe renderlo più chiaramente corretto?
Eric Lippert

3
@ebramtharwat: corretto. Potresti invocare Traversetutti gli elementi. Oppure puoi modificare Traverseper prendere una sequenza e farla inserire tutti gli elementi della sequenza stack. Ricorda, stackè "elementi che non ho ancora attraversato". Oppure potresti creare una radice "fittizia" in cui la sequenza è i suoi figli, e quindi attraversare la radice fittizia.
Eric Lippert

2
Se lo fai foreach (var child in current.Elements.Reverse())otterrai un appiattimento più atteso. In particolare, i bambini appariranno nell'ordine in cui appaiono, anziché per primi nell'ultimo bambino. Questo non dovrebbe avere importanza nella maggior parte dei casi, ma nel mio caso avevo bisogno che l'appiattimento fosse in un ordine prevedibile e previsto.
Micah Zoltu

2
@MicahZoltu, potresti evitarlo .Reversescambiando il Stack<T>per unQueue<T>
Rubens Farias

2
@MicahZoltu Hai ragione riguardo all'ordine, ma il problema Reverseè che crea iteratori aggiuntivi, che è ciò che questo approccio intende evitare. @RubensFarias Sostituzione Queueper Stackrisultati nell'attraversamento in ampiezza.
Jack A.

25

Solo per completezza, ecco la combinazione delle risposte di dasblinkenlight ed Eric Lippert. Unità testata e tutto il resto. :-)

 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
Per evitare NullReferenceException var children = getChildren (current); if (children! = null) {foreach (var child in children) stack.Push (child); }
serg

2
Vorrei notare che anche se questo appiattisce l'elenco, lo restituisce nell'ordine inverso. L'ultimo elemento diventa il primo ecc.
Corcus

21

Aggiornare:

Per le persone interessate al livello di nidificazione (profondità). Uno degli aspetti positivi dell'implementazione esplicita dello stack dell'enumeratore è che in qualsiasi momento (e in particolare quando si restituisce l'elemento) stack.Countrappresenta la profondità di elaborazione corrente. Quindi, tenendo conto di questo e utilizzando le tuple di valori C # 7.0, possiamo semplicemente modificare la dichiarazione del metodo come segue:

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

e yielddichiarazione:

yield return (item, stack.Count);

Quindi possiamo implementare il metodo originale applicando semplice Selectsu quanto sopra:

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

Originale:

Sorprendentemente nessuno (nemmeno Eric) ha mostrato il port iterativo "naturale" di un DFT di preordine ricorsivo, quindi eccolo qui:

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

Presumo che cambi eogni volta che chiami elementSelectorper mantenere il pre-ordine: se l'ordine non fosse importante, potresti cambiare la funzione per elaborare tutto ciascuno euna volta avviato?
NetMage

@NetMage Volevo specificamente preordinare. Con piccole modifiche può gestire l'ordine postale. Ma il punto principale è che questo è Depth First Traversal . Per Breath Prima Traversal avrei usato Queue<T>. Comunque, l'idea qui è di mantenere un piccolo stack con enumeratori, molto simile a quello che sta accadendo nell'implementazione ricorsiva.
Ivan Stoev

@ IvanStoev stavo pensando che il codice sarebbe stato semplificato. Immagino che l'utilizzo di Stackporterebbe a un primo attraversamento in ampiezza a zig-zag.
NetMage

7

Ho riscontrato alcuni piccoli problemi con le risposte fornite qui:

  • Cosa succede se l'elenco iniziale di elementi è nullo?
  • Cosa succede se c'è un valore nullo nell'elenco dei figli?

Costruito sulle risposte precedenti e ha prodotto quanto segue:

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

E i test unitari:

[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

Nel caso in cui qualcun altro lo trovi, ma deve anche conoscere il livello dopo aver appiattito l'albero, questo si espande sulla combinazione di Konamiman di dasblinkenlight e soluzioni di 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

Un'altra opzione è quella di avere un design OO adeguato.

ad esempio, chiedi MyNodedi restituire tutto appiattito.

Come questo:

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

Ora potresti chiedere al MyNode di livello superiore di ottenere tutti i nodi.

var flatten = topNode.GetAllNodes();

Se non puoi modificare la classe, questa non è un'opzione. Ma per il resto, penso che questo potrebbe essere preferito a un metodo LINQ separato (ricorsivo).

Questo sta usando LINQ, quindi penso che questa risposta sia applicabile qui;)


Forse Enumerabl.Empty meglio del nuovo elenco?
Frank

1
Infatti! Aggiornato!
Julian,

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
l'utilizzo di un foreach nella propria estensione significa che non si tratta più di "esecuzione ritardata" (a meno che, ovviamente, non si utilizzi il rendimento del rendimento).
Tri Q Tran

0

Combinando la risposta di Dave e Ivan Stoev nel caso in cui sia necessario il livello di annidamento e l'elenco appiattito "in ordine" e non invertito come nella risposta data da 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();
            }
        }
    }

Sarebbe anche bello poter specificare prima la profondità o l'ampiezza ...
Hugh

0

Basandosi sulla risposta di Konamiman e sul commento che l'ordinamento è inaspettato, ecco una versione con un parametro di ordinamento esplicito:

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

E un esempio di utilizzo:

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

0

Di seguito è riportato il codice di Ivan Stoev con la caratteristica aggiuntiva di raccontare l'indice di ogni oggetto nel percorso. Ad esempio, cerca "Articolo_120":

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

restituirà l'elemento e un array int [1,2,0]. Ovviamente è disponibile anche il livello di annidamento, come lunghezza dell'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();
    }
}

Ciao, @lisz, dove incolli questo codice? Ricevo errori come "Il modificatore" pubblico "non è valido per questo articolo", "Il modificatore" statico "non è valido per questo articolo"
Kynao

0

Ecco alcune implementazioni pronte per l'uso utilizzando Queue e restituendo l'albero Flatten prima a me e poi ai miei figli.

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

Ogni tanto cerco di scalfire questo problema e di ideare la mia soluzione che supporti strutture arbitrariamente profonde (nessuna ricorsione), esegue il primo attraversamento in ampiezza e non abusa di troppe query LINQ o esegue preventivamente la ricorsione sui bambini. Dopo aver scavato nel codice sorgente .NET e provato molte soluzioni, ho finalmente trovato questa soluzione. Alla fine è stato molto vicino alla risposta di Ian Stoev (la cui risposta ho visto solo ora), tuttavia la mia non utilizza loop infiniti o ha un flusso di codice insolito.

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

Un esempio funzionante può essere trovato qui .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.