Используя Linq для получения последних элементов N набора?

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

266
задан Matthew Groves 10 August 2010 в 09:44
поделиться

5 ответов

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).

Если, однако, вы должны использовать перечислимое значение только вперед и хотите избежать двух проходов, рассмотрите однопроходный алгоритм, такой как Лассе В. Карлсен или Марк Байерс описывать. Оба этих подхода используют временный буфер для хранения элементов при перечислении, которые выдаются после обнаружения конца коллекции.

394
ответ дан 23 November 2019 в 02:24
поделиться

Примечание : я пропустил заголовок вашего вопроса, в котором говорилось Использование 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;
    }
}
43
ответ дан 23 November 2019 в 02:24
поделиться
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();
58
ответ дан 23 November 2019 в 02:24
поделиться

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

28
ответ дан 23 November 2019 в 02:24
поделиться

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

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

IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
5
ответ дан 23 November 2019 в 02:24
поделиться
Другие вопросы по тегам:

Похожие вопросы: