Поиск в дереве с использованием 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)
Vidstige
источник
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;
        }

    }
}
CD..
источник
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 или чего угодно.

ForbesLindesay
источник
Мне это нравится, чисто! :)
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);
dlev
источник
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;
    }
BitKFu
источник
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);
Джордж Альбертин
источник