Ricerca in un albero utilizzando LINQ


87

Ho un albero creato da questa classe.

class Node
{
    public string Key { get; }
    public List<Node> Children { get; }
}

Voglio cercare in tutti i bambini e in tutti i loro figli per ottenere quelli che corrispondono a una condizione:

node.Key == SomeSpecialKey

Come posso implementarlo?


Interessante, penso che tu possa farlo usando la funzione SelectMany, ricorda di dover fare qualcosa di simile qualche tempo fa.
Jethro

Risposte:


175

È sbagliato pensare che ciò richieda la ricorsione. Si avrà bisogno di un camino o una coda e il modo più semplice è quello di implementare utilizzando la ricorsione. Per completezza fornirò una risposta non ricorsiva.

static IEnumerable<Node> Descendants(this Node root)
{
    var nodes = new Stack<Node>(new[] {root});
    while (nodes.Any())
    {
        Node node = nodes.Pop();
        yield return node;
        foreach (var n in node.Children) nodes.Push(n);
    }
}

Usa questa espressione ad esempio per usarla:

root.Descendants().Where(node => node.Key == SomeSpecialKey)

31
+1. E questo metodo continuerà a funzionare quando l'albero è così profondo che un attraversamento ricorsivo farà saltare lo stack di chiamate e causerà un errore StackOverflowException.
LukeH

3
@LukeH Sebbene sia utile avere alternative come questa per quelle situazioni, ciò significherebbe un albero molto grande. A meno che l'albero non sia molto profondo, i metodi ricorsivi sono normalmente più semplici / più leggibili.
ForbesLindesay

3
@Tuskan: L'utilizzo di iteratori ricorsivi ha anche implicazioni sulle prestazioni, vedere la sezione "Il costo degli iteratori" di blogs.msdn.com/b/wesdyer/archive/2007/03/23/…vero che gli alberi devono ancora essere abbastanza profondi per questo per essere evidente). Inoltre, trovo che la risposta di vidstige sia leggibile tanto quanto le risposte ricorsive qui.
LukeH

3
Sì, non scegliere la mia soluzione a causa delle prestazioni. La leggibilità è sempre al primo posto, a meno che non sia dimostrato un collo di bottiglia. Sebbene la mia soluzione sia piuttosto semplice, quindi immagino sia una questione di gusti ... in realtà ho pubblicato la mia risposta semplicemente come complemento alle risposte ricorsive, ma sono contento che alla gente sia piaciuta.
vidstige

11
Penso che valga la pena ricordare che la soluzione sopra presentata esegue una ricerca approfondita (last-child-first). Se si desidera una ricerca in ampiezza (first-child-first), è possibile modificare il tipo di raccolta dei nodi in Queue<Node>(con le modifiche corrispondenti a Enqueue/ Dequeueda Push/ Pop).
Andrew Coonce

16

Alla ricerca di un albero di oggetti con Linq

public static class TreeToEnumerableEx
{
    public static IEnumerable<T> AsDepthFirstEnumerable<T>(this T head, Func<T, IEnumerable<T>> childrenFunc)
    {
        yield return head;

        foreach (var node in childrenFunc(head))
        {
            foreach (var child in AsDepthFirstEnumerable(node, childrenFunc))
            {
                yield return child;
            }
        }

    }

    public static IEnumerable<T> AsBreadthFirstEnumerable<T>(this T head, Func<T, IEnumerable<T>> childrenFunc)
    {
        yield return head;

        var last = head;
        foreach (var node in AsBreadthFirstEnumerable(head, childrenFunc))
        {
            foreach (var child in childrenFunc(node))
            {
                yield return child;
                last = child;
            }
            if (last.Equals(node)) yield break;
        }

    }
}

1
+1 Risolve il problema in generale. L'articolo collegato ha fornito un'ottima spiegazione.
John Jesus

Per essere completo, è necessario il controllo null sui parametri heade childrenFuncsuddividere i metodi in due parti in modo che il controllo dei parametri non sia rimandato al tempo di attraversamento.
ErikE

