LINQを介してツリーを平坦化する方法は?


95

だから私は単純なツリーを持っています:

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

私は持っていIEnumerable<MyNode>ます。すべてMyNode(内部ノードオブジェクト(Elements)を含む)のリストを1つのフラットリストとして取得しますWhere group == 1。LINQを介してそのようなことを行う方法は?


1
フラット化されたリストをどのような順序にしたいですか?
フィリップ

1
ノードはいつ子ノードを停止しますか?私Elementsはそれがnullまたは空であるときだと思いますか?
アダムホールズワース2012


これに対処する最も簡単で最も明確な方法は、再帰LINQクエリを使用することです。この質問:stackoverflow.com/questions/732281/expressing-recursion-in-linqにはこれについて多くの議論があり、この特定の答えは、それをどのように実装するかについていくらか詳しく説明しています。
Alvaro Rodriguez

回答:


137

次のようにツリーを平坦化できます。

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

その後、groupを使用してフィルタリングできWhere(...)ます。

「ポイントのスタイル」を獲得するFlattenには、静的クラスの拡張関数に変換します。

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

「さらに良いスタイル」でより多くのポイントを獲得するFlattenには、ツリーと、ノードから子孫を生成する関数を使用する一般的な拡張メソッドに変換します。

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

この関数を次のように呼び出します。

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

ポストオーダーよりもプレオーダーでフラット化したい場合は、 Concat(...)ます。


@AdamHouldsworth編集ありがとうございます!呼び出しの要素がConcatあるべきnew[] {e}ではなくnew[] {c}(それもしてコンパイルされないでしょうcが)。
dasblinkenlight 2012

同意しません。コンパイル、テスト、およびでの作業c。を使用eしてもコンパイルされません。追加if (e == null) return Enumerable.Empty<T>();して、ヌルの子リストに対処することもできます。
アダムホールズワース

1
より多くの `public static IEnumerable <T> Flatten <T>(this IEnumerable <T> source、Func <T、IEnumerable <T >> f){if(source == null)return Enumerable.Empty <T>(); return source.SelectMany(c => f(c).Flatten(f))。Concat(source); } `
myWallJSON

10
この解はO(nh)であることに注意してください。ここで、nはツリー内のアイテムの数、hはツリーの平均深度です。hはO(1)とO(n)の間にあるので、これはO(n)とO(nの2乗)アルゴリズムの間にあります。より良いアルゴリズムがあります。
Eric Lippert、

1
リストがIEnumerable <baseType>の場合、関数はフラット化されたリストに要素を追加しないことに気付きました。これを解決するには、次のような関数を呼び出します。var res = tree.Flatten(node => node.Elements.OfType <DerivedType>)
Frank Horemans

125

受け入れられた回答の問題は、ツリーが深い場合は効率が悪いことです。木が非常に深い場合、スタックを爆破します。明示的なスタックを使用して問題を解決できます。

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

高さhのツリーにn個のノードがあり、分岐係数がnよりかなり小さいとすると、このメソッドはスタック空間ではO(1)、ヒープ空間ではO(h)、時間ではO(n)になります。与えられた他のアルゴリズムは、スタックではO(h)、ヒープではO(1)、時間ではO(nh)です。分岐係数がnに比べて小さい場合、hはO(lg n)とO(n)の間にあります。これは、hがnに近い場合、ナイーブアルゴリズムが危険な量のスタックと長い時間を使用できることを示しています。

これでトラバーサルができたので、クエリは簡単です。

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

3
@johnnycardy:ポイントについて議論するつもりなら、おそらくコードは明らかに正しくありません。何がそれをより明確にすることができるでしょうか?
Eric Lippert、2014年

3
@ebramtharwat:正解。Traverseすべての要素を呼び出すことができます。またはTraverse、シーケンスを取得するように変更して、シーケンスのすべての要素をにプッシュさせることもできますstackstack「私がまだトラバースしていない要素」であることを覚えておいてください。または、シーケンスがその子である「ダミー」ルートを作成し、ダミールートをトラバースすることもできます。
Eric Lippert、2014年

2
そうすればforeach (var child in current.Elements.Reverse())、より期待される平坦化が得られます。特に、子は最後の子からではなく、出現順に表示されます。これはほとんどの場合問題ではないはずですが、私の場合、予測可能で期待される順序で平坦化する必要がありました。
Micah Zoltu、2015

2
@MicahZoltu、あなたは避けることができ.Reverse交換することによって、Stack<T>のためにQueue<T>
ルーベンスファリアス

2
@MicahZoltu順序は正しいですが、問題Reverseは追加の反復子を作成することです。これは、このアプローチが回避しようとしているものです。@RubensFarias 幅優先トラバーサルの結果を置換QueueStackます。
ジャック

