Используя Linq, чтобы получить последние N элементов коллекции?

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

Ответ 1

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

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

Ответ 2

coll.Reverse().Take(N).Reverse().ToList();


public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
    return coll.Reverse().Take(N).Reverse();
}

UPDATE: для решения проблемы clintp: a) Используя метод TakeLast(), который я определил выше, решает проблему, но если вы действительно хотите сделать это без дополнительного метода, то вам просто нужно признать, что в то время как Enumerable.Reverse( ) можно использовать как метод расширения, вам не требуется использовать его таким образом:

List<string> mystring = new List<string>() { "one", "two", "three" }; 
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();

Ответ 3

Примечание. Я пропустил название вопроса, в котором говорилось Использование 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;
    }
}

Ответ 4

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

Ответ 5

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

Ответ 7

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

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

Ответ 8

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

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

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

Ответ 9

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

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

Ответ 10

Если использование сторонней библиотеки является опцией, MoreLinq определяет TakeLast(), которая делает именно это.

Ответ 11

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

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

О производительность: в С# Queue<T> реализуется с помощью круглого буфера, поэтому каждый экземпляр объекта не выполняется (только когда очередь растет). Я не устанавливал емкость очереди (используя выделенный конструктор), потому что кто-то может назвать это расширение с помощью count = int.MaxValue. Для дополнительной производительности вы можете проверить, существует ли реализация источника IList<T>, и если да, то непосредственно извлекайте последние значения с использованием индексов массива.

Ответ 12

Немного неэффективно взять последний N коллекции, используя LINQ, поскольку все вышеперечисленные решения требуют итерации по коллекции. 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});
}

Ответ 13

Я знаю, что поздно ответить на этот вопрос. Но если вы работаете с коллекцией типа IList < > и вам не нужен порядок возвращаемой коллекции, то этот метод работает быстрее. Я использовал ответ Mark Byers и внес небольшие изменения. Итак, теперь метод 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 элементов:

enter image description here

и для получения результатов 1000001 элементов: enter image description here

Ответ 14

Здесь мое решение:

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 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 для не IList`1 основан на том же алгоритме кольцевого буфера, что и в ответах @Mark Byers и @MackieChan. Интересно, насколько они похожи - я написал полностью. Угадайте, что на самом деле есть только один способ правильно выполнить кольцевой буфер.: -)

Глядя на ответ @kbrimington, к этому можно добавить дополнительную проверку для IQuerable<T>, чтобы вернуться к подходу, который хорошо работает с Entity Framework - если предположить, что то, что у меня на данный момент нет.

Ответ 15

Ниже реального примера, как взять последние 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();

Ответ 16

Использование этого метода для получения полного диапазона без ошибок

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