Linqを使用してコレクションの最後のN個の要素を取得しますか?


284

コレクションを前提として、そのコレクションの最後のN個の要素を取得する方法はありますか?フレームワークにメソッドがない場合、これを行う拡張メソッドを作成する最良の方法は何でしょうか?

回答:


422
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. KarlsenMark Byersのような1パスアルゴリズムを検討してください。これらのアプローチはどちらも、一時バッファーを使用して、列挙中にアイテムを保持します。これらは、コレクションの終わりが見つかると生成されます。


2
+1、これはLinq to Entities / SQLで機能するためです。Linq to Objectsでは、James Curranの戦略よりもパフォーマンスが高いと思います。
StriplingWarrior 2010

11
コレクションの性質によって異なります。Count()はO(N)の場合があります。
ジェームズ・カラン

3
@ジェームズ:絶対に正しい。IEnumerableコレクションを厳密に扱う場合、これは2パスクエリになる可能性があります。保証された1パスアルゴリズムを確認することに非常に興味があります。役に立つかもしれません。
kbrimington 2010

4
いくつかのベンチマークを行いました。LINQ to Objectsは、使用しているコレクションの種類に基づいていくつかの最適化を実行することがわかりました。配列ListsとLinkedLists を使用すると、Jamesのソリューションは一桁ではありませんが、高速になる傾向があります。IEnumerableが(Enumerable.Rangeなどを介して)計算される場合、Jamesのソリューションはさらに時間がかかります。実装について何かを知っているか、別のデータ構造に値をコピーすることなく、単一のパスを保証する方法は考えられません。
StriplingWarrior 2010

1
@RedFilter-十分に公平です。私のページングの習慣がここに漏れたと思います。あなたの鋭い目をありがとう。
kbrimington 2012年

59
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の方法ので、私はコンパイルエラーを取得します。提案?
クリントンピアス

1
あなたは明示的にIEnumerableを<文字列>へのMyStringをキャストすることによってこの問題を解決することができます((IEnumerableを<文字列>)のmyString).Reverseは、()(2).Reverseしてください()。
月Hettich

簡単で十分ですが、順序を完全に2回逆にする必要があります。これが最良の方法になる可能性があります
shashwat

kbrimingtonからの受け入れられた回答に加えて、私はそれが好きです。最後のNレコードを取得した後で順序を気にしない場合は、2番目をスキップできますReverse
ZoolWay 14

@shashwat順序を「完全に」2回逆にすることはありません。2番目の反転は、Nアイテムのコレクションにのみ適用されます。さらに、Reverse()の実装方法によっては、最初の呼び出しでN個の項目のみが逆転する場合があります。(.NET 4.0実装はコレクションを配列にコピーし、
それから

47

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

Linqを技術的に使用していなくても、あなたは良い有効な答えを持っていると私はまだ思うので、私はまだあなたに+1を与えます:)
Matthew Groves 2010

クリーンで端正で拡張可能な+1!
Yasser Shaikh

1
ソース列挙子が2回(またはそれ以上)実行されず、列挙の具体化が強制されない唯一の解決策だと思うので、ほとんどのアプリケーションでは、はるかに効率的だと思いますメモリと速度の。
Sprotty 16

30

以下は、列挙可能で機能するが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個の要素が含まれます。


2
+1:これは私のパフォーマンスよりも優れているはずですが、コレクションに含まれる要素の数が少ない場合は、正しく機能することを確認してくださいn
Lasse V. Karlsen、2010

そうですね、ほとんどの場合、SOからコードをプロダクション用にコピーして、そのようなものを自分で追加するときに人々が注意を払うと思いますが、それは問題ではないかもしれません。追加する場合は、コレクション変数のnullもチェックすることを検討してください。それ以外の場合は、優れた解決策:)リンクバッファを使用することを自分で検討していました。リンクされたリストによってGC圧力が追加されるためですが、それを実行してから、テストコードをわざわざ理解する必要がなくなって久しぶりです。私がそれを正しくしたなら。私はLINQPadに恋をしていると言わざるを得ません:) linqpad.net
Lasse V. Karlsen

