Wyszukiwanie drzewa za pomocą LINQ


87

Mam drzewo utworzone z tej klasy.

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

Chcę przeszukać wszystkie dzieci i wszystkie ich dzieci, aby uzyskać te pasujące do warunku:

node.Key == SomeSpecialKey

Jak mogę to zaimplementować?


Ciekawe, myślę, że możesz to osiągnąć za pomocą funkcji SelectMany. Pamiętaj, że jakiś czas temu musiałem zrobić coś podobnego.
Jethro

Odpowiedzi:


175

To błędne przekonanie, że wymaga to rekurencji. To będzie wymagać stos lub kolejkę i najprostszym sposobem jest wykonanie go za pomocą rekursji. Ze względu na kompletność podam nierekurencyjną odpowiedź.

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

Użyj tego wyrażenia na przykład, aby go użyć:

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

31
+1. Ta metoda będzie nadal działać, gdy drzewo jest tak głębokie, że rekurencyjne przechodzenie zepsułoby stos wywołań i spowodowałoby StackOverflowException.
LukeH

3
@LukeH Chociaż warto mieć takie alternatywy w takich sytuacjach, oznaczałoby to bardzo duże drzewo. O ile drzewo nie jest bardzo głębokie, metody rekurencyjne są zwykle prostsze / bardziej czytelne.
ForbesLindesay

3
@Tuskan: Używanie iteratorów rekurencyjnych ma również wpływ na wydajność, zobacz sekcję „Koszt iteratorów” na blogs.msdn.com/b/wesdyer/archive/2007/03/23/ ( trzeba przyznać, że drzewa nadal muszą być dość głębokie aby było to zauważalne). I fwiw, uważam, że odpowiedź vidstige jest tak samo czytelna, jak rekurencyjne odpowiedzi tutaj.
LukeH

3
Tak, nie wybieraj mojego rozwiązania ze względu na wydajność. Czytelność jest zawsze najważniejsza, chyba że udowodniono wąskie gardło. Chociaż moje rozwiązanie jest dość proste, więc wydaje mi się, że to kwestia gustu ... Właściwie zamieściłem moją odpowiedź jedynie jako uzupełnienie odpowiedzi rekurencyjnych, ale cieszę się, że ludzie ją polubili.
vidstige

11
Myślę, że warto wspomnieć, że powyższe rozwiązanie wykonuje wyszukiwanie w głębi (najpierw ostatnie dziecko). Jeśli chcesz przeszukać najpierw wszerz (pierwsze dziecko) wszerz, możesz zmienić typ kolekcji węzłów na Queue<Node>(z odpowiednimi zmianami do Enqueue/ Dequeuez Push/ Pop).
Andrew Coonce,

16

Przeszukiwanie drzewa obiektów za pomocą 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 Ogólnie rozwiązuje problem. Połączony artykuł dostarczył świetnego wyjaśnienia.
Jan Jezus

Aby być kompletnym, musisz sprawdzić parametry zerowe headi childrenFuncpodzielić metody na dwie części, aby sprawdzanie parametrów nie zostało odroczone do czasu przejścia.
ErikE,

15

Jeśli chcesz zachować składnię podobną do Linq, możesz użyć metody do uzyskania wszystkich potomków (dzieci + dzieci dzieci itp.)

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

Ten wyliczalny można następnie przeszukiwać jak każdy inny, używając gdzie lub najpierw lub cokolwiek.


Podoba mi się, czyste! :)
vidstige

3

Możesz wypróbować tę metodę rozszerzenia, aby wyliczyć węzły drzewa:

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

Następnie użyj tego z Where()klauzulą:

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

2
Zauważ, że ta technika jest nieefektywna, jeśli drzewo jest głębokie i może zgłosić wyjątek, jeśli drzewo jest bardzo głębokie.
Eric Lippert

1
@Eric Słuszna uwaga. Witamy z powrotem z wakacji? (Trudno powiedzieć, o co chodzi z tym internetem obejmującym cały świat)
dlev

2

Być może potrzebujesz tylko

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

Jeśli chcesz przeszukać jeden poziom głębiej,

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

Jeśli chcesz wyszukiwać na wszystkich poziomach, wykonaj następujące czynności:

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

Czy to przeszuka dzieci, dzieci?
Jethro

Myślę, że to nie zadziała, ponieważ to wyszukuje tylko na jednym poziomie w drzewie i nie wykonuje pełnego przeszukiwania drzewa
lunactic

@Ufuk: pierwsza linia działa tylko 1 poziom, druga tylko 2 poziomy. Jeśli chcesz wyszukiwać na wszystkich poziomach, potrzebujesz funkcji rekurencyjnej.
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;
        }
    }

Następnie możesz wyszukiwać:

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

Ponieważ dane wejściowe Find to Func <Node, bool> myFunc, możesz użyć tej metody do filtrowania według dowolnej innej właściwości, którą możesz również zdefiniować w Node. Na przykład w Node miał właściwość Name i chciałeś znaleźć węzeł według nazwy, możesz po prostu przekazać p => p.Name == "Coś"
Varun Chatterji

2

Dlaczego nie skorzystać z IEnumerable<T>metody rozszerzenia

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

to po prostu zrób to

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

0

Jakiś czas temu napisałem artykuł dotyczący codeproject, w którym opisano, jak używać Linq do wykonywania zapytań w strukturach drzewiastych:

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

Zapewnia to interfejs API w stylu linq-to-XML, w którym można wyszukiwać potomków, dzieci, przodków itp.

Prawdopodobnie przesada jak na twój obecny problem, ale może zainteresować innych.


0

Możesz użyć tej metody rozszerzenia, aby zapytać o drzewo.

    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

Mam ogólną metodę rozszerzenia, która może spłaszczyć dowolną IEnumerable<T>iz tej spłaszczonej kolekcji, możesz uzyskać żądany węzeł.

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

Użyj tego w ten sposób:

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

0

Używam następujących implementacji do wyliczania elementów 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 w powyższej implementacji używa kolejki sekwencji węzłów zamiast kolejki węzłów. To nie jest klasyczny sposób algorytmu BFS.


0

I dla zabawy (prawie dziesięć lat później) odpowiedź również przy użyciu Generics, ale z pętlą Stack and While, na podstawie zaakceptowanej odpowiedzi przez @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);
        }
    }
}

Biorąc pod uwagę kolekcję, można z niej korzystać

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

lub z obiektem root

        var myNode = root.Descendants(x => x.Children).Where(x => x.Key == SomeKey);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.