Pesquisando uma árvore usando LINQ

87

Eu tenho uma árvore criada a partir desta classe.

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

Quero pesquisar em todas as crianças e em todos os seus filhos para encontrar aqueles que correspondem a uma condição:

node.Key == SomeSpecialKey

Como posso implementá-lo?

Ufuk Hacıoğulları
fonte
Interessante, acho que você pode fazer isso usando a função SelectMany. Lembre-se de ter feito algo semelhante há algum tempo.
Jethro de

Respostas:

175

É um equívoco pensar que isso requer recursão. Ele vai exigir uma pilha ou uma fila ea maneira mais fácil é para implementá-lo usando recursão. Para ser mais completo, vou fornecer uma resposta não recursiva.

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

Use esta expressão por exemplo para usá-lo:

root.Descendants().Where(node => node.Key == SomeSpecialKey)
vidstige
fonte
31
+1. E esse método continuará a funcionar quando a árvore estiver tão profunda que uma travessia recursiva explodiria a pilha de chamadas e causaria a StackOverflowException.
LukeH
3
@LukeH Embora seja útil ter alternativas como essa para essas situações, isso significaria uma árvore muito grande. A menos que sua árvore seja muito profunda, os métodos recursivos são normalmente mais simples / mais legíveis.
ForbesLindesay
3
@Tuskan: Usar iteradores recursivos também tem implicações de desempenho, consulte a seção "The Cost of Iterators" de blogs.msdn.com/b/wesdyer/archive/2007/03/23/… (reconhecidamente as árvores ainda precisam ser bastante profundas para isso seja perceptível). E, fwiw, acho a resposta de vidstige tão legível quanto as respostas recursivas aqui.
LukeH
3
Sim, não escolha minha solução por causa do desempenho. A legibilidade está sempre em primeiro lugar, a menos que seja comprovado um gargalo. Embora minha solução seja bastante direta, então acho que é uma questão de gosto ... Na verdade, postei minha resposta apenas como um complemento às respostas recursivas, mas estou feliz que as pessoas gostaram.
vidstige de
11
Acho que vale a pena mencionar que a solução apresentada acima realiza uma pesquisa em profundidade (primeiro filho). Se você quiser uma pesquisa em amplitude (primeiro filho primeiro), pode alterar o tipo da coleção de nós para Queue<Node>(com as alterações correspondentes para Enqueue/ Dequeuede Push/ Pop).
Andrew Coonce
16

Pesquisando uma árvore de objetos com o 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..
fonte
1
+1 Resolve o problema em geral. O artigo vinculado forneceu uma ótima explicação.
João Jesus
Para ser concluído, você precisa de verificação nula nos parâmetros heade childrenFuncde dividir os métodos em duas partes, para que a verificação de parâmetro não seja adiada para o tempo de passagem.
ErikE
15

Se você deseja manter a sintaxe do Linq, você pode usar um método para obter todos os descendentes (filhos + filhos dos filhos etc.)

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

Este enumerável pode então ser consultado como qualquer outro usando where ou first ou o que quer que seja.

ForbesLindesay
fonte
Eu gosto disso, limpo! :)
vidstige
3

Você pode tentar este método de extensão para enumerar os nós da árvore:

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

Em seguida, use isso com uma Where()cláusula:

var matchingNodes = rootNode.GetTreeNodes().Where(x => x.Key == SomeSpecialKey);
dlev
fonte
2
Observe que essa técnica é ineficiente se a árvore for profunda e pode lançar uma exceção se a árvore for muito profunda.
Eric Lippert
1
@Eric Bom ponto. E bem-vindo de volta das férias? (É difícil dizer o que com essa coisa da Internet se
espalhando
2

Talvez você precise apenas

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

Ou, se você precisar pesquisar um nível mais profundo,

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

Se você precisar pesquisar em todos os níveis, faça o seguinte:

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
fonte
Isso vai pesquisar as crianças, as crianças?
Jethro de
Acho que não vai funcionar, pois pesquisa apenas em um nível da árvore e não faz uma travessia completa da árvore
lunático
@Ufuk: a 1ª linha funciona com apenas 1 nível de profundidade, a segunda com apenas 2 níveis de profundidade. Se você precisa procurar em todos os níveis, você precisa de uma função recursiva.
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 então você pode pesquisar como:

    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
fonte
Como a entrada de Find é Func <Node, bool> myFunc, você pode usar esse método para filtrar por qualquer outra propriedade que possa definir em Node também. Por exemplo, em Node tinha uma propriedade Name e você queria encontrar um Node por Nome, você poderia simplesmente passar p => p.Name == "Something"
Varun Chatterji
2

Por que não usar um IEnumerable<T>método de extensão

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

então apenas faça isso

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

Há algum tempo, escrevi um artigo de projeto de código que descreve como usar o Linq para consultar estruturas semelhantes a árvores:

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

Isso fornece uma API de estilo linq-to-XML onde você pode pesquisar descendentes, filhos, ancestrais etc ...

Provavelmente um exagero para o seu problema atual, mas pode ser do interesse de outras pessoas.

ColinE
fonte
0

Você pode usar este método de extensão para consultar a árvore.

    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
fonte
0

Eu tenho um método de extensão genérico que pode nivelar qualquer IEnumerable<T>e dessa coleção nivelada, você pode obter o nó que deseja.

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

Use assim:

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

Eu uso as seguintes implementações para enumerar itens da Árvore

    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 na implementação acima usa a fila de sequências de nós em vez da fila de nós. Este não é o método clássico de algoritmo BFS.

Valentine Zakharenko
fonte
0

E apenas por diversão (quase uma década depois), uma resposta também usando Genéricos, mas com um loop Stack e While, baseado na resposta aceita por @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);
        }
    }
}

Dada uma coleção, pode-se usar assim

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

ou com um objeto raiz

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