LINQ ile ağaç nasıl düzleştirilir?


97

Yani basit bir ağacım var:

class MyNode
{
 public MyNode Parent;
 public IEnumerable<MyNode> Elements;
 int group = 1;
}

Bir IEnumerable<MyNode>. Tüm MyNode(iç düğüm nesneleri ( Elements) dahil) listesini tek bir düz liste olarak almak istiyorum Where group == 1. LINQ aracılığıyla böyle bir şey nasıl yapılır?


1
Düzleştirilmiş listenin hangi sırada olmasını istiyorsunuz?
Philip

1
Düğümler ne zaman alt düğümlere sahip olmayı bırakır? Sanırım ne zaman Elementsboş veya boş?
Adam Houldsworth


Bunu ele almanın en kolay / en net yolu, özyinelemeli bir LINQ sorgusu kullanmaktır. Bu soru: stackoverflow.com/questions/732281/expressing-recursion-in-linq'in bununla ilgili çok tartışması var ve bu özel yanıt, onu nasıl uygulayacağınıza dair biraz ayrıntı veriyor.
Alvaro Rodriguez

Yanıtlar:


141

Bir ağacı şu şekilde düzleştirebilirsiniz:

IEnumerable<MyNode> Flatten(IEnumerable<MyNode> e) =>
    e.SelectMany(c => Flatten(c.Elements)).Concat(new[] { e });

Daha sonra groupkullanarak filtreleyebilirsiniz Where(...).

Bazı "stil için puanlar" kazanmak için, Flattenstatik bir sınıfta bir uzantı işlevine dönüştürün .

public static IEnumerable<MyNode> Flatten(this IEnumerable<MyNode> e) =>
    e.SelectMany(c => c.Elements.Flatten()).Concat(e);

"Daha da iyi stil" için daha fazla puan kazanmak için Flatten, bir ağaç ve bir düğümden nesiller üreten bir işleve sahip genel bir genişletme yöntemine dönüştürün :

public static IEnumerable<T> Flatten<T>(
    this IEnumerable<T> e
,   Func<T,IEnumerable<T>> f
) => e.SelectMany(c => f(c).Flatten(f)).Concat(e);

Bu işlevi şöyle çağırın:

IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);

Düzleştirmeyi sonradan sipariş yerine ön siparişle tercih ederseniz Concat(...),.


@AdamHouldsworth Düzenleme için teşekkürler! Çağrısında eleman için Concatolmalı new[] {e}değil, new[] {c}(hatta ile derlemek olmaz corada).
Sergey Kalinichenko

Katılmıyorum: derlendi, test edildi ve birlikte çalışıldı c. Kullanmak ederlemez. if (e == null) return Enumerable.Empty<T>();Boş çocuk listeleri ile başa çıkmak için de ekleyebilirsiniz .
Adam Houldsworth

