だから私は単純なツリーを持っています:
class MyNode
{
public MyNode Parent;
public IEnumerable<MyNode> Elements;
int group = 1;
}
IEnumerable<MyNode>
。すべてのMyNode
(内部ノードオブジェクト(Elements
)を含む)のリストを1つのフラットリストとして取得したいWhere
group == 1
。 LINQ経由でそのようなことを行う方法は?
次のようにツリーをフラット化できます。
_IEnumerable<MyNode> Flatten(IEnumerable<MyNode> e) {
return e.SelectMany(c => Flatten(c.Elements)).Concat(new[] {e});
}
_
その後、Where(...)
を使用してgroup
でフィルタリングできます。
「スタイルのポイント」を獲得するには、Flatten
を静的クラスの拡張関数に変換します。
_public static IEnumerable<MyNode> Flatten(this IEnumerable<MyNode> e) {
return e.SelectMany(c => c.Elements.Flatten()).Concat(e);
}
_
「さらに良いスタイル」でいくつかのポイントを獲得するには、Flatten
を、ツリーと子孫を生成する関数を使用する一般的な拡張メソッドに変換します。
_public static IEnumerable<T> Flatten<T>(
this IEnumerable<T> e,
Func<T,IEnumerable<T>> f)
{
return e.SelectMany(c => f(c).Flatten(f)).Concat(e);
}
_
この関数を次のように呼び出します。
_IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);
_
ポストオーダーではなくプレオーダーでフラット化することを希望する場合は、Concat(...)
の両側を切り替えます。
受け入れられた答えの問題は、ツリーが深い場合は効率が悪いことです。ツリーがvery deepである場合、スタックを爆破します。明示的なスタックを使用して問題を解決できます。
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) in time。分岐係数がnと比較して小さい場合、hはO(lg n)とO(n)の間にあります。ナイーブアルゴリズムは、hがnに近い場合、危険な量のスタックと長い時間を使用する可能性があること。
トラバーサルができたので、クエリは簡単です。
root.Traverse().Where(item=>item.group == 1);
完全を期すために、ここに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);
}
}
更新:
ネストのレベル(深さ)に興味がある人向け。明示的な列挙子スタックの実装の良い点の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
を適用することにより、元のメソッドを実装できます。
public static IEnumerable<T> Expand<T>(
this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector) =>
source.ExpandWithLevel(elementSelector).Select(e => e.Item);
オリジナル:
驚くべきことに、誰も(エリックでさえ)再帰的な事前注文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();
}
}
ここで与えられた答えにいくつかの小さな問題が見つかりました。
以前の回答に基づいて構築され、以下を思い付きました。
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; }
}
}
他の誰かがこれを見つけたが、ツリーを平坦化した後にレベルを知る必要がある場合、これは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));
}
}
本当に他のオプションは、適切なOOデザインです。
例えばMyNode
にすべてフラット化を返すように依頼します。
このような:
class MyNode
{
public MyNode Parent;
public IEnumerable<MyNode> Elements;
int group = 1;
public IEnumerable<MyNode> GetAllNodes()
{
if (Elements == null)
{
return new List<MyNode>();
}
return Elements.SelectMany(e => e.GetAllNodes());
}
}
これで、最上位のMyNodeにすべてのノードを取得するように依頼できます。
var flatten = topNode.GetAllNodes();
クラスを編集できない場合、これはオプションではありません。しかし、そうでなければ、これは別の(再帰的な)LINQメソッドよりも好ましいと思います。
これはLINQを使用しているため、この回答はここで適用できると思います;)
以下は、パス内のすべてのオブジェクトのインデックスを通知する追加機能を備えた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();
}
}
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;
}
ネストのレベルが必要な場合に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();
}
}
}
コナミマンの答えと、順序が予想外であるというコメントに基づいて、明示的な並べ替えパラメーターを使用したバージョンを次に示します。
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();