Usi Linq per ottenere gli ultimi N elementi di una raccolta?


284

Data una raccolta, c'è un modo per ottenere gli ultimi N elementi di quella raccolta? Se non esiste un metodo nel framework, quale sarebbe il modo migliore per scrivere un metodo di estensione per farlo?

Risposte:


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

Questo approccio preserva l'ordine degli articoli senza dipendere da alcun ordinamento e ha un'ampia compatibilità tra diversi provider LINQ.

È importante fare attenzione a non chiamare Skipcon un numero negativo. Alcuni provider, come Entity Framework, produrranno ArgumentException se presentati con un argomento negativo. La chiamata aMath.Max evitare questo ordinatamente.

La classe seguente contiene tutti gli elementi essenziali per i metodi di estensione, che sono: una classe statica, un metodo statico e l'uso della thisparola chiave.

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 sulle prestazioni:

Poiché la chiamata a Count()può causare l'enumerazione di determinate strutture di dati, questo approccio ha il rischio di causare due passaggi sui dati. Questo non è davvero un problema con la maggior parte degli enumerabili; in effetti, esistono già ottimizzazioni per elenchi, matrici e persino query EF per valutare l' Count()operazione in tempo O (1).

Se, tuttavia, è necessario utilizzare un enumerabile solo forward e si desidera evitare di effettuare due passaggi, considerare un algoritmo a passaggio singolo come descrivono Lasse V. Karlsen o Mark Byers . Entrambi questi approcci utilizzano un buffer temporaneo per contenere gli elementi durante l'enumerazione, che vengono restituiti una volta trovata la fine della raccolta.


2
+1, poiché funziona in Linq to Entities / SQL. Immagino sia anche più performante in Linq to Objects della strategia di James Curran.
StriplingWarrior il

11
Dipende dalla natura della raccolta. Count () potrebbe essere O (N).
James Curran,

3
@James: assolutamente corretto. Se si tratta strettamente di raccolte IEnumerable, potrebbe trattarsi di una query a due passaggi. Sarei molto interessato a vedere un algoritmo a 1 passaggio garantito. Potrebbe essere utile
kbrimington,

4
Ha fatto alcuni benchmark. Si scopre che LINQ to Objects esegue alcune ottimizzazioni in base al tipo di raccolta che stai utilizzando. Usando array, Lists e LinkedLists, la soluzione di James tende ad essere più veloce, sebbene non per un ordine di grandezza. Se viene calcolato IEnumerable (tramite Enumerable.Range, ad esempio), la soluzione di James richiede più tempo. Non riesco a pensare a un modo per garantire un singolo passaggio senza sapere qualcosa sull'implementazione o copiare i valori in una diversa struttura di dati.
StriplingWarrior il

1
@RedFilter - Abbastanza giusto. Suppongo che le mie abitudini di cercapersone siano trapelate qui. Grazie per il tuo occhio acuto.
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();
}

AGGIORNAMENTO: Per risolvere il problema di clintp: a) L'uso del metodo TakeLast () che ho definito sopra risolve il problema, ma se vuoi davvero farlo senza il metodo extra, devi solo riconoscere che mentre Enumerable.Reverse () può essere utilizzato come metodo di estensione, non è necessario utilizzarlo in questo modo:

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

Il problema che ho con questo, è se dico: List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse(); ottengo un errore del compilatore perché .Reverse () restituisce void e il compilatore sceglie quel metodo invece di quello Linq che restituisce un IEnumerable. Suggerimenti?
Clinton Pierce,

1
È possibile risolvere questo problema eseguendo il cast esplicito di mystring su IEnumerable <String>: ((IEnumerable <String>) mystring) .Reverse (). Take (2) .Reverse ()
Jan Hettich

Facile e abbastanza semplice, ma richiede di invertire l'ordine due volte completamente. Questo può essere il modo migliore
shashwat,

Mi piace oltre alla risposta accettata da Kbrimington. Se non ti interessa l'ordine dopo aver ottenuto gli ultimi Nrecord, puoi saltare il secondo Reverse.
ZoolWay,

@shashwat Non inverte l'ordine due volte "completamente". La seconda inversione si applica solo alla raccolta di N articoli. Inoltre, a seconda dell'implementazione di Reverse (), la prima chiamata ad essa può solo invertire N elementi. (L'implementazione di .NET 4.0 copierà la raccolta in un array e la indicizzerà all'indietro)
James Curran,

47

Nota : ho perso il titolo della domanda che diceva Utilizzo di Linq , quindi la mia risposta non utilizza Linq.