1
daha çok `public static IEnumerable <T> Flatten <T> (this IEnumerable <T> source, Func <T, IEnumerable <T>> f) {if (source == null) return Enumerable.Empty <T> (); dönüş kaynağı.SelectMany (c => f (c) .Flatten (f)). Concat (kaynak); } ''
myWallJSON

11
Bu çözümün O (nh) olduğuna dikkat edin, burada n ağaçtaki öğe sayısı ve h ağacın ortalama derinliğidir. H, O (1) ve O (n) arasında olabileceğinden, bu bir O (n) ile bir O (n kare) algoritması arasındadır. Daha iyi algoritmalar var.
Eric Lippert

1
Liste IEnumerable <baseType> ise, işlevin düzleştirilmiş listeye öğe eklemeyeceğini fark ettim. Bunu, işlevi şu şekilde çağırarak çözebilirsiniz: var res = tree.Flatten (node.Elements.OfType <DerivedType>)
Frank Horemans

127

Kabul edilen yanıtla ilgili sorun, ağacın derin olması verimsiz olmasıdır. Ağaç çok derinse yığını patlatır. Sorunu açık bir yığın kullanarak çözebilirsiniz:

public static IEnumerable<MyNode> Traverse(this MyNode root)
{
    var stack = new Stack<MyNode>();
    stack.Push(root);
    while(stack.Count > 0)
    {
        var current = stack.Pop();
        yield return current;
        foreach(var child in current.Elements)
            stack.Push(child);
    }
}

Yüksekliği h olan bir ağaçtaki düğümler ve n'den önemli ölçüde daha küçük dallanma faktörleri varsayıldığında, bu yöntem yığın uzayında O (1), yığın uzayında O (h) ve zamanda O (n) şeklindedir. Verilen diğer algoritma yığında O (h), yığında O (1) ve zamanda O (nh) 'dir. Dallanma faktörü n'ye kıyasla küçükse, h O (lg n) ve O (n) arasındadır; bu, naif algoritmanın tehlikeli miktarda yığın ve eğer h n'ye yakınsa büyük miktarda zaman kullanabileceğini gösterir.

Artık bir geçiş yaptığımıza göre, sorgunuz basit:

root.Traverse().Where(item=>item.group == 1);

3
@johnnycardy: Bir noktayı tartışacaksanız, o zaman belki de kod açıkça doğru değildir . Bunu daha açık bir şekilde doğru yapan ne olabilir?
Eric Lippert

4
@ebramtharwat: Doğru. TraverseTüm unsurları arayabilirsin . Veya Traversebir sekans almak için değişiklik yapabilir ve sekansın tüm öğelerini üzerine itmesini sağlayabilirsiniz stack. Unutma, stack"henüz geçmediğim öğeler". Ya da sekansınızın alt öğeleri olduğu "kukla" bir kök oluşturabilir ve sonra kukla kökü geçebilirsiniz.
Eric Lippert

2
Eğer yaparsanız foreach (var child in current.Elements.Reverse())daha beklenen bir düzleşme elde edersiniz. Özellikle çocuklar, son çocuktan önce göründükleri sırayla görüneceklerdir. Bu çoğu durumda önemli olmamalı, ancak benim durumumda düzleştirmenin öngörülebilir ve beklenen bir sırada olmasına ihtiyacım vardı.
Micah Zoltu

2
@MicahZoltu, a.ReverseStack<T>Queue<T>
Rubens

2
@MicahZoltu Sipariş konusunda haklısınız, ancak sorun Reverseşu ki, bu yaklaşımın kaçınması gereken şey, ek yineleyiciler yaratıyor. @RubensFarias Enine geçişteki sonuçlar Queueiçin ikame Stack.
Jack A.

27

Tamlık adına, işte dasblinkenlight ve Eric Lippert'ten gelen yanıtların kombinasyonu. Birim test edildi ve her şey. :-)

 public static IEnumerable<T> Flatten<T>(
        this IEnumerable<T> items,
        Func<T, IEnumerable<T>> getChildren)
 {
     var stack = new Stack<T>();
     foreach(var item in items)
         stack.Push(item);

     while(stack.Count > 0)
     {
         var current = stack.Pop();
         yield return current;

         var children = getChildren(current);
         if (children == null) continue;

         foreach (var child in children) 
            stack.Push(child);
     }
 }

3
NullReferenceException'ı önlemek için var children = getChildren (current); if (çocuklar! = null) {foreach (çocuklarda var olan çocuk) stack.Push (çocuk); }
serg

3
Bu, listeyi düzleştirse bile, ters sırada döndürdüğünü belirtmek isterim. Son element birinci olur vs.
Corcus

23

Güncelleme:

Yuvalama seviyesi (derinlik) ile ilgilenen insanlar için. Açık numaralandırıcı yığın uygulamasıyla ilgili iyi şeylerden biri, herhangi bir anda (ve özellikle öğeyi verirken) stack.Countşu anda işlem derinliğini temsil etmesidir. Dolayısıyla, bunu hesaba katarak ve C # 7.0 değer demetlerini kullanarak, yöntem bildirimini aşağıdaki gibi basitçe değiştirebiliriz:

public static IEnumerable<(T Item, int Level)> ExpandWithLevel<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)

ve yieldifade:

yield return (item, stack.Count);

Daha sonra Selectyukarıdakileri basitleştirerek orijinal yöntemi uygulayabiliriz :

public static IEnumerable<T> Expand<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector) =>
    source.ExpandWithLevel(elementSelector).Select(e => e.Item);

Orijinal:

Şaşırtıcı bir şekilde hiç kimse (Eric bile) özyinelemeli ön sipariş DFT'nin "doğal" yinelemeli bağlantı noktasını göstermedi, işte burada:

    public static IEnumerable<T> Expand<T>(
        this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
    {
        var stack = new Stack<IEnumerator<T>>();
        var e = source.GetEnumerator();
        try
        {
            while (true)
            {
                while (e.MoveNext())
                {
                    var item = e.Current;
                    yield return item;
                    var elements = elementSelector(item);
                    if (elements == null) continue;
                    stack.Push(e);
                    e = elements.GetEnumerator();
                }
                if (stack.Count == 0) break;
                e.Dispose();
                e = stack.Pop();
            }
        }
        finally
        {
            e.Dispose();
            while (stack.Count != 0) stack.Pop().Dispose();
        }
    }

Ön siparişi sürdürmek için eher aradığınızda geçiş yaptığınızı varsayıyorum elementSelector- sipariş önemli değilse, ebaşladıktan sonra her birini işlemek için işlevi değiştirebilir miydiniz ?
NetMage

@NetMage Özellikle ön sipariş istedim. Küçük bir değişiklikle sipariş sonrası işlem yapabilir. Ama asıl mesele şu ki, bu Derinlik İlk Geçiş . İçin Nefes Birinci Geçiş ben kullanırım Queue<T>. Her neyse, buradaki fikir, özyinelemeli uygulamada olana çok benzer şekilde, numaralandırıcılarla küçük bir yığını tutmaktır.
Ivan Stoev

@IvanStoev Kodun basitleştirileceğini düşünüyordum. Sanırım, Stackzig-zag Genişlik İlk Geçiş ile sonuçlanacaktır.
NetMage

A Stack<IEnumerator<T>>yerine a'yı korumanın amacı nedir Stack<T>? Numaralandırıcılar genellikle değiştirilebilir değer türleridir ve genellikle durum makineleri olarak uygulanır. Bu nedenle, bir Stack<IEnumerator<T>>çözümün genellikle bellek verimsiz olmasını ve çöp toplayıcıya (kutulu değer türleri nedeniyle) baskı eklemesini bekliyorum.
Theodor Zoulias

1
Ivan, yakından incelendikten sonra her iki noktada da haklısın. Boks kaçınılmazdır ve tüm çocukları saklamaktansa bir çocuk sayacını saklamak kesinlikle tercih edilir. Olumlu oy verildi. :-)
Theodor Zoulias

8

Burada verilen cevaplarla ilgili bazı küçük sorunlar buldum:

  • İlk öğe listesi boşsa ne olur?
  • Ya çocuk listesinde boş bir değer varsa?

Önceki cevaplar üzerine inşa edildi ve aşağıdakileri buldu:

public static class IEnumerableExtensions
{
    public static IEnumerable<T> Flatten<T>(
        this IEnumerable<T> items, 
        Func<T, IEnumerable<T>> getChildren)
    {
        if (items == null)
            yield break;

        var stack = new Stack<T>(items);
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current;

            if (current == null) continue;

            var children = getChildren(current);
            if (children == null) continue;

            foreach (var child in children)
                stack.Push(child);
        }
    }
}

Ve birim testleri:

[TestClass]
public class IEnumerableExtensionsTests
{
    [TestMethod]
    public void NullList()
    {
        IEnumerable<Test> items = null;
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(0, flattened.Count());
    }
    [TestMethod]
    public void EmptyList()
    {
        var items = new Test[0];
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(0, flattened.Count());
    }
    [TestMethod]
    public void OneItem()
    {
        var items = new[] { new Test() };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(1, flattened.Count());
    }
    [TestMethod]
    public void OneItemWithChild()
    {
        var items = new[] { new Test { Id = 1, Children = new[] { new Test { Id = 2 } } } };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(2, flattened.Count());
        Assert.IsTrue(flattened.Any(i => i.Id == 1));
        Assert.IsTrue(flattened.Any(i => i.Id == 2));
    }
    [TestMethod]
    public void OneItemWithNullChild()
    {
        var items = new[] { new Test { Id = 1, Children = new Test[] { null } } };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(2, flattened.Count());
        Assert.IsTrue(flattened.Any(i => i.Id == 1));
        Assert.IsTrue(flattened.Any(i => i == null));
    }
    class Test
    {
        public int Id { get; set; }
        public IEnumerable<Test> Children { get; set; }
    }
}

4

Başka birinin bunu bulması, ancak ağacı düzleştirdikten sonra seviyeyi de bilmesi durumunda, bu, Konamiman'ın dasblinkenlight ve Eric Lippert'in çözümlerinin kombinasyonunu genişletir:

    public static IEnumerable<Tuple<T, int>> FlattenWithLevel<T>(
            this IEnumerable<T> items,
            Func<T, IEnumerable<T>> getChilds)
    {
        var stack = new Stack<Tuple<T, int>>();
        foreach (var item in items)
            stack.Push(new Tuple<T, int>(item, 1));

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current;
            foreach (var child in getChilds(current.Item1))
                stack.Push(new Tuple<T, int>(child, current.Item2 + 1));
        }
    }

2

Gerçekten başka bir seçenek de uygun bir OO tasarımına sahip olmaktır.

örn. ' MyNodeden tüm düz olarak dönmesini isteyin .

Bunun gibi:

class MyNode
{
    public MyNode Parent;
    public IEnumerable<MyNode> Elements;
    int group = 1;

    public IEnumerable<MyNode> GetAllNodes()
    {
        if (Elements == null)
        {
            return Enumerable.Empty<MyNode>(); 
        }

        return Elements.SelectMany(e => e.GetAllNodes());
    }
}

Artık en üst düzey MyNode'dan tüm düğümleri almasını isteyebilirsiniz.

var flatten = topNode.GetAllNodes();

Sınıfı düzenleyemiyorsanız, bu bir seçenek değildir. Ancak aksi takdirde, bunun ayrı (özyinelemeli) bir LINQ yönteminin tercih edilebileceğini düşünüyorum.

Bu LINQ kullanıyor, Bu yüzden bu cevabın burada geçerli olduğunu düşünüyorum;)


Belki Enumerabl.Empty yeni Listeden daha iyidir?
Frank

1
Aslında! Güncellenmiş!
Julian

1

Konamiman'ın verdiği cevapta olduğu gibi, iç içe geçme seviyesine ve listenin "sırayla" düzleştirilmesine ihtiyaç duymanız durumunda Dave ve Ivan Stoev'in cevabını birleştirmek.

 public static class HierarchicalEnumerableUtils
    {
        private static IEnumerable<Tuple<T, int>> ToLeveled<T>(this IEnumerable<T> source, int level)
        {
            if (source == null)
            {
                return null;
            }
            else
            {
                return source.Select(item => new Tuple<T, int>(item, level));
            }
        }

        public static IEnumerable<Tuple<T, int>> FlattenWithLevel<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
        {
            var stack = new Stack<IEnumerator<Tuple<T, int>>>();
            var leveledSource = source.ToLeveled(0);
            var e = leveledSource.GetEnumerator();
            try
            {
                while (true)
                {
                    while (e.MoveNext())
                    {
                        var item = e.Current;
                        yield return item;
                        var elements = elementSelector(item.Item1).ToLeveled(item.Item2 + 1);
                        if (elements == null) continue;
                        stack.Push(e);
                        e = elements.GetEnumerator();
                    }
                    if (stack.Count == 0) break;
                    e.Dispose();
                    e = stack.Pop();
                }
            }
            finally
            {
                e.Dispose();
                while (stack.Count != 0) stack.Pop().Dispose();
            }
        }
    }

Önce derinliği veya genişliği belirleyebilmek de güzel olurdu ...
Hugh

1

Burada bazı kullanıma hazır uygulama Queue'yu kullanarak ve Flatten ağacını önce ben ve sonra çocuklarımı döndürüyor.

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> items, 
    Func<T,IEnumerable<T>> getChildren)
    {
        if (items == null)
            yield break;

        var queue = new Queue<T>();

        foreach (var item in items) {
            if (item == null)
                continue;

            queue.Enqueue(item);

            while (queue.Count > 0) {
                var current = queue.Dequeue();
                yield return current;

                if (current == null)
                    continue;

                var children = getChildren(current);
                if (children == null)
                    continue;

                foreach (var child in children)
                    queue.Enqueue(child);
            }
        }

    }

1

Burada sunulan yanıtların çoğu önce derinlik veya zikzak dizileri üretiyor . Örneğin aşağıdaki ağaçtan başlayarak:

        1                   2 
       / \                 / \
      /   \               /   \
     /     \             /     \
    /       \           /       \
   11       12         21       22
  / \       / \       / \       / \
 /   \     /   \     /   \     /   \
111 112   121 122   211 212   221 222

dasblinkenlight'ın cevabı bu düzleştirilmiş diziyi oluşturur:

111, 112, 121, 122, 11, 12, 211, 212, 221, 222, 21, 22, 1, 2

Konamiman en cevabı (genelleştirir Eric Lippert'ın en o cevabı ) Bu düzleştirilmiş diziyi üretir:

2, 22, 222, 221, 21, 212, 211, 1, 12, 122, 121, 11, 112, 111

Ivan Stoev'in cevabı bu düzleştirilmiş sekansı oluşturur:

1, 11, 111, 112, 12, 121, 122, 2, 21, 211, 212, 22, 221, 222

Bunun gibi enine bir diziyle ilgileniyorsanız :

1, 2, 11, 12, 21, 22, 111, 112, 121, 122, 211, 212, 221, 222

... o zaman bu sizin için çözüm:

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source,
    Func<T, IEnumerable<T>> childrenSelector)
{
    var queue = new Queue<T>(source);
    while (queue.Count > 0)
    {
        var current = queue.Dequeue();
        yield return current;
        var children = childrenSelector(current);
        if (children == null) continue;
        foreach (var child in children) queue.Enqueue(child);
    }
}

Gerçekleştirmedeki fark temelde a Queueyerine a kullanmaktır Stack. Gerçek bir sıralama gerçekleşmiyor.


Dikkat: Bu uygulama bellek verimliliği açısından optimal olmaktan uzaktır, çünkü toplam öğe sayısının büyük bir yüzdesi numaralandırma sırasında dahili kuyrukta depolanır. Stack-based ağaç geçişleri- Queuetabanlı uygulamalara göre bellek kullanımı açısından çok daha verimlidir .


0
void Main()
{
    var allNodes = GetTreeNodes().Flatten(x => x.Elements);

    allNodes.Dump();
}

public static class ExtensionMethods
{
    public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> childrenSelector = null)
    {
        if (source == null)
        {
            return new List<T>();
        }

        var list = source;

        if (childrenSelector != null)
        {
            foreach (var item in source)
            {
                list = list.Concat(childrenSelector(item).Flatten(childrenSelector));
            }
        }

        return list;
    }
}

IEnumerable<MyNode> GetTreeNodes() {
    return new[] { 
        new MyNode { Elements = new[] { new MyNode() }},
        new MyNode { Elements = new[] { new MyNode(), new MyNode(), new MyNode() }}
    };
}

class MyNode
{
    public MyNode Parent;
    public IEnumerable<MyNode> Elements;
    int group = 1;
}

1
Uzantınızda bir foreach kullanmak, artık 'gecikmeli yürütme' olmadığı anlamına gelir (elbette getiri getirisini kullanmadığınız sürece).
Tri Q Tran

0

Konamiman'ın cevabına ve sıralamanın beklenmedik olduğu yorumuna dayanarak, burada açık bir sıralama parametresi olan bir sürüm var:

public static IEnumerable<T> TraverseAndFlatten<T, V>(this IEnumerable<T> items, Func<T, IEnumerable<T>> nested, Func<T, V> orderBy)
{
    var stack = new Stack<T>();
    foreach (var item in items.OrderBy(orderBy))
        stack.Push(item);

    while (stack.Count > 0)
    {
        var current = stack.Pop();
        yield return current;

        var children = nested(current).OrderBy(orderBy);
        if (children == null) continue;

        foreach (var child in children)
            stack.Push(child);
    }
}

Ve bir örnek kullanım:

var flattened = doc.TraverseAndFlatten(x => x.DependentDocuments, y => y.Document.DocDated).ToList();

0

Aşağıda Ivan Stoev'in yoldaki her nesnenin indeksini söyleyen ek özelliğe sahip kodu bulunmaktadır. Örneğin, "Item_120" araması yapın:

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

öğeyi ve bir int dizisi [1,2,0] döndürür. Açıkçası, dizinin uzunluğu olarak yuvalama seviyesi de mevcuttur.

public static IEnumerable<(T, int[])> Expand<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> getChildren) {
    var stack = new Stack<IEnumerator<T>>();
    var e = source.GetEnumerator();
    List<int> indexes = new List<int>() { -1 };
    try {
        while (true) {
            while (e.MoveNext()) {
                var item = e.Current;
                indexes[stack.Count]++;
                yield return (item, indexes.Take(stack.Count + 1).ToArray());
                var elements = getChildren(item);
                if (elements == null) continue;
                stack.Push(e);
                e = elements.GetEnumerator();
                if (indexes.Count == stack.Count)
                    indexes.Add(-1);
                }
            if (stack.Count == 0) break;
            e.Dispose();
            indexes[stack.Count] = -1;
            e = stack.Pop();
        }
    } finally {
        e.Dispose();
        while (stack.Count != 0) stack.Pop().Dispose();
    }
}

Merhaba @lisz, bu kodu nereye yapıştırıyorsunuz? "'Public' değiştiricisi bu öğe için geçerli değil", "'statik' değiştirici bu öğe için geçerli değil" gibi hatalar
alıyorum

0

Arada bir bu problemi kazımaya çalışıyorum ve keyfi derin yapıları destekleyen (özyineleme yok), enine ilk geçişi gerçekleştiren ve çok fazla LINQ sorgusunu kötüye kullanmayan veya çocuklar üzerinde önceden özyineleme gerçekleştirmeyen kendi çözümümü geliştirmeye çalışıyorum. .NET kaynağını araştırdıktan ve birçok çözümü denedikten sonra , sonunda bu çözümü buldum. Sonunda Ian Stoev'in cevabına çok yakın oldu (cevabını sadece şimdi gördüm), ancak benimki sonsuz döngüler kullanmıyor veya alışılmadık kod akışına sahip değil.

public static IEnumerable<T> Traverse<T>(
    this IEnumerable<T> source,
    Func<T, IEnumerable<T>> fnRecurse)
{
    if (source != null)
    {
        Stack<IEnumerator<T>> enumerators = new Stack<IEnumerator<T>>();
        try
        {
            enumerators.Push(source.GetEnumerator());
            while (enumerators.Count > 0)
            {
                var top = enumerators.Peek();
                while (top.MoveNext())
                {
                    yield return top.Current;

                    var children = fnRecurse(top.Current);
                    if (children != null)
                    {
                        top = children.GetEnumerator();
                        enumerators.Push(top);
                    }
                }

                enumerators.Pop().Dispose();
            }
        }
        finally
        {
            while (enumerators.Count > 0)
                enumerators.Pop().Dispose();
        }
    }
}

Çalışan bir örnek burada bulunabilir .

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.