Mencari pohon menggunakan LINQ


87

Saya memiliki pohon yang dibuat dari kelas ini.

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

Saya ingin mencari pada semua anak dan semua anaknya untuk mendapatkan yang cocok dengan kondisi:

node.Key == SomeSpecialKey

Bagaimana cara menerapkannya?


Menarik, saya pikir Anda dapat melakukannya dengan menggunakan fungsi SelectMany, Ingatlah harus melakukan sesuatu yang serupa beberapa waktu lalu.
Yitro

Jawaban:


176

Ini adalah kesalahpahaman bahwa ini membutuhkan rekursi. Ini akan membutuhkan tumpukan atau antrian dan cara termudah adalah menerapkannya menggunakan rekursi. Demi kelengkapan, saya akan memberikan jawaban non-rekursif.

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

Gunakan ekspresi ini misalnya untuk menggunakannya:

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

31
+1. Dan metode ini akan terus bekerja jika struktur pohon terlalu dalam sehingga traversal rekursif akan meledakkan tumpukan panggilan dan menyebabkan a StackOverflowException.
LukeH

3
@LukeH Meskipun berguna untuk memiliki alternatif seperti ini untuk situasi tersebut, itu berarti pohon yang sangat besar. Kecuali pohon Anda sangat dalam, metode rekursif biasanya lebih sederhana / lebih mudah dibaca.
ForbesLindesay

3
@Tuskan: Menggunakan iterator rekursif juga memiliki implikasi kinerja, lihat bagian "Biaya Iterator" di blogs.msdn.com/b/wesdyer/archive/2007/03/23/… (memang pohon masih perlu cukup dalam untuk ini agar terlihat). Dan, fwiw, saya menemukan jawaban vidstige sama mudahnya dengan jawaban rekursif di sini.
LukeH

3
Ya, jangan memilih solusi saya karena performa. Keterbacaan selalu menjadi yang pertama, kecuali jika terbukti menjadi hambatan. Meskipun solusi saya cukup mudah, jadi saya rasa ini masalah selera ... Saya sebenarnya memposting jawaban saya hanya sebagai pelengkap jawaban rekursif, tetapi saya senang orang menyukainya.
vidstige

11
Saya pikir perlu disebutkan bahwa solusi yang disajikan di atas melakukan pencarian mendalam-pertama (terakhir-anak-pertama). Jika Anda menginginkan pencarian (first-child-first) breadth-first, Anda dapat mengubah jenis kumpulan node menjadi Queue<Node>(dengan perubahan yang sesuai ke Enqueue/ Dequeuedari Push/ Pop).
Andrew Coonce

16

Mencari Pohon Objek dengan 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 Memecahkan masalah secara umum. Artikel terkait memberikan penjelasan yang bagus.
John Jesus

Untuk menyelesaikannya, Anda memerlukan pemeriksaan null pada parameter headdan childrenFuncmemecah metode menjadi dua bagian sehingga pemeriksaan parameter tidak ditangguhkan ke waktu traversal.
ErikE

15

Jika Anda ingin mempertahankan sintaks seperti Linq, Anda dapat menggunakan metode untuk mendapatkan semua turunan (anak + anak anak, dll.)

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

Enumerable ini kemudian dapat di-query seperti yang lain menggunakan where atau first atau apapun.


Saya suka ini, bersih! :)
vidstige

3

Anda dapat mencoba metode ekstensi ini untuk menghitung simpul pohon:

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

Kemudian gunakan itu dengan Where()klausa:

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

2
Perhatikan bahwa teknik ini tidak efisien jika pohonnya dalam dan dapat membuat pengecualian jika pohonnya sangat dalam.
Eric Lippert

1
@ Poin yang bagus. Dan selamat datang kembali dari liburan? (Sulit untuk mengatakan apa dengan internet yang mencakup dunia ini.)
dlev

2

Mungkin Anda hanya perlu

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

Atau, jika Anda perlu mencari satu tingkat lebih dalam,

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

Jika Anda perlu mencari di semua level, lakukan yang berikut:

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

Apakah itu akan menggeledah anak-anak?
Yitro

Saya pikir ini tidak akan berhasil, karena pencarian ini hanya pada satu tingkat di pohon dan tidak melakukan
penjelajahan

@Ufuk: baris pertama hanya bekerja sedalam 1 tingkat, yang kedua hanya sedalam 2 tingkat. Jika Anda perlu mencari di semua level, Anda memerlukan fungsi rekursif.
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;
        }
    }

Dan kemudian Anda dapat menelusuri seperti:

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

Karena input Find adalah Func <Node, bool> myFunc, Anda dapat menggunakan metode ini untuk memfilter oleh properti lain yang mungkin Anda tentukan di Node juga. Misalnya di Node memiliki properti Name dan Anda ingin menemukan Node by Name, Anda cukup memasukkan p => p.Name == "Something"
Varun Chatterji

2

Mengapa tidak menggunakan IEnumerable<T>metode ekstensi

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

lalu lakukan saja ini

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

0

Beberapa waktu yang lalu saya menulis artikel codeproject yang menjelaskan bagaimana menggunakan Linq untuk menanyakan struktur seperti pohon:

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

Ini menyediakan API gaya linq-ke-XML tempat Anda dapat mencari turunan, turunan, leluhur, dll ...

Mungkin berlebihan untuk masalah Anda saat ini, tetapi mungkin menarik bagi orang lain.


0

Anda dapat menggunakan metode ekstensi ini untuk menanyakan pohon.

    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

Saya memiliki metode ekstensi generik yang dapat meratakan apa saja IEnumerable<T>dan dari koleksi yang diratakan itu, Anda bisa mendapatkan simpul yang Anda inginkan.

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

Gunakan ini seperti ini:

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

0

Saya menggunakan implementasi berikut untuk menghitung item Pohon

    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 dalam implementasi di atas menggunakan antrian urutan node, bukan antrian node. Ini bukan cara algoritma BFS klasik.


0

Dan hanya untuk bersenang-senang (hampir satu dekade kemudian) jawaban juga menggunakan Generik tetapi dengan loop Stack dan While, berdasarkan jawaban yang diterima oleh @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);
        }
    }
}

Diberikan koleksi seseorang dapat menggunakan seperti ini

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

atau dengan objek root

        var myNode = root.Descendants(x => x.Children).Where(x => x.Key == SomeKey);
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.