Використовуючи 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()може спричинити перерахування певних структур даних, такий підхід має ризик викликати два передачі даних. Це насправді не проблема більшості перелічених номерів; насправді оптимізації вже існують для запитів Списків, масивів і навіть EF для оцінки Count()операції в O (1) час.

Якщо, однак, ви повинні використовувати перелічені лише вперед і хотіли б уникнути двох проходів, розгляньте алгоритм з одним проходом, як описують Lasse V. Karlsen або Mark Byers . Обидва ці підходи використовують тимчасовий буфер для зберігання елементів під час перерахування, які отримуються, коли знайдеться кінець колекції.


2
+1, як це працює в Linq to Entities / SQL. Я здогадуюсь, що вона також більш ефективною в Linq to Objects, ніж стратегія Джеймса Куррана.
StriplingWarrior

11
Залежить від характеру колекції. Count () може бути O (N).
Джеймс Курран

3
@James: Абсолютно правильно. Якщо ви маєте чіткий порядок з колекціями IEsumerable, це може бути запит на два переходи. Мені б дуже цікаво побачити гарантований алгоритм 1 проходу. Це може бути корисно.
kbrimington

4
Зробив кілька орієнтирів. Виявляється, LINQ to Objects виконує деякі оптимізації на основі типу колекції, яку ви використовуєте. Використовуючи масиви, Lists і LinkedLists, рішення Джеймса, як правило, швидше, хоча і не на порядок. Якщо обчислюється IEnumerable (через Enumerable.Range, наприклад), рішення Джеймса займає більше часу. Я не можу придумати жодного способу гарантувати один пропуск, не знаючи нічого про реалізацію чи копіювання значень в іншу структуру даних.
Стриптинг-воїн

1
@RedFilter - досить справедливо. Я гадаю, що тут просочилися мої звички. Дякую за пильне око.
kbrimington

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 () повертає нікчемність, і компілятор вибирає цей метод замість Linq, який повертає IEnumerable. Пропозиції?
Клінтон Пірс

1
Ви можете вирішити цю проблему, явно відкинувши mystring до IEnumerable <String>: ((IEnumerable <String>) mystring) .Reverse (). Take (2) .Reverse ()
Ян Хеттіч

Досить просто і просто, але вимагає повернення замовлення два рази повністю. Це може бути найкращим способом
shashwat

Мені це подобається, крім прийнятої відповіді від kbrimington. Якщо ви не дбаєте про замовлення після того, як у вас є останні Nзаписи, ви можете пропустити другий Reverse.
ZoolWay

@shashwat Це не скасовує замовлення двічі "повністю". Другий розворот стосується лише колекції 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

чистий, акуратний та розширюваний +1!
Ясер Шайх

1
Я думаю, що це єдине рішення, яке не змушує запускати нумеру джерела двічі (або більше) і не змушує матеріалізувати перерахування, тому в більшості застосунків я б сказав, що це було б набагато ефективніше з точки зору пам'яті та швидкості.
Sprotty

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 для зберігання елементів так, як їх бачить, перезаписуючи старі елементи новими. Коли кінець нумерації досягається, буфер кільця містить останні N елементів.


2
+1: Це має мати кращі показники, ніж у мене, але ви повинні переконатися, що він робить правильно, коли колекція містить менше елементів, ніж n.
Лассе В. Карлсен

Ну, більшість часу я припускаю, що люди будуть дбати про копіювання коду з SO для виробничого використання, щоб додати такі речі самі, це може не бути проблемою. Якщо ви збираєтесь додати його, також перевірте зміну колекції на null. В іншому випадку відмінне рішення :) Я розглядав можливість використання кільця-буфера самостійно, тому що зв'язаний список додасть тиску GC, але минув такий час, як я його зробив, і мені не хотілося займатись тестовим кодом, щоб розібратися якби я зробив це правильно. Треба сказати, що я закохався в LINQPad, хоча :) linqpad.net
Lasse V. Karlsen

2
Можливою оптимізацією було б перевірити, чи перелічено реалізований IList, і використовувати тривіальне рішення, якщо воно є. Тоді підхід до тимчасового зберігання потрібен був би лише для справжнього "потокового" IEnumerables
piers7

1
тривіальний ніт-підбір: ваші аргументи до 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), але лише для v2.0 та v2.1 dotnetcore ( netcoreapp2.x). Можливо, ви можете орієнтуватися на повний фреймворк (наприклад net472), який також не підтримується. (.net стандартні лібри можуть бути використані будь-яким із перерахованих вище, але можуть відкривати лише певні API, характерні для цільової рамки. див. docs.microsoft.com/en-us/dotnet/standard/frameworks )
Рей

1
Зараз вони повинні бути вище. Не потрібно заново вигадувати колесо
Джеймс Вудлі

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)

Єдиною помітною перевагою, яку це рішення представляє перед іншими, є те, що ви можете мати можливість додавати в предикат, щоб зробити більш потужний і ефективний запит LINQ, замість того, щоб проводити дві окремі операції, які проходять IE Численні двічі.

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

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

(Примітка. Це версія IEnumerable - не версія IObservable, хоча реалізація цих двох майже однакова)


Це найкраща відповідь. Не робіть свою власну, якщо є відповідна бібліотека, яка виконує цю роботу, а команда RX - висока якість.
bradgonesurfing

Якщо ви збираєтеся це зробити, встановіть його з Nuget - nuget.org/packages/Ix-Async
nikib3ro

Чи не Queue<T>реалізовано C # за допомогою кругового буфера ?
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
Вам не потрібен AsObservable (), якщо ви посилаєтесь на System.Interactive RX замість System.Reactive (див. Мою відповідь)
piers7

2

Якщо використання сторонньої бібліотеки є опцією, MoreLinq визначає, TakeLast()що саме це робить.


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

Трохи неефективно приймати останній 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});
}

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

Для тесту я використовував метод Марка Байєрса та kbrimington's 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();
            }
        }
    }
}

Код трохи кумедний, але як компонент для багаторазового використання, який використовується для повторного використання, він повинен працювати так само добре, як у більшості сценаріїв, і він буде зберігати код, який використовує його красиво і стисло. :-)

Моє TakeLastдля не IList`1- базується на тому ж алгоритмі буферного дзвінка, що і у відповідях @Mark Byers та @MackieChan далі. Цікаво, наскільки вони схожі - я написав свою повністю самостійно. Здогадайтесь, що дійсно є лише один спосіб правильно зробити буфер дзвінка. :-)

Дивлячись на відповідь @ kbrimington, до цього може бути додана додаткова перевірка, IQuerable<T>щоб повернутися до підходу, який добре працює з Entity Framework - припускаючи, що те, що я маю на даний момент, не відповідає.


0

Нижче наведено реальний приклад, як взяти три останні елементи з колекції (масиву):

// 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

Трохи інша реалізація з використанням кругового буфера. Тести показують, що метод є вдвічі швидшим, ніж ті, що використовують чергу (реалізація TakeLast в System.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];

    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.