15

Se vuoi mantenere la sintassi simile a Linq, puoi usare un metodo per ottenere tutti i discendenti (bambini + figli dei bambini ecc.)

static class NodeExtensions
{
    public static IEnumerable<Node> Descendants(this Node node)
    {
        return node.Children.Concat(node.Children.SelectMany(n => n.Descendants()));
    }
}

Questo enumerabile può quindi essere interrogato come qualsiasi altro usando where o first o qualunque cosa.


Mi piace, pulito! :)
vidstige

3

Puoi provare questo metodo di estensione per enumerare i nodi dell'albero:

static IEnumerable<Node> GetTreeNodes(this Node rootNode)
{
    yield return rootNode;
    foreach (var childNode in rootNode.Children)
    {
        foreach (var child in childNode.GetTreeNodes())
            yield return child;
    }
}

Quindi usalo con una Where()clausola:

var matchingNodes = rootNode.GetTreeNodes().Where(x => x.Key == SomeSpecialKey);

2
Nota che questa tecnica è inefficiente se l'albero è profondo e può generare un'eccezione se l'albero è molto profondo.
Eric Lippert

1
@ Eric buon punto. E bentornato dalle vacanze? (È difficile dire cosa con questa cosa di Internet in tutto il mondo.)
dlev

2

Forse ti serve solo

node.Children.Where(child => child.Key == SomeSpecialKey)

Oppure, se devi cercare un livello più in profondità,

node.Children.SelectMany(
        child => child.Children.Where(child => child.Key == SomeSpecialKey))

Se devi cercare a tutti i livelli, procedi come segue:

IEnumerable<Node> FlattenAndFilter(Node source)
{
    List<Node> l = new List();
    if (source.Key == SomeSpecialKey)
        l.Add(source);
    return
        l.Concat(source.Children.SelectMany(child => FlattenAndFilter(child)));
}

Quello perquisirà i bambini dei bambini?
Jethro

Penso che questo non funzionerà, poiché cerca solo su un livello nell'albero e non esegue un attraversamento completo dell'albero
lunattico

@Ufuk: la prima riga funziona solo a 1 livello di profondità, la seconda a soli 2 livelli. Se devi cercare a tutti i livelli, hai bisogno di una funzione ricorsiva.
Vlad

2
public class Node
    {
        string key;
        List<Node> children;

        public Node(string key)
        {
            this.key = key;
            children = new List<Node>();
        }

        public string Key { get { return key; } }
        public List<Node> Children { get { return children; } }

        public Node Find(Func<Node, bool> myFunc)
        {
            foreach (Node node in Children)
            {
                if (myFunc(node))
                {
                    return node;
                }
                else 
                {
                    Node test = node.Find(myFunc);
                    if (test != null)
                        return test;
                }
            }

            return null;
        }
    }

E poi puoi cercare come:

    Node root = new Node("root");
    Node child1 = new Node("child1");
    Node child2 = new Node("child2");
    Node child3 = new Node("child3");
    Node child4 = new Node("child4");
    Node child5 = new Node("child5");
    Node child6 = new Node("child6");
    root.Children.Add(child1);
    root.Children.Add(child2);
    child1.Children.Add(child3);
    child2.Children.Add(child4);
    child4.Children.Add(child5);
    child5.Children.Add(child6);

    Node test = root.Find(p => p.Key == "child6");

Poiché l'input di Find è Func <Node, bool> myFunc, puoi utilizzare questo metodo per filtrare in base a qualsiasi altra proprietà che potresti definire anche in Node. Ad esempio, in Node era presente una proprietà Name e si desiderava trovare un Node by Name, è possibile semplicemente passare p => p.Name == "Something"
Varun Chatterji

2

Perché non utilizzare un IEnumerable<T>metodo di estensione

