Пошук дерева за допомогою LINQ


87

У мене є дерево, створене з цього класу.

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

Я хочу шукати всіх дітей та всіх їхніх дітей, щоб отримати тих, хто відповідає умові:

node.Key == SomeSpecialKey

Як я можу це реалізувати?


Цікаво, я думаю, ви можете досягти цього за допомогою функції SelectMany. Пам’ятайте, що щось подібне потрібно робити ще деякий час тому.
Jethro

Відповіді:


175

Помилкова думка, що для цього потрібна рекурсія. Для цього знадобиться стек або черга, і найпростіший спосіб - це реалізувати за допомогою рекурсії. Для повноти надам нерекурсивну відповідь.

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

Використовуйте, наприклад, цей вираз:

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

31
+1. І цей метод буде продовжувати працювати, коли дерево настільки глибоке, що рекурсивний обхід розірве стек викликів і спричинить a StackOverflowException.
LukeH

3
@LukeH Хоча корисно мати такі альтернативи для таких ситуацій, це означало б дуже велике дерево. Якщо ви не дуже глибоке дерево, рекурсивні методи, як правило, простіші та читабельніші.
ForbesLindesay

3
@Tuskan: Використання рекурсивних ітераторів також впливає на продуктивність, див. Розділ "Вартість ітераторів" на blogs.msdn.com/b/wesdyer/archive/2007/03/23/… (правда, дерева все ще повинні бути досить глибокими для це буде помітно). І, до речі, я вважаю, що відповідь vidstige така ж читабельна, як і рекурсивні відповіді тут.
LukeH

3
Так, не вибирайте моє рішення через продуктивність. Читаність завжди на першому місці, якщо не доведено вузьке місце. Хоча моє рішення досить прямолінійне, тому, мабуть, це справа смаку ... Я насправді опублікував свою відповідь лише як доповнення до рекурсивних відповідей, але я радий, що людям сподобалось.
vidstige

11
Думаю, варто згадати, що представлене вище рішення виконує пошук (для першої дитини) глибокого пошуку. Якщо ви хотіли здійснити пошук (для першої дитини), ви можете змінити тип колекції вузлів на Queue<Node>(із відповідними змінами в Enqueue/ Dequeueз Push/ Pop).
Ендрю Кунс,

16

Пошук дерева об’єктів за допомогою 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 Вирішує проблему загалом. Пов’язана стаття дала чудове пояснення.
Іван Ісус

Для завершення вам потрібна нульова перевірка параметрів headі childrenFuncрозбиття методів на дві частини, щоб перевірка параметрів не відкладалася на час обходу.
ErikE

15

Якщо ви хочете підтримувати Linq як синтаксис, ви можете використовувати метод для отримання всіх нащадків (діти + діти дітей тощо)

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

Потім цей перелік можна запитувати, як і будь-який інший, використовуючи де чи перший або що завгодно.


Мені це подобається, чисто! :)
vidstige

3

Ви можете спробувати цей метод розширення для перерахування деревних вузлів:

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

Тоді використовуйте це з Where()реченням:

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

2
Зверніть увагу, що ця техніка неефективна, якщо дерево глибоке, і може створити виняток, якщо дерево дуже глибоке.
Eric Lippert

1
@Eric Хороший момент. І ласкаво просимо з відпустки? (Важко сказати, що з цим Інтернетом, що охоплює земну кулю.)
dlev

2

Можливо, вам потрібно просто

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

Або, якщо вам потрібно шукати на один рівень глибше,

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

Якщо вам потрібно шукати на всіх рівнях, виконайте наступне:

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

Чи буде це проводити пошук у дітей?
Jethro

Я думаю, це не спрацює, оскільки пошук здійснюється лише на одному рівні в дереві і не робить повного обходу дерева
безумство

@Ufuk: 1-й рядок працює лише на 1 рівень, другий - лише на 2 рівні. Якщо вам потрібно шукати на всіх рівнях, вам потрібна рекурсивна функція.
Влад

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

І тоді ви можете шукати, як:

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

Оскільки вхідними даними Find є Func <Node, bool> myFunc, ви можете використовувати цей метод для фільтрації за будь-якою іншою властивістю, яку ви також можете визначити в Node. Наприклад, у Node була властивість Name, і ви хотіли знайти Node по імені, ви можете просто передати p => p.Name == "Щось"
Варун Чаттерджі

2

Чому б не використовувати IEnumerable<T>метод розширення

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

тоді просто зробіть це

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

0

Деякий час тому я написав статтю про кодовий проект, в якій описується, як використовувати Linq для запитів деревоподібних структур:

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

Це забезпечує API стилю linq-to-XML, де ви можете шукати нащадків, дітей, предків тощо ...

Можливо, надмірна для вашої поточної проблеми, але може зацікавити інших.


0

Ви можете використовувати цей метод розширення для запиту дерева.

    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

У мене є загальний метод розширення, який може згладити будь-який, IEnumerable<T>і з цієї сплощеної колекції ви можете отримати потрібний вузол.

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

Використовуйте це так:

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

0

Я використовую наступні реалізації для перерахування елементів дерева

    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 у реалізації вище використовує чергу послідовностей вузлів замість черги вузлів. Це не класичний спосіб алгоритму BFS.


0

І просто для розваги (майже через десять років) відповідь, що також використовує Generics, але з циклом Stack and While, заснована на прийнятій відповіді @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);
        }
    }
}

Отримавши колекцію, можна користуватися нею так

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

або з кореневим об’єктом

        var myNode = root.Descendants(x => x.Children).Where(x => x.Key == SomeKey);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.