Recherche dans une arborescence à l'aide de LINQ

87

J'ai un arbre créé à partir de cette classe.

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

Je veux rechercher dans tous les enfants et tous leurs enfants pour obtenir ceux qui correspondent à une condition:

node.Key == SomeSpecialKey

Comment puis-je l'implémenter?

Ufuk Hacıoğulları
la source
Intéressant, je pense que vous pouvez accomplir cela en utilisant la fonction SelectMany.N'oubliez pas d'avoir à faire quelque chose de similaire il y a quelque temps.
Jethro

Réponses:

175

C'est une idée fausse que cela nécessite une récursivité. Il va avoir besoin d' une pile ou une file d' attente et le plus simple est de mettre en œuvre à l'aide de la récursivité. Par souci d'exhaustivité, je vais fournir une réponse non récursive.

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

Utilisez cette expression par exemple pour l'utiliser:

root.Descendants().Where(node => node.Key == SomeSpecialKey)
vidstige
la source
31
+1. Et cette méthode continuera à fonctionner lorsque l'arborescence est si profonde qu'une traversée récursive ferait exploser la pile d'appels et provoquerait un StackOverflowException.
LukeH
3
@LukeH Bien qu'il soit utile d'avoir des alternatives comme celle-ci pour ces situations, cela signifierait un très grand arbre. À moins que votre arbre ne soit très profond, les méthodes récursives sont normalement plus simples / plus lisibles.
ForbesLindesay
3
@Tuskan: L'utilisation d'itérateurs récursifs a également des implications sur les performances, voir la section "Le coût des itérateurs" de blogs.msdn.com/b/wesdyer/archive/2007/03/23/ ... (il est vrai que les arbres doivent encore être assez profonds pour ceci pour être perceptible). Et, fwiw, je trouve que la réponse de vidstige est tout aussi lisible que les réponses récursives ici.
LukeH
3
Ouais, ne choisis pas ma solution à cause des performances. La lisibilité est toujours la première, sauf si un goulot d'étranglement est prouvé. Bien que ma solution soit assez simple, je suppose que c'est une question de goût ... J'ai en fait publié ma réponse simplement en complément des réponses récursives, mais je suis heureux que les gens l'aient aimé.
vidstige
11
Je pense qu'il vaut la peine de mentionner que la solution présentée ci-dessus effectue une recherche en profondeur (dernier enfant en premier). Si vous vouliez une recherche en largeur (premier enfant d'abord), vous pouvez changer le type de la collection de nœuds en Queue<Node>(avec les modifications correspondantes de Enqueue/ Dequeuede Push/ Pop).
Andrew Coonce
16

Recherche dans une arborescence d'objets avec 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;
        }

    }
}
CD..
la source
1
+1 Résout le problème en général. L'article lié a fourni une excellente explication.
John Jesus
Pour être complet, vous avez besoin d'une vérification nulle des paramètres headet childrenFuncde diviser les méthodes en deux parties afin que la vérification des paramètres ne soit pas différée au temps de parcours.
ErikE
15

Si vous souhaitez conserver une syntaxe similaire à Linq, vous pouvez utiliser une méthode pour obtenir tous les descendants (enfants + enfants enfants, etc.)

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

Cet énumérable peut ensuite être interrogé comme tout autre en utilisant where ou first ou autre.

ForbesLindesay
la source
J'aime ça, propre! :)
vidstige
3

Vous pouvez essayer cette méthode d'extension pour énumérer les nœuds de l'arborescence:

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

Ensuite, utilisez cela avec une Where()clause:

var matchingNodes = rootNode.GetTreeNodes().Where(x => x.Key == SomeSpecialKey);
dlev
la source
2
Notez que cette technique est inefficace si l'arbre est profond et peut lever une exception si l'arbre est très profond.
Eric Lippert
1
@Eric Bon point. Et bon retour de vacances? (Il est difficile de dire quoi avec cette chose Internet couvrant le monde entier.)
dlev
2

Peut-être avez-vous juste besoin

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

Ou, si vous avez besoin de rechercher un niveau plus profond,

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

Si vous devez effectuer une recherche à tous les niveaux, procédez comme suit:

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)));
}
Vlad
la source
Est-ce que cela va chercher les enfants des enfants?
Jethro
Je pense que cela ne fonctionnera pas, car cela ne recherche qu'à un seul niveau de l'arbre et ne fait pas une traversée complète de l'arbre
lunactic
@Ufuk: la 1ère ligne ne fonctionne qu'à 1 niveau de profondeur, la seconde à seulement 2 niveaux de profondeur. Si vous avez besoin de rechercher à tous les niveaux, vous avez besoin d'une fonction récursive.
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;
        }
    }

Et puis vous pouvez rechercher comme:

    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");
Varun Chatterji
la source
Étant donné que l'entrée de Find est Func <Node, bool> myFunc, vous pouvez utiliser cette méthode pour filtrer par toute autre propriété que vous pourriez également définir dans Node. Par exemple, dans Node avait une propriété Name et vous vouliez trouver un nœud par nom, vous pouvez simplement passer p => p.Name == "Something"
Varun Chatterji
2

Pourquoi ne pas utiliser une IEnumerable<T>méthode d'extension

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

alors fais juste ça

var result = nodes.Children.SelectHierarchy(n => n.Children, n => n.Key.IndexOf(searchString) != -1);
Dean Chalk
la source
0

Il y a quelque temps, j'ai écrit un article sur le projet de code qui décrit comment utiliser Linq pour interroger des structures arborescentes:

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

Cela fournit une API de style linq-to-XML où vous pouvez rechercher des descendants, des enfants, des ancêtres, etc.

Probablement exagéré pour votre problème actuel, mais pourrait intéresser les autres.

Coline
la source
0

Vous pouvez utiliser cette méthode d'extension pour interroger l'arborescence.

    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;
    }
BitKFu
la source
0

J'ai une méthode d'extension générique qui peut aplatir n'importe lequel IEnumerable<T>et à partir de cette collection aplatie, vous pouvez obtenir le nœud que vous voulez.

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

Utilisez ceci comme ceci:

var q = from node in myTree.FlattenHierarchy(x => x.Children)
        where node.Key == "MyKey"
        select node;
var theNode = q.SingleOrDefault();
Mikael Östberg
la source
0

J'utilise les implémentations suivantes pour énumérer les éléments de l'arborescence

    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 dans l'implémentation ci-dessus utilise la file d'attente des séquences de nœuds au lieu de la file d'attente des nœuds. Ce n'est pas une manière classique d'algorithme BFS.

Valentine Zakharenko
la source
0

Et juste pour le plaisir (presque une décennie plus tard) une réponse utilisant également des génériques mais avec une boucle Stack et While, basée sur la réponse acceptée par @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);
        }
    }
}

Étant donné une collection, on peut utiliser comme ça

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

ou avec un objet racine

        var myNode = root.Descendants(x => x.Children).Where(x => x.Key == SomeKey);
George Albertyn
la source