public static IEnumerable<TResult> SelectHierarchy<TResult>(this IEnumerable<TResult> source, Func<TResult, IEnumerable<TResult>> collectionSelector, Func<TResult, bool> predicate)
{
    if (source == null)
    {
        yield break;
    }
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
        var childResults = SelectHierarchy(collectionSelector(item), collectionSelector, predicate);
        foreach (var childItem in childResults)
        {
            yield return childItem;
        }
    }
}

allora fallo e basta

var result = nodes.Children.SelectHierarchy(n => n.Children, n => n.Key.IndexOf(searchString) != -1);

0

Qualche tempo fa ho scritto un articolo sul progetto di codice che descrive come utilizzare Linq per interrogare strutture ad albero:

http://www.codeproject.com/KB/linq/LinqToTree.aspx

Questo fornisce un'API in stile linq-to-XML in cui è possibile cercare discendenti, figli, antenati ecc ...

Probabilmente eccessivo per il tuo problema attuale, ma potrebbe interessare gli altri.


0

È possibile utilizzare questo metodo di estensione per interrogare l'albero.

    public static IEnumerable<Node> InTree(this Node treeNode)
    {
        yield return treeNode;

        foreach (var childNode in treeNode.Children)
            foreach (var flattendChild in InTree(childNode))
                yield return flattendChild;
    }

0

Ho un metodo di estensione generico che può appiattire qualsiasi IEnumerable<T>e da quella raccolta appiattita, puoi ottenere il nodo che desideri.

public static IEnumerable<T> FlattenHierarchy<T>(this T node, Func<T, IEnumerable<T>> getChildEnumerator)
{
    yield return node;
    if (getChildEnumerator(node) != null)
    {
        foreach (var child in getChildEnumerator(node))
        {
            foreach (var childOrDescendant in child.FlattenHierarchy(getChildEnumerator))
            {
                yield return childOrDescendant;
            }
        }
    }
}

Usa questo in questo modo:

var q = from node in myTree.FlattenHierarchy(x => x.Children)
        where node.Key == "MyKey"
        select node;
var theNode = q.SingleOrDefault();

0

Uso le seguenti implementazioni per enumerare gli elementi Tree

    public static IEnumerable<Node> DepthFirstUnfold(this Node root) =>
        ObjectAsEnumerable(root).Concat(root.Children.SelectMany(DepthFirstUnfold));

    public static IEnumerable<Node> BreadthFirstUnfold(this Node root) {
        var queue = new Queue<IEnumerable<Node>>();
        queue.Enqueue(ObjectAsEnumerable(root));

        while (queue.Count != 0)
            foreach (var node in queue.Dequeue()) {
                yield return node;
                queue.Enqueue(node.Children);
            }
    }

    private static IEnumerable<T> ObjectAsEnumerable<T>(T obj) {
        yield return obj;
    }

BreadthFirstUnfold nell'implementazione precedente utilizza la coda delle sequenze di nodi invece della coda dei nodi. Questo non è il classico metodo dell'algoritmo BFS.


0

E solo per divertimento (quasi un decennio dopo) una risposta usando anche Generics ma con un ciclo Stack e While, basata sulla risposta accettata da @vidstige.

public static class TypeExtentions
{

    public static IEnumerable<T> Descendants<T>(this T root, Func<T, IEnumerable<T>> selector)
    {
        var nodes = new Stack<T>(new[] { root });
        while (nodes.Any())
        {
            T node = nodes.Pop();
            yield return node;
            foreach (var n in selector(node)) nodes.Push(n);
        }
    }

    public static IEnumerable<T> Descendants<T>(this IEnumerable<T> encounter, Func<T, IEnumerable<T>> selector)
    {
        var nodes = new Stack<T>(encounter);
        while (nodes.Any())
        {
            T node = nodes.Pop();
            yield return node;
            if (selector(node) != null)
                foreach (var n in selector(node))
                    nodes.Push(n);
        }
    }
}

Data una collezione si può usare in questo modo

        var myNode = ListNodes.Descendants(x => x.Children).Where(x => x.Key == SomeKey);

o con un oggetto radice

        var myNode = root.Descendants(x => x.Children).Where(x => x.Key == SomeKey);
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.