¿Usar Linq para obtener los últimos N elementos de una colección?

Dada una colección, ¿hay alguna manera 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?

collection.Skip(Math.Max(0, collection.Count() - N)); 

Este enfoque conserva el orden de los elementos sin depender de ninguna clasificación, y tiene una amplia compatibilidad con varios proveedores de LINQ.

Es importante tener cuidado de no llamar a Skip con un número negativo. Algunos proveedores, como Entity Framework, producirán una ArgumentException cuando se presente un argumento negativo. La llamada a Math.Max evita esto prolijamente.

La clase a continuación 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 this palabra clave.

 public static class MiscExtensions { // Ex: collection.TakeLast(5); public static IEnumerable TakeLast(this IEnumerable 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 pasadas sobre los datos. Esto no es realmente un problema con la mayoría de los enumerables; de hecho, ya existen optimizaciones para listas, matrices e incluso consultas EF para evaluar la operación Count() en O (1) tiempo.

Sin embargo, si debe usar un enumerable de solo reenvío y desea evitar hacer dos pases, considere un algoritmo de pasada única como describen Lasse V. Karlsen o Mark Byers . Ambos enfoques utilizan un búfer temporal para contener elementos mientras se enumeran, que se producen una vez que se encuentra el final de la colección.

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

ACTUALIZACIÓN: Para abordar el problema de clintp: a) Usar el método TakeLast () que he definido anteriormente resuelve el problema, pero si realmente quieres hacerlo sin el método extra, entonces solo tienes que reconocer que, mientras que Enumerable.Reverse () puede ser utilizado como método de extensión, no está obligado a usarlo de esa manera:

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

Nota : Me perdí el título de su pregunta que decía ” Usar Linq” , por lo que mi respuesta en realidad 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 mediante 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 hasta la cantidad de elementos requeridos. Como mantiene la lista vinculada recortada a este número de elementos todo el tiempo al recorrer la colección, solo conservará una copia de un máximo de N elementos de la colección original.

No requiere que conozca la cantidad de elementos en la colección original ni itere sobre ella más de una vez.

Uso:

 IEnumerable sequence = Enumerable.Range(1, 10000); IEnumerable last10 = sequence.TakeLast(10); ... 

Método de extensión:

 public static class Extensions { public static IEnumerable TakeLast(this IEnumerable collection, int n) { if (collection == null) throw new ArgumentNullException("collection"); if (n < 0) throw new ArgumentOutOfRangeException("n", "n must be 0 or greater"); LinkedList temp = new LinkedList(); foreach (var value in collection) { temp.AddLast(value); if (temp.Count > n) temp.RemoveFirst(); } return temp; } } 

