LINQ kullanarak bir ağaç arama


87

Bu sınıftan oluşturulmuş bir ağacım var.

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

Bir koşulla eşleşenleri bulmak için tüm çocukları ve tüm çocuklarını aramak istiyorum:

node.Key == SomeSpecialKey

Nasıl uygulayabilirim?


İlginç, bunu SelectMany işlevini kullanarak başarabileceğinizi düşünüyorum. Bir süre önce benzer bir şey yapmanız gerektiğini unutmayın.
Jethro

Yanıtlar:


176

Bunun yineleme gerektirdiği yanılgısıdır. Bu olacak bir yığın ya da kuyruk ve en kolay yolu gerektiren özyineleme kullanarak bunu uygulamaktır. Tamlık adına yinelemeli olmayan bir cevap vereceğim.

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

Örneğin kullanmak için bu ifadeyi kullanın:

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

31
+1. Ve bu yöntem, ağaç o kadar derin olduğunda çalışmaya devam eder ki, yinelemeli bir geçiş çağrı yığınını patlatır ve a neden olur StackOverflowException.
LukeH

3
@LukeH Bu durumlar için bunun gibi alternatiflerin olması faydalı olsa da, bu çok büyük bir ağaç anlamına gelir. Ağaç çok derin olmadıkça, yinelemeli yöntemler normalde daha basit / daha okunabilirdir.
ForbesLindesay

3
@Tuskan: Yinelemeli yinelemeleri kullanmanın performansla ilgili sonuçları da vardır, blogs.msdn.com/b/wesdyer/archive/2007/03/23/… 'nin " Yinelemelerin Maliyeti" bölümüne bakın (kuşkusuz ağaçların hala oldukça derin olması gerekir. bu farkedilebilir). Ve fwiw, vidstige'nin cevabını, buradaki yinelemeli cevaplar kadar okunabilir buluyorum.
LukeH

3
Evet, performansım yüzünden çözümümü seçmeyin. Bir darboğaz kanıtlanmadığı sürece okunabilirlik her zaman ilk sıradadır. Çözümüm oldukça basit olmasına rağmen, sanırım bu bir zevk meselesi ... Aslında cevabımı sadece yinelemeli cevapların bir tamamlayıcısı olarak gönderdim, ama insanların beğenmesine sevindim.
vidstige

11
Yukarıda sunulan çözümün (son çocuk önce) önce derinlemesine arama yaptığından bahsetmeye değer olduğunu düşünüyorum. Bir (ilk çocuk ilk) genişliği birinci aramayı istedik varsa ayrıca düğümler koleksiyonu türünü değiştirebilirsiniz Queue<Node>(karşılık gelen değişikliklerle Enqueue/ Dequeueden Push/ ' Pop).
Andrew Coonce

16

Linq ile Nesne Ağacı Arama

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 Genel olarak sorunu çözer. Bağlantılı makale harika bir açıklama sağladı.
John Jesus

Tamamlamak için, parametreler üzerinde sıfır denetimine ihtiyacınız vardır headve childrenFuncyöntemleri iki bölüme ayırmanız gerekir, böylece parametre denetimi geçiş süresine ertelenmez.
ErikE

15

Linq'i sözdizimi gibi sürdürmek istiyorsanız, tüm torunları (çocuklar + çocuklar vb.) Elde etmek için bir yöntem kullanabilirsiniz.

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

Bu numaralandırılabilir daha sonra herhangi bir başkası gibi nerede veya ilk veya her neyse kullanılarak sorgulanabilir.


Bunu beğendim, temiz! :)
vidstige

3

Ağaç düğümlerini numaralandırmak için bu uzantı yöntemini deneyebilirsiniz:

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

Sonra bunu bir Where()cümle ile kullanın :

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

2
Ağaç derinse bu tekniğin verimsiz olduğunu ve ağaç çok derinse bir istisna oluşturabileceğini unutmayın.
Eric Lippert

1
@Eric İyi nokta. Ve tatilden tekrar hoş geldiniz? (
Dünyayı

2

Belki de ihtiyacın var

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

Veya bir seviye daha derinlemesine aramanız gerekirse,

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

Tüm seviyelerde arama yapmanız gerekiyorsa, aşağıdakileri yapın:

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

Bu, çocukların çocuklarını arar mı?
Jethro

Bunun işe yaramayacağını düşünüyorum, çünkü bu ağaçta yalnızca bir seviyede arama yapıyor ve tam bir ağaç geçişi
yapmıyor

@Ufuk: 1. hat sadece 1 seviye derinlikte, ikincisi ise sadece 2 seviye derinlikte çalışıyor. Tüm düzeylerde arama yapmanız gerekiyorsa, özyinelemeli bir işleve ihtiyacınız vardır.
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;
        }
    }

Ve sonra şu şekilde arama yapabilirsiniz:

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

Bul'un girdisi Func <Node, bool> myFunc olduğundan, bu yöntemi Node'da tanımlayabileceğiniz diğer herhangi bir özelliğe göre filtrelemek için de kullanabilirsiniz. Örneğin Düğümde Ad özelliği vardı ve Ada göre Düğüm bulmak istediniz, sadece p => p.Name == "Bir şey" yazabilirsiniz
Varun Chatterji

2

Neden bir IEnumerable<T>uzatma yöntemi kullanmıyorsunuz?

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

o zaman sadece bunu yap

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

0

Bir süre önce, ağaç benzeri yapıları sorgulamak için Linq'in nasıl kullanılacağını açıklayan bir kod projesi makalesi yazdım:

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

Bu, torunları, alt öğeleri, ataları vb. Arayabileceğiniz linq-to-XML tarzı bir API sağlar.

Muhtemelen mevcut sorununuz için fazla öldürür, ancak başkalarının ilgisini çekebilir.


0

Ağacı sorgulamak için bu uzantı yöntemini kullanabilirsiniz.

    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

Herhangi birini düzleştirebilen genel bir uzatma yöntemim var IEnumerable<T>ve bu düzleştirilmiş koleksiyondan istediğiniz düğümü elde edebilirsiniz.

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

Bunu şu şekilde kullanın:

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

0

Ağaç öğelerini numaralandırmak için aşağıdaki uygulamaları kullanıyorum

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

Yukarıdaki uygulamada BreadthFirstUnfold, düğüm sırası yerine düğüm sırası kuyruğunu kullanır. Bu klasik BFS algoritması yolu değildir.


0

Ve sadece eğlence için (neredeyse on yıl sonra), Generics kullanarak, ancak @vidstige tarafından kabul edilen yanıta dayanan bir Yığın ve Süre döngüsüyle bir yanıt.

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

Bir koleksiyon verildiğinde bu şekilde kullanılabilir

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

veya bir kök nesneyle

        var myNode = root.Descendants(x => x.Children).Where(x => x.Key == SomeKey);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.