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

284

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?

Matthew Groves
fuente

Respuestas:

422
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 Skipcon 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 thispalabra 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.

kbrimington
fuente
2
+1, ya que esto funciona en Linq to Entities / SQL. Supongo que también es más eficaz en Linq to Objects que la estrategia de James Curran.
StriplingWarrior
11
Depende de la naturaleza de la colección. Count () podría ser O (N).
James Curran
3
@ James: Absolutamente correcto. Si se trata estrictamente de colecciones IEnumerable, esta podría ser una consulta de dos pasos. Estaría muy interesado en ver un algoritmo garantizado de 1 pase. Puede ser útil
kbrimington
44
Hice algunos puntos de referencia. Resulta que LINQ to Objects realiza algunas optimizaciones basadas en el tipo de colección que está utilizando. Usando matrices, Listsy LinkedLists, 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.
StriplingWarrior
1
@RedFilter - Bastante justo. Supongo que mis hábitos de búsqueda se filtraron aquí. Gracias por tu buen ojo.
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();
}

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();
James Curran
fuente
El problema que tengo con esto es si digo: 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?
Clinton Pierce
1
Puede resolver este problema al enviar explícitamente mystring a IEnumerable <String>: ((IEnumerable <String>) mystring) .Reverse (). Take (2) .Reverse ()
Jan Hettich
Fácil y simple, pero requiere invertir el orden dos veces por completo. Esta puede ser la mejor manera
shashwat
Me gusta además de la respuesta aceptada de kbrimington. Si no le importa el pedido después de tener los últimos Nregistros, puede omitir el segundo Reverse.
ZoolWay
@shashwat No invierte el orden dos veces "completamente". La segunda inversión solo se aplica a la colección de N artículos. Además, dependiendo de cómo se implemente Reverse (), la primera llamada solo puede revertir N elementos. (La implementación de .NET 4.0 copiará la colección en una matriz y la indexará hacia atrás)
James Curran,
47

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;
    }
}
Lasse V. Karlsen
fuente
Todavía creo que tienes una respuesta buena y válida, incluso si técnicamente no está usando Linq, así que todavía te doy un +1 :)
Matthew Groves
¡limpio, ordenado y extensible +1!
Yasser Shaikh
1
Creo que es la única solución que no hace que el enumerador de origen se ejecute dos veces (o más), y no fuerza la materialización de la enumeración, por lo que en la mayoría de las aplicaciones diría que sería mucho más eficiente en términos de memoria y velocidad.
Sprotty
30

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.

Mark Byers
fuente
2
+1: Esto debería tener un mejor rendimiento que el mío, pero debe asegurarse de que hace lo correcto cuando la colección contiene menos elementos que n.
Lasse V. Karlsen
Bueno, la mayoría de las veces supongo que las personas se ocuparán de copiar el código de SO para su uso en producción para agregar esas cosas por sí mismas, puede que no sea un problema. Si va a agregarlo, considere también verificar la variable de colección para nulo. De lo contrario, una solución excelente :) Estaba considerando usar un buffer de anillo yo mismo, porque una lista vinculada agregará presión GC, pero ha pasado un tiempo desde que hice uno y no quería molestarme con el código de prueba para descubrir si lo hice bien Debo decir que me estoy enamorando de LINQPad :) linqpad.net
Lasse V. Karlsen
2
Una posible optimización sería verificar si la IList implementada enumerable, y usar la solución trivial si lo hace. El enfoque de almacenamiento temporal solo sería necesario para una verdadera 'transmisión' de IEnumerables
piers7
1
trivial nit-pick: sus argumentos a ArgumentOutOfRangeException están en el orden incorrecto (R # dice)
piers7
28

.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
Rayo
fuente
Estoy usando: NET Standard 2.0 y no lo tengo disponible. Que pasa :(
SuperJMN
@SuperJMN Aunque puede estar haciendo referencia a bibliotecas .net standard 2.0, es posible que no esté apuntando a la versión correcta de dotnet core en su proyecto. Este método no está disponible para v1.x ( 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 )
Ray
1
Estos deben estar más arriba ahora. No es necesario reinventar la rueda
James Woodley
11

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));
}
Nick Babcock
fuente
9

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)

muelles7
fuente
Esta es la mejor respuesta. No utilice el suyo si hay una biblioteca adecuada que haga el trabajo y el equipo de RX sea de alta calidad.
bradgonesurfing
Si va con esto, instálelo desde Nuget - nuget.org/packages/Ix-Async
nikib3ro
¿No se Queue<T>implementa C # usando un búfer circular ?
tigrou
@tigrou. no, no es circular
citykid
6

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);
dav_i
fuente
+1 funciona para mí y es fácil de leer, tengo una pequeña cantidad de objetos en mi lista
fubo
5

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();
Richard Szalay
fuente
2
No es necesario AsObservable () si hace referencia a System.Interactive de RX en lugar de System.Reactive (véase mi respuesta)
piers7
2

Si usar una biblioteca de terceros es una opción, MoreLinq define TakeLast()qué hace exactamente esto.

sm
fuente
2

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.

tigrou
fuente
1

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});
}
bradgonesurfing
fuente
1

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:

ingrese la descripción de la imagen aquí

y para tomar 1000001 elementos los resultados son: ingrese la descripción de la imagen aquí

Sasha
fuente
1

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 TakeLastpara 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.

Jonathan Gilbert
fuente
0

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();
Aleksey Timkov
fuente
0

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 ;
        }
Ali asghar Fendereski
fuente
0

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

    }
}
Romasz
fuente