コレクションを前提として、そのコレクションの最後のN個の要素を取得する方法はありますか?フレームワークにメソッドがない場合、これを行う拡張メソッドを作成する最良の方法は何でしょうか?
コレクションを前提として、そのコレクションの最後のN個の要素を取得する方法はありますか?フレームワークにメソッドがない場合、これを行う拡張メソッドを作成する最良の方法は何でしょうか?
回答:
collection.Skip(Math.Max(0, collection.Count() - N));
このアプローチは、並べ替えに依存せずにアイテムの順序を維持し、いくつかのLINQプロバイダー間で幅広い互換性を備えています。
Skip
負の数で電話しないように注意することが重要です。Entity Frameworkなどの一部のプロバイダーは、負の引数が提示されるとArgumentExceptionを生成します。Math.Max
これをきちんと回避するための呼び出し。
以下のクラスには、静的クラス、静的メソッド、this
キーワードの使用など、拡張メソッドのすべての必須要素があります。
public static class MiscExtensions
{
// Ex: collection.TakeLast(5);
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int N)
{
return source.Skip(Math.Max(0, source.Count() - N));
}
}
パフォーマンスに関する簡単なメモ:
への呼び出しCount()
は特定のデータ構造の列挙を引き起こす可能性があるため、このアプローチには、データに対して2つのパスが発生するリスクがあります。これは、ほとんどの列挙型の問題ではありません。実際、Count()
O、(1)時間で操作を評価するために、リスト、配列、さらにはEFクエリの最適化がすでに存在しています。
ただし、前方のみの列挙型を使用する必要があり、2つのパスを作成したくない場合は、Lasse V. KarlsenやMark Byersのような1パスアルゴリズムを検討してください。これらのアプローチはどちらも、一時バッファーを使用して、列挙中にアイテムを保持します。これらは、コレクションの終わりが見つかると生成されます。
List
sとLinkedList
s を使用すると、Jamesのソリューションは一桁ではありませんが、高速になる傾向があります。IEnumerableが(Enumerable.Rangeなどを介して)計算される場合、Jamesのソリューションはさらに時間がかかります。実装について何かを知っているか、別のデータ構造に値をコピーすることなく、単一のパスを保証する方法は考えられません。
coll.Reverse().Take(N).Reverse().ToList();
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
return coll.Reverse().Take(N).Reverse();
}
更新:clintpの問題に対処するには:a)上記で定義したTakeLast()メソッドを使用して問題を解決しますが、追加のメソッドなしで本当に実行したい場合は、Enumerable.Reverse()が拡張メソッドとして使用されるため、そのように使用する必要はありません。
List<string> mystring = new List<string>() { "one", "two", "three" };
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();
List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse();
.Reverse()はvoidを返すとコンパイラ選ぶ代わりにIEnumerableをを返すLINQの1の方法ので、私はコンパイルエラーを取得します。提案?
N
レコードを取得した後で順序を気にしない場合は、2番目をスキップできますReverse
。
注:「Linqの使用」と書かれた質問のタイトルを見逃していたため、私の回答では実際にはLinqを使用していません。
コレクション全体の非遅延コピーをキャッシュしないようにする場合は、リンクリストを使用してそれを行う簡単なメソッドを記述できます。
次のメソッドは、元のコレクションで見つかった各値をリンクリストに追加し、リンクリストを必要なアイテム数にトリミングします。リンクされたリストは、コレクションを反復処理する間、常にこの数のアイテムにトリミングされたままになるため、元のコレクションから最大でN個のアイテムのコピーのみが保持されます。
元のコレクションのアイテム数を知っている必要はありません。また、複数回繰り返す必要もありません。
使用法:
IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
延長方法:
public static class Extensions
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> collection,
int n)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
if (n < 0)
throw new ArgumentOutOfRangeException(nameof(n), $"{nameof(n)} must be 0 or greater");
LinkedList<T> temp = new LinkedList<T>();
foreach (var value in collection)
{
temp.AddLast(value);
if (temp.Count > n)
temp.RemoveFirst();
}
return temp;
}
}
以下は、列挙可能で機能するがO(N)一時ストレージのみを使用するメソッドです。
public static class TakeLastExtension
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int takeCount)
{
if (source == null) { throw new ArgumentNullException("source"); }
if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); }
if (takeCount == 0) { yield break; }
T[] result = new T[takeCount];
int i = 0;
int sourceCount = 0;
foreach (T element in source)
{
result[i] = element;
i = (i + 1) % takeCount;
sourceCount++;
}
if (sourceCount < takeCount)
{
takeCount = sourceCount;
i = 0;
}
for (int j = 0; j < takeCount; ++j)
{
yield return result[(i + j) % takeCount];
}
}
}
使用法:
List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();
これは、サイズNのリングバッファーを使用して要素を格納し、古い要素を新しい要素で上書きすることで機能します。enumerableの最後に到達すると、リングバッファーには最後のN個の要素が含まれます。
n
。
.NET Core 2.0+は、LINQメソッドを提供しますTakeLast()
。
https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast
例:
Enumerable
.Range(1, 10)
.TakeLast(3) // <--- takes last 3 items
.ToList()
.ForEach(i => System.Console.WriteLine(i))
// outputs:
// 8
// 9
// 10
netcoreapp1.x
)では使用できませんが、dotnetcore(netcoreapp2.x
)のv2.0およびv2.1でのみ使用できます。おそらくnet472
サポートされていない完全なフレームワーク(例:)をターゲットにしている可能性があります。(.net標準ライブラリは上記のいずれでも使用できますが、ターゲットフレームワークに固有の特定のAPIのみを公開する可能性があります。docs.microsoft.com/en
誰も言及していないことに驚いていますが、SkipWhileには要素のインデックスを使用するメソッドがあります。
public static IEnumerable<T> TakeLastN<T>(this IEnumerable<T> source, int n)
{
if (source == null)
throw new ArgumentNullException("Source cannot be null");
int goldenIndex = source.Count() - n;
return source.SkipWhile((val, index) => index < goldenIndex);
}
//Or if you like them one-liners (in the spirit of the current accepted answer);
//However, this is most likely impractical due to the repeated calculations
collection.SkipWhile((val, index) => index < collection.Count() - N)
このソリューションが他のソリューションよりも優れていると認識できる唯一の利点は、述語を追加して、IEnumerableを2回トラバースする2つの個別の操作を使用する代わりに、より強力で効率的なLINQクエリを作成できるということです。
public static IEnumerable<T> FilterLastN<T>(this IEnumerable<T> source, int n, Predicate<T> pred)
{
int goldenIndex = source.Count() - n;
return source.SkipWhile((val, index) => index < goldenIndex && pred(val));
}
RXのSystem.InteractiveアセンブリでEnumerableEx.TakeLastを使用します。これは@MarkのようなO(N)実装ですが、リングバッファー構成ではなくキューを使用します(バッファーの容量に達したときにアイテムをデキューします)。
(注:これはIEnumerableバージョンです-IObservableバージョンではありませんが、2つの実装はほぼ同じです)
私は効率とシンプルさを組み合わせて、これで終わりました:
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count)
{
if (source == null) { throw new ArgumentNullException("source"); }
Queue<T> lastElements = new Queue<T>();
foreach (T element in source)
{
lastElements.Enqueue(element);
if (lastElements.Count > count)
{
lastElements.Dequeue();
}
}
return lastElements;
}
パフォーマンスについて:C#ではQueue<T>
、循環バッファーを使用して実装されるため、ループごとにオブジェクトのインスタンス化は行われません(キューが大きくなっている場合のみ)。誰かがこの拡張機能をで呼び出す可能性があるため、(専用コンストラクタを使用して)キュー容量を設定しませんでしたcount = int.MaxValue
。追加のパフォーマンスを得るには、ソースが実装されIList<T>
ているかどうかを確認し、実装されている場合は、配列インデックスを使用して最後の値を直接抽出します。
上記のすべてのソリューションではコレクション全体を反復処理する必要があるため、LINQを使用してコレクションの最後のNを取得することは少し非効率的です。TakeLast(int n)
にSystem.Interactive
もこの問題があります。
リストがある場合、より効率的な方法は、次の方法を使用してスライスすることです
/// Select from start to end exclusive of end using the same semantics
/// as python slice.
/// <param name="list"> the list to slice</param>
/// <param name="start">The starting index</param>
/// <param name="end">The ending index. The result does not include this index</param>
public static List<T> Slice<T>
(this IReadOnlyList<T> list, int start, int? end = null)
{
if (end == null)
{
end = list.Count();
}
if (start < 0)
{
start = list.Count + start;
}
if (start >= 0 && end.Value > 0 && end.Value > start)
{
return list.GetRange(start, end.Value - start);
}
if (end < 0)
{
return list.GetRange(start, (list.Count() + end.Value) - start);
}
if (end == start)
{
return new List<T>();
}
throw new IndexOutOfRangeException(
"count = " + list.Count() +
" start = " + start +
" end = " + end);
}
と
public static List<T> GetRange<T>( this IReadOnlyList<T> list, int index, int count )
{
List<T> r = new List<T>(count);
for ( int i = 0; i < count; i++ )
{
int j=i + index;
if ( j >= list.Count )
{
break;
}
r.Add(list[j]);
}
return r;
}
そしていくつかのテストケース
[Fact]
public void GetRange()
{
IReadOnlyList<int> l = new List<int>() { 0, 10, 20, 30, 40, 50, 60 };
l
.GetRange(2, 3)
.ShouldAllBeEquivalentTo(new[] { 20, 30, 40 });
l
.GetRange(5, 10)
.ShouldAllBeEquivalentTo(new[] { 50, 60 });
}
[Fact]
void SliceMethodShouldWork()
{
var list = new List<int>() { 1, 3, 5, 7, 9, 11 };
list.Slice(1, 4).ShouldBeEquivalentTo(new[] { 3, 5, 7 });
list.Slice(1, -2).ShouldBeEquivalentTo(new[] { 3, 5, 7 });
list.Slice(1, null).ShouldBeEquivalentTo(new[] { 3, 5, 7, 9, 11 });
list.Slice(-2)
.Should()
.BeEquivalentTo(new[] {9, 11});
list.Slice(-2,-1 )
.Should()
.BeEquivalentTo(new[] {9});
}
私はこの質問に答えるのが遅いことを知っています。ただし、IList <>型のコレクションを使用していて、返されるコレクションの順序を気にしない場合は、このメソッドの方が高速です。私が使ってきたマークバイヤーズ氏の答えを少し変更を加えました。したがって、メソッドTakeLastは次のとおりです。
public static IEnumerable<T> TakeLast<T>(IList<T> source, int takeCount)
{
if (source == null) { throw new ArgumentNullException("source"); }
if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); }
if (takeCount == 0) { yield break; }
if (source.Count > takeCount)
{
for (int z = source.Count - 1; takeCount > 0; z--)
{
takeCount--;
yield return source[z];
}
}
else
{
for(int i = 0; i < source.Count; i++)
{
yield return source[i];
}
}
}
テストでは、Mark Byersメソッドとkbrimingtonのandswerを使用しました。これはテストです:
IList<int> test = new List<int>();
for(int i = 0; i<1000000; i++)
{
test.Add(i);
}
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
IList<int> result = TakeLast(test, 10).ToList();
stopwatch.Stop();
Stopwatch stopwatch1 = new Stopwatch();
stopwatch1.Start();
IList<int> result1 = TakeLast2(test, 10).ToList();
stopwatch1.Stop();
Stopwatch stopwatch2 = new Stopwatch();
stopwatch2.Start();
IList<int> result2 = test.Skip(Math.Max(0, test.Count - 10)).Take(10).ToList();
stopwatch2.Stop();
そして、これは10の要素をとった結果です:
1000001要素を取る場合の結果は次のとおりです。
これが私の解決策です:
public static class EnumerationExtensions
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> input, int count)
{
if (count <= 0)
yield break;
var inputList = input as IList<T>;
if (inputList != null)
{
int last = inputList.Count;
int first = last - count;
if (first < 0)
first = 0;
for (int i = first; i < last; i++)
yield return inputList[i];
}
else
{
// Use a ring buffer. We have to enumerate the input, and we don't know in advance how many elements it will contain.
T[] buffer = new T[count];
int index = 0;
count = 0;
foreach (T item in input)
{
buffer[index] = item;
index = (index + 1) % buffer.Length;
count++;
}
// The index variable now points at the next buffer entry that would be filled. If the buffer isn't completely
// full, then there are 'count' elements preceding index. If the buffer *is* full, then index is pointing at
// the oldest entry, which is the first one to return.
//
// If the buffer isn't full, which means that the enumeration has fewer than 'count' elements, we'll fix up
// 'index' to point at the first entry to return. That's easy to do; if the buffer isn't full, then the oldest
// entry is the first one. :-)
//
// We'll also set 'count' to the number of elements to be returned. It only needs adjustment if we've wrapped
// past the end of the buffer and have enumerated more than the original count value.
if (count < buffer.Length)
index = 0;
else
count = buffer.Length;
// Return the values in the correct order.
while (count > 0)
{
yield return buffer[index];
index = (index + 1) % buffer.Length;
count--;
}
}
}
public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> input, int count)
{
if (count <= 0)
return input;
else
return input.SkipLastIter(count);
}
private static IEnumerable<T> SkipLastIter<T>(this IEnumerable<T> input, int count)
{
var inputList = input as IList<T>;
if (inputList != null)
{
int first = 0;
int last = inputList.Count - count;
if (last < 0)
last = 0;
for (int i = first; i < last; i++)
yield return inputList[i];
}
else
{
// Aim to leave 'count' items in the queue. If the input has fewer than 'count'
// items, then the queue won't ever fill and we return nothing.
Queue<T> elements = new Queue<T>();
foreach (T item in input)
{
elements.Enqueue(item);
if (elements.Count > count)
yield return elements.Dequeue();
}
}
}
}
コードは少し分量がありますが、ドロップインの再利用可能なコンポーネントとして、ほとんどのシナリオで十分に機能するはずであり、使用しているコードを簡潔かつ簡潔に保ちます。:-)
My TakeLast
for non- IList`1
は、@ Mark Byersおよび@MackieChanの回答と同じリングバッファアルゴリズムに基づいています。それらがどれほど似ているかは興味深いです-私は完全に独立して私のものを書きました。リングバッファを適切に実行する方法は1つだけあると思います。:-)
@kbrimingtonの回答を見るIQuerable<T>
と、Entity Frameworkで適切に機能するアプローチにフォールバックするために、これに追加のチェックを追加できます。
コレクション(配列)から最後の3つの要素を取得する実際の例の下:
// split address by spaces into array
string[] adrParts = adr.Split(new string[] { " " },StringSplitOptions.RemoveEmptyEntries);
// take only 3 last items in array
adrParts = adrParts.SkipWhile((value, index) => { return adrParts.Length - index > 3; }).ToArray();
このメソッドを使用してエラーなしですべての範囲を取得する
public List<T> GetTsRate( List<T> AllT,int Index,int Count)
{
List<T> Ts = null;
try
{
Ts = AllT.ToList().GetRange(Index, Count);
}
catch (Exception ex)
{
Ts = AllT.Skip(Index).ToList();
}
return Ts ;
}
循環バッファーの使用による少し異なる実装。ベンチマークは、この方法は、年頃2倍高速使用のものよりもあることを示しているキュー(の実装TakeLastでSystem.Linqのを、それはあなたが持っている場合でも、要素の要求された数と一緒に成長するバッファを必要とする-しかしないコストなし、)小さなコレクションでは、巨大なメモリ割り当てを取得できます。
public IEnumerable<T> TakeLast<T>(IEnumerable<T> source, int count)
{
int i = 0;
if (count < 1)
yield break;
if (source is IList<T> listSource)
{
if (listSource.Count < 1)
yield break;
for (i = listSource.Count < count ? 0 : listSource.Count - count; i < listSource.Count; i++)
yield return listSource[i];
}
else
{
bool move = true;
bool filled = false;
T[] result = new T[count];
using (var enumerator = source.GetEnumerator())
while (move)
{
for (i = 0; (move = enumerator.MoveNext()) && i < count; i++)
result[i] = enumerator.Current;
filled |= move;
}
if (filled)
for (int j = i; j < count; j++)
yield return result[j];
for (int j = 0; j < i; j++)
yield return result[j];
}
}