[C#] Linq를 사용하여 컬렉션의 마지막 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()특정 데이터 구조가 열거 될 수 있으므로이 접근 방식은 데이터를 두 번 통과시킬 위험이 있습니다. 이것은 실제로 대부분의 열거 가능한 문제가 아닙니다. 실제로 Count()O (1) 시간 내에 연산 을 평가하기 위해 목록, 배열 및 EF 쿼리에 대한 최적화가 이미 존재합니다 .

그러나 순방향 전용 열거 형을 사용해야하고 두 번의 통과를 피하려면 Lasse V. Karlsen 또는 Mark Byers 와 같은 단일 패스 알고리즘을 고려하십시오 . 이 두 가지 접근 방법은 열거하는 동안 항목을 보유하기 위해 임시 버퍼를 사용하며, 콜렉션의 끝이 발견되면 생성됩니다.


답변

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


답변

참고 : 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 크기의 링 버퍼를 사용하여 요소를 볼 때 저장하여 이전 요소를 새 요소로 덮어 씁니다. 열거 가능 항목의 끝에 도달하면 링 버퍼에 마지막 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


답변

아무도 언급하지 않았지만 SkipWhile에는 요소의 index사용 하는 메소드가 있습니다 .

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을 두 번 통과하는 두 개의 별도 작업을 수행하는 대신보다 강력하고 효율적인 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) 구현이지만 링 버퍼 구문 대신 대기열을 사용합니다 (버퍼 용량에 도달하면 항목을 대기열에서 제외).

(NB : 이것은 IEnumerable 버전입니다. IObservable 버전은 아닙니다. 두 버전의 구현은 거의 동일합니다)