LINQを使用してツリーを検索する


87

このクラスから作成されたツリーがあります。

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

すべての子供とそのすべての子供を検索して、条件に一致するものを取得したいと思います。

node.Key == SomeSpecialKey

どうすれば実装できますか?


興味深いことに、SelectMany関数を使用してこれを実現できると思います。しばらく前に、同様のことをしなければならなかったことを忘れないでください。
ジェスロ2011

回答:


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 2011

3
@LukeHこのような状況ではこのような代替手段があると便利ですが、それは非常に大きなツリーを意味します。ツリーが非常に深い場合を除いて、再帰メソッドは通常、より単純で読みやすくなります。
ForbesLindesay 2011

3
@Tuskan:再帰的イテレーターを使用すると、パフォーマンスにも影響があります。blogs.msdn.com/ b / wesdyer / archive / 2007/03/23 /の「イテレーターのコスト」セクションを参照してください(確かに、ツリーはまだかなり深い必要があります。これは目立つように)。そして、fwiw、私はvidstigeの答えがここでの再帰的な答えと同じくらい読みやすいと思います。
LukeH 2011

3
ええ、パフォーマンスのために私のソリューションを選択しないでください。ボトルネックが証明されない限り、読みやすさが常に最優先されます。私の解決策は非常に単純ですが、それは好みの問題だと思います...私は実際には再帰的な回答を補完するものとして自分の回答を投稿しましたが、人々がそれを気に入ってくれてうれしいです。
vidstige 2011

11
上記のソリューションは、(最後の子を優先する)深さ優先探索を実行することを言及する価値があると思います。(first-child-first)幅優先探索が必要な場合は、ノードコレクションのタイプをに変更できますQueue<Node>(対応する変更はEnqueue/DequeueからPush/になりますPop)。
Andrew Coonce 2013

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、メソッドを2つの部分に分割して、パラメーターチェックがトラバーサル時間に延期されないようにする必要があります。
ErikE 2015

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 2015

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 2011

1
@エリック良い点。そして休暇から戻って歓迎しますか?(世界中に広がるこのインターネットのことで何を知るのは難しいです。)
dlev 2011

2

たぶんあなたはただ必要です

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

または、1レベル深く検索する必要がある場合は、

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

それは子供たちの子供たちを検索しますか?
ジェスロ2011

これはツリー内の1つのレベルでのみ検索し、完全なツリートラバーサルを実行しないため、これは機能しないと思います
狂気の2011

@Ufuk:1行目は1レベルの深さでのみ機能し、2行目は2レベルの深さでのみ機能します。すべてのレベルで検索する必要がある場合は、再帰関数が必要です。
vlad 2011

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"
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を使用してツリーのような構造をクエリする方法を説明するcodeprojectの記事を書きました。

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年後)、ジェネリックを使用しているが、@ vidstigeによって受け入れられた回答に基づいて、スタックと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.