LINQ를 사용하여 트리 검색


87

이 클래스에서 만든 트리가 있습니다.

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

모든 자녀와 모든 자녀를 검색하여 조건과 일치하는 항목을 얻고 싶습니다.

node.Key == SomeSpecialKey

어떻게 구현할 수 있습니까?


흥미롭게도 SelectMany 함수를 사용하여이 작업을 수행 할 수 있다고 생각합니다. 얼마 전에 비슷한 작업을 수행해야했습니다.
이드로

답변:


176

이것은 재귀가 필요하다는 오해입니다. 그것은 것입니다 스택 또는 큐와 쉬운 방법을 필요로 재귀를 사용하여 구현하는 것입니다. 완전성을 위해 비재 귀적 답변을 제공하겠습니다.

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 2011-08-15

3
@Tuskan : 재귀 반복자를 사용하는 것도 성능에 영향을줍니다. blogs.msdn.com/b/wesdyer/archive/2007/03/23/…의 "반복기 비용"섹션을 참조하십시오. (분명히 트리는 여전히 상당히 깊어 야합니다. 이것은 눈에 띄게). 그리고 fwiw, vidstige의 답변은 여기의 재귀 답변만큼 읽기 쉽습니다.
LukeH

3
예, 성능 때문에 내 솔루션을 선택하지 마십시오. 병목 현상이 입증되지 않는 한 가독성이 항상 우선입니다. 내 솔루션은 매우 간단하지만 맛의 문제라고 생각합니다 ... 실제로는 재귀 답변에 대한 보완으로 내 답변을 게시했지만 사람들이 좋아해 기쁩니다.
vidstige '2011-08-15

11
위에 제시된 솔루션이 (마지막 자식 우선) 깊이 우선 검색을 수행한다는 점을 언급 할 가치가 있다고 생각합니다. (첫 번째 자식 우선) 너비 우선 검색을 원하면 노드 컬렉션의 유형을 Queue<Node>( Enqueue/ Dequeue에서 Push/에 해당하는 변경 사항과 함께)로 변경할 수 있습니다 Pop.
Andrew Coonce

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 일반적으로 문제를 해결합니다. 링크 된 기사는 훌륭한 설명을 제공했습니다.
존 예수

당신이 매개 변수에 널 (null) 검사를 필요로 완료하려면 headchildrenFunc매개 변수 검사가 통과 시간을 지연되지 않도록 두 부분으로 방법을 깰.
ErikE 2015-08-19

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
이 기술은 트리가 깊으면 비효율적이며 트리가 매우 깊으면 예외를 throw 할 수 있습니다.
Eric Lippert

1
@ 에릭 좋은 지적. 그리고 휴가에서 돌아온 것을 환영합니까? (전 세계에
퍼져

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

트리의 한 수준에서만 검색하고 완전한 트리 순회를 수행하지 않기 때문에 이것이 작동하지 않을 것이라고 생각합니다
lunactic

@Ufuk : 첫 번째 줄은 1 단계 만 작동하고 두 번째 줄은 2 단계 만 작동합니다. 모든 수준에서 검색 해야하는 경우 재귀 함수가 필요합니다.
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;
        }
    }

그런 다음 다음과 같이 검색 할 수 있습니다.

    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에서도 정의 할 수있는 다른 속성으로 필터링 할 수 있습니다. 예를 들어 노드의 이름 속성을 가지고 당신은 이름으로 노드를 찾기 위해, 당신은 단지 페이지에 전달할 수 원 => p.Name == "뭔가"
하기 Varun Chatterji

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

하위 항목, 하위 항목, 조상 등을 검색 할 수있는 linq-to-XML 스타일 API를 제공합니다.

현재 문제에 대해서는 과잉 일 가능성이 있지만 다른 사람들에게는 관심이있을 수 있습니다.


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

그리고 재미를 위해 (거의 10 년 후) Generics를 사용하지만 @vidstige가 수락 한 답변을 기반으로 Stack 및 While 루프를 사용하는 답변입니다.

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);
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.