Поиск в дереве с использованием LINQ


87

У меня есть дерево, созданное из этого класса.

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

Я хочу найти всех детей и всех их детей, чтобы получить те, которые соответствуют условию:

node.Key == SomeSpecialKey

Как я могу это реализовать?


Интересно, я думаю, вы можете сделать это с помощью функции SelectMany. Помните, что некоторое время назад приходилось делать что-то подобное.
— Джетро

Ответы:


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. И этот метод будет продолжать работать, когда дерево настолько глубокое, что рекурсивный обход приведет к повреждению стека вызовов и вызову 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 Решает проблему в целом. Связанная статья предоставила прекрасное объяснение.
— John Jesus

Для завершения вам потребуется проверка параметров на null 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()));
    }
}

Затем это перечислимое число можно запросить, как и любое другое, с помощью where, first или чего угодно.


Мне это нравится, чисто! :)
— 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
Обратите внимание, что этот метод неэффективен, если дерево глубокое, и может вызвать исключение, если дерево очень глубокое.
— Эрик Липперт

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

Будет ли это искать детей в детях?
— Джетро

Я думаю, что это не сработает, так как это выполняет поиск только на одном уровне в дереве и не выполняет полный обход дерева
— лунактический

@Ufuk: первая строка работает только на 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, и вы хотели найти узел по имени, вы могли просто передать p => p.Name == "Something"
— Варун Чаттерджи

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 и 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.