25

完全を期すために、dasblinkenlightとEric Lippertからの回答を組み合わせます。単体テストとすべて。:-)

 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を回避するには、var children = getChildren(current); if(children!= null){foreach(var child in children)stack.Push(child); }
serg 2015

2
これはリストをフラット化しますが、逆の順序で返すことに注意したいと思います。最後の要素が最初になるなど
Corcus

21

更新:

入れ子のレベル(深さ)に興味がある人向け。明示的な列挙スタックの実装の良い点の1つは、いつでも(特に要素を生成するとき)stack.Count、現在の処理深度を表すことです。したがって、これを考慮してC#7.0値タプルを利用すると、メソッド宣言を次のように変更できます。

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

yieldステートメント:

yield return (item, stack.Count);

次にSelect、上記にsimple を適用して元のメソッドを実装できます。

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

元の:

驚くべきことに、誰も(Ericでさえ)は、再帰的なプレオーダーDFTの「自然な」反復ポートを示していなかったため、ここに示します。

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

プレオーダーを維持するためにe呼び出すたびに切り替えると思いますelementSelector-順序が問題ではない場合、e一度開始したすべてを処理するように機能を変更できますか?
NetMage

@NetMage特に予約注文したかった。小さな変更で、ポストオーダーを処理できます。しかし、要点は、これは深度優先トラバーサルです。ブレスまずトラバーサル私が使用しますQueue<T>。とにかく、ここでのアイデアは、列挙子を含む小さなスタックを保持することです。これは、再帰的な実装で行われていることと非常によく似ています。
Ivan Stoev

@IvanStoev私はコードが単純化されるだろうと考えていました。を使用するStackと、ジグザグの幅優先トラバーサルになります。
NetMage 2018年

7

私はここに与えられた答えにいくつかの小さな問題を見つけました:

  • アイテムの最初のリストがnullの場合はどうなりますか?
  • 子のリストにnull値がある場合はどうなりますか?

以前の答えに基づいて構築され、次のことを思いつきました:

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

そしてユニットテスト:

[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

他の誰かがこれを見つけたが、ツリーを平坦化した後のレベルも知る必要がある場合、これはコナミマンのdasblinkenlightとEric Lippertのソリューションの組み合わせを拡張します。

    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

本当に他のオプションは、適切なOO設計を持つことです。

たとえば、MyNodeすべてを平坦化して返すように依頼します。

このような:

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

これで、最上位のMyNodeにすべてのノードを取得するよう要求できます。

var flatten = topNode.GetAllNodes();

クラスを編集できない場合、これはオプションではありません。しかし、それ以外の場合、これは別の(再帰的な)LINQメソッドよりも好ましいと思います。

これはLINQを使用しているので、この答えはここに当てはまると思います;)


新しいリストよりもEnumerabl.Emptyのほうがいいのでは?
フランク

1
確かに!更新しました!
ジュリアン

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
拡張機能でforeachを使用すると、「実行の遅延」がなくなります(もちろん、yield returnを使用しない限り)。
Tri Q Tran

0

コナミマンの回答のように入れ子のレベルとリストを「順序どおり」にフラット化し、逆にする必要がない場合に備えて、DaveとIvan Stoevの回答を組み合わせる。

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

また、深さ優先または幅優先を指定できると便利です...
Hugh

0

コナミマンの答えと、順序が予想外であるというコメントに基づいて、明示的な並べ替えパラメーターを持つバージョンを次に示します。

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

そして使用例:

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

0

以下は、パス内のすべてのオブジェクトのインデックスを通知する追加機能を備えたIvan Stoevのコードです。たとえば、「Item_120」を検索します。

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

アイテムとint配列[1,2,0]を返します。当然、配列の長さとしてネストレベルも使用できます。

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

こんにちは、@ lisz、このコードをどこに貼り付けますか?「修飾子 'public'はこのアイテムには有効ではありません」、「修飾子 'static'はこのアイテムには無効です」などのエラーが発生する
Kynao

0

ここで、Queueを使用して実装を使用する準備ができており、最初にFlattenツリーを返し、次に子供を返します。

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

    }

0

時々、この問題をスクラッチして、任意の深さの構造(再帰なし)をサポートし、幅優先の全探索を実行し、あまりにも多くのLINQクエリを乱用したり、子に対して再帰を先制的に実行したりする独自のソリューションを考案します。.NETソースを調べて多くのソリューションを試した後、ようやくこのソリューションを思いつきました。最終的には、Ian Stoevの答え(私がたった今見たばかりの答え)に非常に近い結果になりましたが、私は無限ループを利用したり、異常なコードフローを使用したりしていません。

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

実際の例はここにあります

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.