2
可能な最適化は、列挙可能な実装されたIListであるかどうかを確認し、そうである場合は簡単なソリューションを使用することです。一時的なストレージアプローチは、
真に

1
簡単なnit-pick:ArgumentOutOfRangeExceptionへの引数の順序が間違っています(R#によると)
piers7

28

.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

私は:NET Standard 2.0を使用していますが、使用できません。どうしましたか?:(
SuperJMN

@SuperJMN .net標準2.0ライブラリを参照している可能性がありますが、プロジェクトで正しいバージョンのdotnetコアをターゲットにしていない可能性があります。この方法はv1.x(netcoreapp1.x)では使用できませんが、dotnetcore(netcoreapp2.x)のv2.0およびv2.1でのみ使用できます。おそらくnet472サポートされていない完全なフレームワーク(例:)をターゲットにしている可能性があります。(.net標準ライブラリは上記のいずれでも使用できますが、ターゲットフレームワークに固有の特定のAPIのみを公開する可能性があります。docs.microsoft.com/en
Ray

1
これらは今より高くする必要があります。ホイールを再発明する必要はありません
James Woodley、

11

誰も言及していないことに驚いていますが、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));
}

9

RXのSystem.InteractiveアセンブリでEnumerableEx.TakeLastを使用します。これは@MarkのようなO(N)実装ですが、リングバッファー構成ではなくキューを使用します(バッファーの容量に達したときにアイテムをデキューします)。

(注:これはIEnumerableバージョンです-IObservableバージョンではありませんが、2つの実装はほぼ同じです)


これが最良の答えです。仕事をする適切なライブラリーがあり、RXチームが高品質である場合は、自分でロールバックしないでください。
Bradgonesurfing 14

これを使用する場合は、Nuget-nuget.org/ packages
Ix

C#Queue<T>循環バッファーを使用して実装されていませんか?
tigrou

@tigrou。いいえ、循環していません
シティキッド


6

キーを使用したコレクション(データベースからのエントリなど)を処理している場合、迅速な(つまり、選択した回答よりも速い)ソリューションは次のようになります。

collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);

+1は私にとっては効果的で、読みやすく、リストにいくつかのオブジェクトがあります
fubo

5

モナドの一部としてRxに浸ることを気にしない場合は、以下を使用できますTakeLast

IEnumerable<int> source = Enumerable.Range(1, 10000);

IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();

2
System.ReactiveではなくRXのSystem.Interactiveを参照する場合、AsObservable()は必要ありません(私の回答を参照)
piers7

2

サードパーティのライブラリの使用がオプションである場合、MoreLinqTakeLast()これを正確に実行するものを定義します。


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>ているかどうかを確認し、実装されている場合は、配列インデックスを使用して最後の値を直接抽出します。


1

上記のすべてのソリューションではコレクション全体を反復処理する必要があるため、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});
}

1

私はこの質問に答えるのが遅いことを知っています。ただし、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要素を取る場合の結果は次のとおりです。 ここに画像の説明を入力してください


1

これが私の解決策です:

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 TakeLastfor non- IList`1は、@ Mark Byersおよび@MackieChanの回答と同じリングバッファアルゴリズムに基づいています。それらがどれほど似ているかは興味深いです-私は完全に独立して私のものを書きました。リングバッファを適切に実行する方法は1つだけあると思います。:-)

@kbrimingtonの回答を見るIQuerable<T>と、Entity Frameworkで適切に機能するアプローチにフォールバックするために、これに追加のチェックを追加できます。


0

コレクション(配列)から最後の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();

0

このメソッドを使用してエラーなしですべての範囲を取得する

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

0

循環バッファーの使用による少し異なる実装。ベンチマークは、この方法は、年頃2倍高速使用のものよりもあることを示しているキュー(の実装TakeLastSystem.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];

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