Se si desidera evitare la memorizzazione nella cache di una copia non pigra dell'intera raccolta, è possibile scrivere un metodo semplice che lo fa utilizzando un elenco collegato.

Il seguente metodo aggiungerà ogni valore trovato nella raccolta originale in un elenco collegato e ridimensionerà l'elenco collegato fino al numero di elementi richiesti. Dal momento che mantiene l'elenco collegato a questo numero di elementi per tutto il tempo attraverso l'iterazione della raccolta, manterrà solo una copia di al massimo N elementi della raccolta originale.

Non richiede di conoscere il numero di elementi nella raccolta originale, né di iterarlo più di una volta.

Uso:

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

Metodo di estensione:

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

Penso ancora che tu abbia una risposta valida e valida anche se tecnicamente non utilizza Linq, quindi ti do ancora un +1 :)
Matthew Groves,

+1 pulito, ordinato ed estensibile!
Yasser Shaikh

1
Penso che sia l'unica soluzione che non causa l'esecuzione dell'enumeratore di origine due volte (o più) e non impone la materializzazione dell'enumerazione, quindi nella maggior parte delle applicazioni direi che sarebbe molto più efficiente in termini di memoria e velocità.
Sprotty,

30

Ecco un metodo che funziona su qualsiasi enumerabile ma utilizza solo l'archiviazione temporanea 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();

Funziona utilizzando un buffer ad anello di dimensione N per memorizzare gli elementi come li vede, sovrascrivendo gli elementi vecchi con quelli nuovi. Quando viene raggiunta la fine dell'enumerabile, il buffer ad anello contiene gli ultimi N elementi.


2
+1: questo dovrebbe avere prestazioni migliori del mio, ma dovresti assicurarti che faccia la cosa giusta quando la raccolta contiene meno elementi di n.
Lasse V. Karlsen,

Bene, la maggior parte delle volte suppongo che le persone si prenderanno cura quando copiano il codice da SO per l'uso in produzione per aggiungere tali cose da soli, potrebbe non essere un problema. Se hai intenzione di aggiungerlo, considera di controllare anche la variabile di raccolta per null. Altrimenti, soluzione eccellente :) Stavo pensando di utilizzare un ring buffer da solo, perché un elenco collegato aggiungerà la pressione GC, ma è da un po 'che non lo faccio e non volevo preoccuparmi di test-code per capire se l'ho fatto bene. Devo dire che mi sto innamorando di LINQPad però :) linqpad.net
Lasse V. Karlsen

2
Una possibile ottimizzazione sarebbe quella di verificare se l'enumerabile IList implementato, e utilizzare la soluzione banale se lo fa. L'approccio di archiviazione temporanea sarebbe quindi necessario solo per il vero "streaming" di IEnumerables
piers7

1
banale nit-pick: i tuoi argomenti su ArgumentOutOfRangeException sono nell'ordine sbagliato (dice R #)
piers7

28

.NET Core 2.0+ fornisce il metodo LINQ TakeLast():

https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast

esempio :

Enumerable
    .Range(1, 10)
    .TakeLast(3) // <--- takes last 3 items
    .ToList()
    .ForEach(i => System.Console.WriteLine(i))

// outputs:
// 8
// 9
// 10

Sto usando: NET Standard 2.0 e non l'ho disponibile. Cosa c'è che non va? :(
SuperJMN

@SuperJMN Anche se potresti fare riferimento a librerie .net standard 2.0, potresti non scegliere come target la versione corretta di dotnet core nel tuo progetto. Questo metodo non è disponibile per v1.x ( netcoreapp1.x) ma solo per v2.0 e v2.1 di dotnetcore ( netcoreapp2.x). È possibile che tu possa scegliere come target l'intero framework (ad es. net472) Che non è supportato. (Le librerie standard .net possono essere utilizzate da uno qualsiasi dei precedenti, ma possono esporre solo determinate API specifiche per un framework di destinazione. vedere docs.microsoft.com/en-us/dotnet/standard/frameworks )
Ray

1
Questi devono essere più in alto ora. Non è necessario reinventare la ruota
James Woodley il

11

Sono sorpreso che nessuno lo abbia menzionato, ma SkipWhile ha un metodo che utilizza l'indice dell'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)

L'unico vantaggio percepibile che questa soluzione presenta sugli altri è che puoi avere l'opzione di aggiungere un predicato per creare una query LINQ più potente ed efficiente, invece di avere due operazioni separate che attraversano l'IEnumerable due volte.

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

