Для данной коллекции есть ли способ получить последние 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()
может вызвать перечисление определенных структур данных, такой подход может вызвать два прохода по данным. На самом деле это не проблема для большинства перечислимых; фактически уже существуют оптимизации для списков, массивов и даже запросов EF для оценки Count()
операции за O (1) времени.
Однако, если вы должны использовать перечислимое только для пересылки и хотели бы избежать двухпроходного прохождения, рассмотрите однопроходный алгоритм, как описывают Лассе В. Карлсен или Марк Байерс . Оба этих подхода используют временный буфер для хранения элементов при перечислении, которые выдаются после того, как будет найден конец коллекции.
List
s и LinkedList
s, решение Джеймса имеет тенденцию быть быстрее, хотя и не на порядок. Если рассчитывается IEnumerable (например, через Enumerable.Range), решение Джеймса занимает больше времени. Я не могу придумать способа гарантировать один проход, не зная ничего о реализации или не копируя значения в другую структуру данных.
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 () возвращает void, и компилятор выбирает этот метод вместо метода Linq, который возвращает IEnumerable. Предложения?
N
записи, вы можете пропустить вторую Reverse
.
Примечание : я пропустил заголовок вашего вопроса, который гласил « Использование 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 элементов.
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
netcoreapp1.x
), но только для v2.0 и v2.1 функции dotnetcore ( netcoreapp2.x
). Возможно, вы нацелены на полную структуру (например net472
), которая также не поддерживается. (Стандартные библиотеки .net могут использоваться любым из вышеперечисленных, но могут предоставлять только определенные API-интерфейсы, специфичные для целевой платформы. См. docs.microsoft.com/en-us/dotnet/standard/frameworks )
Я удивлен, что никто не упомянул об этом, но у 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));
}
Используйте EnumerableEx.TakeLast в сборке System.Interactive RX. Это реализация O (N), как у @ Mark, но она использует очередь, а не конструкцию кольцевого буфера (и удаляет элементы, когда она достигает емкости буфера).
(Примечание: это версия IEnumerable, а не версия IObservable, хотя реализация этих двух программ практически идентична)
Queue<T>
реализован с использованием циклического буфера ?
Если вы имеете дело с коллекцией с ключом (например, записи из базы данных), быстрое (т.е. быстрее, чем выбранный ответ) решение будет
collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
Если вы не против погрузиться в Rx как часть монады, вы можете использовать TakeLast
:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
Я попытался объединить эффективность и простоту и в конечном итоге с этим:
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>
и если да, напрямую извлечь последние значения, используя индексы массива.
Немного неэффективно брать последние 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});
}
Я знаю, что уже поздно отвечать на этот вопрос. Но если вы работаете с коллекцией типа 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];
}
}
}
Для теста я использовал метод Марка Байерса и ответ Кбримингтона . Это тест:
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 элементов получаются следующие результаты:
Вот мое решение:
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 Байерс и @MackieChan дальше. Интересно, насколько они похожи - я написал абсолютно самостоятельно. Думаю, на самом деле есть только один способ правильно создать кольцевой буфер. :-)
Глядя на ответ @ kbrimington, можно добавить дополнительную проверку, IQuerable<T>
чтобы вернуться к подходу, который хорошо работает с Entity Framework - при условии, что то, что у меня есть на данный момент, не так.
Ниже приведен реальный пример того, как взять последние 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();
Используя этот метод, чтобы получить весь диапазон без ошибок
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 ;
}
Немного отличается реализация с использованием кольцевого буфера. Тесты показывают, что метод примерно в два раза быстрее, чем те, которые используют Queue (реализация 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];
}
}