Dada una colección, ¿hay alguna forma de obtener los últimos N elementos de esa colección? Si no hay un método en el marco, ¿cuál sería la mejor manera de escribir un método de extensión para hacer esto?
Dada una colección, ¿hay alguna forma de obtener los últimos N elementos de esa colección? Si no hay un método en el marco, ¿cuál sería la mejor manera de escribir un método de extensión para hacer esto?
Respuestas:
collection.Skip(Math.Max(0, collection.Count() - N));
Este enfoque conserva el orden de los artículos sin depender de ninguna clasificación y tiene una amplia compatibilidad entre varios proveedores de LINQ.
Es importante tener cuidado de no llamar Skip
con un número negativo. Algunos proveedores, como el Entity Framework, producirán una excepción ArgumentException cuando se les presente un argumento negativo. La llamada aMath.Max
evita esto perfectamente.
La siguiente clase tiene todos los elementos esenciales para los métodos de extensión, que son: una clase estática, un método estático y el uso de la this
palabra clave.
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));
}
}
Una breve nota sobre el rendimiento:
Debido a que la llamada a Count()
puede causar la enumeración de ciertas estructuras de datos, este enfoque tiene el riesgo de causar dos pases sobre los datos. Esto no es realmente un problema con la mayoría de los enumerables; de hecho, ya existen optimizaciones para Listas, Arreglos e incluso consultas EF para evaluar la Count()
operación en tiempo O (1).
Sin embargo, si debe usar un enumerable solo hacia adelante y desea evitar hacer dos pases, considere un algoritmo de un solo paso como describen Lasse V. Karlsen o Mark Byers . Ambos enfoques utilizan un búfer temporal para contener elementos durante la enumeración, que se obtienen una vez que se encuentra el final de la colección.
List
sy LinkedList
s, la solución de James tiende a ser más rápida, aunque no por un orden de magnitud. Si se calcula IEnumerable (a través de Enumerable.Range, por ejemplo), la solución de James lleva más tiempo. No se me ocurre ninguna forma de garantizar una sola pasada sin saber algo sobre la implementación o copiar valores en una estructura de datos diferente.
coll.Reverse().Take(N).Reverse().ToList();
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
return coll.Reverse().Take(N).Reverse();
}
ACTUALIZACIÓN: Para abordar el problema de clintp: a) El uso del método TakeLast () que definí anteriormente resuelve el problema, pero si realmente quieres hacerlo sin el método adicional, entonces solo tienes que reconocerlo mientras Enumerable.Reverse () puede ser utilizado como método de extensión, no es necesario que lo use de esa manera:
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();
me sale un error del compilador porque .Reverse () devuelve void y el compilador elige ese método en lugar del método Linq que devuelve un IEnumerable. Sugerencias?
N
registros, puede omitir el segundo Reverse
.
Nota : Me perdí el título de su pregunta que decía Usar Linq , por lo que mi respuesta no usa Linq.
Si desea evitar el almacenamiento en caché de una copia no perezosa de toda la colección, puede escribir un método simple que lo haga utilizando una lista vinculada.
El siguiente método agregará cada valor que encuentre en la colección original en una lista vinculada y recortará la lista vinculada a la cantidad de elementos necesarios. Como mantiene la lista vinculada ajustada a este número de elementos todo el tiempo a través de la iteración a través de la colección, solo mantendrá una copia de como máximo N elementos de la colección original.
No requiere que sepa la cantidad de elementos en la colección original, ni que repita más de una vez.
Uso:
IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
Método de extensión:
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;
}
}
Aquí hay un método que funciona en cualquier enumerable pero usa solo almacenamiento temporal 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];
}
}
}
Uso:
List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();
Funciona mediante el uso de un búfer de anillo de tamaño N para almacenar los elementos tal como los ve, sobrescribiendo los elementos antiguos con los nuevos. Cuando se alcanza el final de lo enumerable, el buffer de anillo contiene los últimos N elementos.
n
.
.NET Core 2.0+ proporciona el método LINQ TakeLast()
:
https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast
ejemplo :
Enumerable
.Range(1, 10)
.TakeLast(3) // <--- takes last 3 items
.ToList()
.ForEach(i => System.Console.WriteLine(i))
// outputs:
// 8
// 9
// 10
netcoreapp1.x
) sino solo para v2.0 y v2.1 de dotnetcore ( netcoreapp2.x
). Es posible que tenga como objetivo el marco completo (p net472
. Ej. ) Que tampoco es compatible. (Las bibliotecas estándar de .net pueden ser utilizadas por cualquiera de los anteriores, pero solo pueden exponer ciertas API específicas a un marco de destino. ver docs.microsoft.com/en-us/dotnet/standard/frameworks )
Me sorprende que nadie lo haya mencionado, pero SkipWhile sí tiene un método que utiliza el índice del elemento .
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)
El único beneficio perceptible que esta solución presenta sobre otros es que puede tener la opción de agregar un predicado para hacer una consulta LINQ más potente y eficiente, en lugar de tener dos operaciones separadas que atraviesen el IEnumerable dos veces.
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));
}
Use EnumerableEx.TakeLast en el sistema de RX. Ensamblaje interactivo. Es una implementación O (N) como @ Mark's, pero utiliza una cola en lugar de una construcción de anillo de búfer (y retira los elementos cuando alcanza la capacidad de búfer).
(Nota: esta es la versión IEnumerable, no la versión IObservable, aunque la implementación de las dos es bastante idéntica)
Queue<T>
implementa C # usando un búfer circular ?
Si se trata de una colección con una clave (por ejemplo, entradas de una base de datos), una solución rápida (es decir, más rápida que la respuesta seleccionada) sería
collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
Si no te importa sumergirte en Rx como parte de la mónada, puedes usar TakeLast
:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
Intenté combinar eficiencia y simplicidad y terminar con esto:
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;
}
Acerca del rendimiento: en C #, Queue<T>
se implementa utilizando un búfer circular para que no se ejecute ninguna instancia de objeto en cada bucle (solo cuando la cola está creciendo). No configuré la capacidad de la cola (usando un constructor dedicado) porque alguien podría llamar a esta extensión con count = int.MaxValue
. Para obtener un rendimiento adicional, puede verificar si la fuente se implementa IList<T>
y, en caso afirmativo, extraer directamente los últimos valores utilizando índices de matriz.
Es un poco ineficiente tomar el último N de una colección usando LINQ ya que todas las soluciones anteriores requieren iterar en toda la colección. TakeLast(int n)
enSystem.Interactive
también tiene este problema.
Si tiene una lista, lo más eficiente es cortarla con el siguiente método
/// 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);
}
con
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;
}
y algunos casos de prueba
[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});
}
Sé que es demasiado tarde para responder esta pregunta. Pero si está trabajando con una colección de tipo IList <> y no le importa un orden de la colección devuelta, entonces este método funciona más rápido. Utilicé la respuesta de Mark Byers e hice algunos pequeños cambios. Entonces, el método TakeLast es:
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];
}
}
}
Para la prueba he usado el método Mark Byers y kbrimington's andswer . Esta es la prueba:
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();
Y aquí están los resultados para tomar 10 elementos:
y para tomar 1000001 elementos los resultados son:
Aquí está mi solución:
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();
}
}
}
}
El código es un poco grueso, pero como componente reutilizable, debe funcionar tan bien como puede en la mayoría de los escenarios, y mantendrá el código que lo está usando de manera agradable y concisa. :-)
Mi TakeLast
para noIList`1
se basa en el mismo algoritmo de buffer de anillo que el de las respuestas de @Mark Byers y @MackieChan más arriba. Es interesante lo similares que son: escribí el mío de forma completamente independiente. Supongo que en realidad solo hay una manera de hacer un buffer de anillo correctamente. :-)
Mirando la respuesta de @kbrimington, se podría agregar una verificación adicional para IQuerable<T>
volver al enfoque que funciona bien con Entity Framework, suponiendo que lo que tengo en este momento no.
Debajo del ejemplo real de cómo tomar los últimos 3 elementos de una colección (matriz):
// 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();
Usando este método para obtener todo el rango sin error
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 ;
}
Implementación poco diferente con el uso de búfer circular. Los puntos de referencia muestran que el método es aproximadamente dos veces más rápido que los que usan Queue (implementación de TakeLast en System.Linq ), pero no sin costo: necesita un búfer que crece junto con el número de elementos solicitados, incluso si tiene un pequeña colección puede obtener una gran asignación de memoria.
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];
}
}