Este es un método que funciona en cualquier enumerable pero que solo usa O (N) almacenamiento temporal:

 public static class TakeLastExtension { public static IEnumerable TakeLast(this IEnumerable 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 l = new List {4, 6, 3, 6, 2, 5, 7}; List lastElements = l.TakeLast(3).ToList(); 

Funciona mediante el uso de un buffer de anillo de tamaño N para almacenar los elementos como los ve, sobrescribiendo los elementos antiguos con los nuevos. Cuando se alcanza el final del enumerable, el buffer de anillo contiene los últimos N elementos.

Me sorprende que nadie lo haya mencionado, pero SkipWhile tiene un método que usa el índice del elemento .

 public static IEnumerable TakeLastN(this IEnumerable 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 presenta esta solución sobre los demás 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 atraviesan IEnumerable dos veces.

 public static IEnumerable FilterLastN(this IEnumerable source, int n, Predicate pred) { int goldenIndex = source.Count() - n; return source.SkipWhile((val, index) => index < goldenIndex && pred(val)); } 

Use EnumerableEx.TakeLast en el ensamblaje System.Interactive de RX. Es una implementación de O (N) como @ Mark, pero usa una cola en lugar de una construcción de búfer de anillo (y elimina elementos cuando alcanza la capacidad de búfer).

(NB: Esta es la versión IEnumerable, no la versión IObservable, aunque la implementación de las dos es prácticamente idéntica)

.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 

Si no te importa sumergir en Rx como parte de la mónada, puedes usar TakeLast :

 IEnumerable source = Enumerable.Range(1, 10000); IEnumerable lastThree = source.AsObservable().TakeLast(3).AsEnumerable(); 

Si está tratando con 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 usar una biblioteca de terceros es una opción, MoreLinq define TakeLast() que hace exactamente esto.

Traté de combinar eficiencia y simplicidad y terminar con esto:

 public static IEnumerable TakeLast(this IEnumerable source, int count) { if (source == null) { throw new ArgumentNullException("source"); } Queue lastElements = new Queue(); foreach (T element in source) { lastElements.Enqueue(element); if (lastElements.Count > count) { lastElements.Dequeue(); } } return lastElements; } 

Acerca del rendimiento: en C #, Queue se implementa con un búfer circular, por lo que no se crea instanciación de objetos en cada bucle (solo cuando la cola está creciendo). No configuré la capacidad de la cola (usando el constructor dedicado) porque alguien podría llamar a esta extensión con count = int.MaxValue . Para un rendimiento adicional, puede comprobar si el origen implementa IList y, en caso afirmativo, extraer directamente los últimos valores utilizando índices de matriz.

Es un poco ineficiente tomar la última N de una colección usando LINQ ya que todas las soluciones anteriores requieren iteración a través de la colección. TakeLast(int n) en System.Interactive también tiene este problema.

Si tiene una lista, una cosa más eficiente es dividirla usando el siguiente método

 /// Select from start to end exclusive of end using the same semantics /// as python slice. ///  the list to slice /// The starting index /// The ending index. The result does not include this index public static List Slice (this IReadOnlyList 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(); } throw new IndexOutOfRangeException( "count = " + list.Count() + " start = " + start + " end = " + end); } 

con

 public static List GetRange( this IReadOnlyList list, int index, int count ) { List r = new List(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 l = new List() { 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() { 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 a esta pregunta. Pero si está trabajando con una colección de tipo IList <> y no le importa un pedido de la colección devuelta, entonces este método funciona más rápido. He usado la respuesta de Mark Byers e hice algunos cambios. Entonces, ahora el método TakeLast es:

 public static IEnumerable TakeLast(IList 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 una prueba:

 IList test = new List(); for(int i = 0; i<1000000; i++) { test.Add(i); } Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); IList result = TakeLast(test, 10).ToList(); stopwatch.Stop(); Stopwatch stopwatch1 = new Stopwatch(); stopwatch1.Start(); IList result1 = TakeLast2(test, 10).ToList(); stopwatch1.Stop(); Stopwatch stopwatch2 = new Stopwatch(); stopwatch2.Start(); IList result2 = test.Skip(Math.Max(0, test.Count - 10)).Take(10).ToList(); stopwatch2.Stop(); 

Y aquí hay resultados para tomar 10 elementos:

enter image description here

y para tomar 1000001 elementos los resultados son: enter image description here

Aquí está mi solución:

 public static class EnumerationExtensions { public static IEnumerable TakeLast(this IEnumerable input, int count) { if (count <= 0) yield break; var inputList = input as IList; 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 SkipLast(this IEnumerable input, int count) { if (count <= 0) return input; else return input.SkipLastIter(count); } private static IEnumerable SkipLastIter(this IEnumerable input, int count) { var inputList = input as IList; 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 elements = new Queue(); 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 un componente reutilizable, debe funcionar tan bien como le sea posible en la mayoría de los escenarios, y mantendrá el código que lo usa de manera agradable y concisa. 🙂

My TakeLast for TakeLastIList`1 se basa en el mismo algoritmo de buffer de anillo que en las respuestas de @Mark Byers y @MackieChan más arriba. Es interesante lo similares que son: escribí el mío de manera completamente independiente. Supongo que realmente hay una sola forma de hacer un buffer de anillo correctamente. 🙂

En cuanto a la respuesta de @kbrimington, se podría agregar una verificación adicional a esto para que IQuerable al enfoque que funciona bien con Entity Framework, suponiendo que lo que tengo en este momento no lo haga.

Debajo del ejemplo real 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(); 

Usar este método para obtener todo el rango sin errores

  public List GetTsRate( List AllT,int Index,int Count) { List Ts = null; try { Ts = AllT.ToList().GetRange(Index, Count); } catch (Exception ex) { Ts = AllT.Skip(Index).ToList(); } return Ts ; }