Utilizzare EnumerableEx.TakeLast nell'assieme System.Interactive di RX. È un'implementazione O (N) come @ Mark's, ma usa una coda piuttosto che un costrutto ring-buffer (e svuota gli elementi quando raggiunge la capacità del buffer).

(NB: questa è la versione di IEnumerable, non la versione di IObservable, sebbene l'implementazione delle due sia praticamente identica)


Questa è la risposta migliore Non esagerare se esiste una libreria adatta che fa il lavoro e il team RX è di alta qualità.
bradgonesurfing,

Se stai andando con questo, installalo da Nuget - nuget.org/packages/Ix-Async
nikib3ro

C # non è Queue<T>implementato usando un buffer circolare ?
Tigrou,

@tigrou. no, non è circolare
citykid


6

Se si ha a che fare con una raccolta con una chiave (ad es. Voci da un database), una soluzione rapida (ovvero più veloce della risposta selezionata) sarebbe

collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);

+1 funziona per me ed è facile da leggere, ho un piccolo numero di oggetti nella mia lista
fubo,

5

Se non ti dispiace immergerti in Rx come parte della monade, puoi usare TakeLast:

IEnumerable<int> source = Enumerable.Range(1, 10000);

IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();

2
Non hai bisogno di AsObservable () se fai riferimento a System.Interactive di RX invece di System.Reactive (vedi la mia risposta)
piers7

2

Se l'utilizzo di una libreria di terze parti è un'opzione, MoreLinq definisce TakeLast()quale fa esattamente questo.


2

Ho provato a combinare efficienza e semplicità e alla fine con questo:

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

Informazioni sulle prestazioni: in C #, Queue<T>è implementato usando un buffer circolare in modo che non vi siano istanze di oggetti eseguite su ogni ciclo (solo quando la coda sta crescendo). Non ho impostato la capacità della coda (usando un costruttore dedicato) perché qualcuno potrebbe chiamare questa estensione con count = int.MaxValue. Per prestazioni extra, è possibile verificare se implementa l'origine IList<T>e, in caso affermativo, estrarre direttamente gli ultimi valori utilizzando gli indici di array.


1

È un po 'inefficiente prendere l'ultima N di una raccolta usando LINQ poiché tutte le soluzioni sopra richiedono iterare in tutta la raccolta. TakeLast(int n)nelSystem.Interactive ha anche questo problema.

Se hai un elenco, una cosa più efficiente da fare è dividerlo usando il seguente metodo

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

e alcuni casi di test

[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

So che è troppo tardi per rispondere a questa domanda. Ma se stai lavorando con una raccolta di tipo IList <> e non ti interessa un ordine della raccolta restituita, questo metodo funziona più velocemente. Ho usato la risposta di Mark Byers e ho apportato alcune modifiche. Quindi ora il metodo 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];
        }
    }
}

Per il test ho usato il metodo Mark Byers e la risposta di kbrimington . Questo è test:

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();

E qui ci sono risultati per prendere 10 elementi:

inserisci qui la descrizione dell'immagine

e per prendere 1000001 elementi i risultati sono: inserisci qui la descrizione dell'immagine


1

Ecco la mia soluzione:

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();
            }
        }
    }
}

Il codice è un po 'grosso, ma come componente riutilizzabile drop-in, dovrebbe funzionare come nella maggior parte degli scenari e manterrà il codice che lo utilizza in modo piacevole e conciso. :-)

Mio TakeLastper nonIList`1 si basa sullo stesso algoritmo ring buffer di quello nelle risposte di @Mark Byers e @MackieChan più avanti. È interessante quanto siano simili - ho scritto il mio in modo completamente indipendente. Immagino che ci sia davvero solo un modo per eseguire correttamente un buffer ad anello. :-)

Guardando la risposta di @ kbrimington, si potrebbe aggiungere un ulteriore controllo per ricorrere IQuerable<T>all'approccio che funziona bene con Entity Framework, supponendo che ciò che ho a questo punto non lo faccia.


0

Di seguito il vero esempio su come prendere gli ultimi 3 elementi da una raccolta (array):

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

Utilizzando questo metodo per ottenere tutti gli intervalli senza errori

 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

Implementazione leggermente diversa con l'utilizzo del buffer circolare. I benchmark mostrano che il metodo è circa due volte più veloce di quelli che usano Queue (implementazione di TakeLast in System.Linq ), ma non senza un costo - ha bisogno di un buffer che cresce insieme al numero richiesto di elementi, anche se si ha un piccola raccolta è possibile ottenere un'enorme allocazione di 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];

    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.