Используя Linq, чтобы получить последние N элементов коллекции? - PullRequest
259 голосов
/ 11 августа 2010

Учитывая коллекцию, есть ли способ получить последние N элементов этой коллекции? Если в структуре нет метода, как лучше написать метод расширения для этого?

Ответы [ 17 ]

376 голосов
/ 11 августа 2010
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() может вызвать перечисление определенных структур данных, такой подход может вызвать два прохода по данным. На самом деле это не проблема для большинства перечислимых; фактически уже существуют оптимизации для списков, массивов и даже запросов EF для оценки операции Count() за O (1).

Если, однако, вы должны использовать перечислимое только для пересылки и хотели бы избежать двухпроходного прохождения, рассмотрите однопроходный алгоритм, например Lasse V. Karlsen или Mark Byers описать. Оба эти подхода используют временный буфер для хранения элементов при перечислении, которые выдаются после того, как будет найден конец коллекции.

54 голосов
/ 11 августа 2010
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();
43 голосов
/ 11 августа 2010

Примечание : я пропустил заголовок вашего вопроса, который сказал Использование 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("collection");
        if (n < 0)
            throw new ArgumentOutOfRangeException("n", "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;
    }
}
28 голосов
/ 11 августа 2010

Вот метод, который работает на любом перечисляемом, но использует только 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 элементов.

13 голосов
/ 24 февраля 2018

.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
11 голосов
/ 09 июля 2012

Я удивлен, что никто не упомянул об этом, но у 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)

Единственное ощутимое преимущество, которое дает это решение перед другими, заключается в том, что вы можете добавить предикат для создания более мощного и эффективного запроса LINQ вместо двух отдельных операций, которые дважды пересекают IEnumerable.

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));
}
8 голосов
/ 29 июля 2011

Используйте EnumerableEx.TakeLast в сборке System.Interactive RX. Это реализация O (N), как у @ Mark, но она использует очередь, а не конструкцию кольцевого буфера (и удаляет элементы, когда она достигает емкости буфера).

(Примечание: это версия IEnumerable, а не версия IObservable, хотя реализация этих двух программ практически идентична)

5 голосов
/ 06 августа 2013

Если вы имеете дело с коллекцией с ключом (например, записи из базы данных), быстрое (т.е. быстрее, чем выбранный ответ) решение будет

collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
5 голосов
/ 11 августа 2010

Если вы не против погрузиться в Rx как часть монады, вы можете использовать TakeLast:

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

IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
2 голосов
/ 01 июля 2016

Я попытался объединить эффективность и простоту и в итоге получил:

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>, и если да, напрямую извлечь последние значения, используя индексы массива.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...