Utiliser Linq pour obtenir les N derniers éléments d'une collection?


284

Étant donné une collection, existe-t-il un moyen d'obtenir les N derniers éléments de cette collection? S'il n'y a pas de méthode dans le framework, quelle serait la meilleure façon d'écrire une méthode d'extension pour ce faire?

Réponses:


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

Cette approche préserve l'ordre des éléments sans dépendre d'aucun tri et a une large compatibilité entre plusieurs fournisseurs LINQ.

Il est important de ne pas appeler Skipavec un numéro négatif. Certains fournisseurs, tels que Entity Framework, produisent une ArgumentException lorsqu'ils sont présentés avec un argument négatif. L'appel à Math.Maxévite cela proprement.

La classe ci-dessous contient tous les éléments essentiels des méthodes d'extension, qui sont: une classe statique, une méthode statique et l'utilisation du thismot clé.

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

Une brève note sur les performances:

Étant donné que l'appel à Count()peut provoquer l'énumération de certaines structures de données, cette approche présente le risque de provoquer deux passages sur les données. Ce n'est pas vraiment un problème avec la plupart des énumérables; en fait, des optimisations existent déjà pour les requêtes Lists, Arrays et même EF pour évaluer l' Count()opération en temps O (1).

Si, toutefois, vous devez utiliser un énumérable uniquement vers l'avant et que vous souhaitez éviter d'effectuer deux passes, envisagez un algorithme à une passe comme Lasse V. Karlsen ou Mark Byers le décrivent. Ces deux approches utilisent un tampon temporaire pour conserver les éléments lors de l'énumération, qui sont générés une fois la fin de la collection trouvée.


2
+1, car cela fonctionne dans Linq to Entities / SQL. Je suppose que c'est aussi plus performant dans Linq to Objects que la stratégie de James Curran.
StriplingWarrior

11
Dépend de la nature de la collection. Count () peut être O (N).
James Curran

3
@James: Absolument correct. S'il s'agit strictement de collections IEnumerable, il peut s'agir d'une requête en deux passes. Je serais très intéressé à voir un algorithme à 1 passage garanti. Cela pourrait être utile.
kbrimington

4
A fait quelques repères. Il s'avère que LINQ to Objects effectue certaines optimisations en fonction du type de collection que vous utilisez. En utilisant des tableaux, Lists et LinkedLists, la solution de James a tendance à être plus rapide, mais pas d'un ordre de grandeur. Si l'IEnumerable est calculé (via Enumerable.Range, par exemple), la solution de James prend plus de temps. Je ne peux penser à aucun moyen de garantir un seul passage sans savoir quelque chose sur l'implémentation ou copier des valeurs dans une structure de données différente.
StriplingWarrior

1
@RedFilter - Assez juste. Je suppose que mes habitudes de pagination se sont répandues ici. Merci pour votre œil attentif.
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();
}

MISE À JOUR: Pour résoudre le problème de clintp: a) L'utilisation de la méthode TakeLast () que j'ai définie ci-dessus résout le problème, mais si vous voulez vraiment le faire sans la méthode supplémentaire, alors il vous suffit de reconnaître que si Enumerable.Reverse () peut être utilisé comme méthode d'extension, vous n'êtes pas obligé de l'utiliser de cette façon:

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

Le problème que j'ai avec cela, c'est si je dis: List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse(); j'obtiens une erreur de compilation car .Reverse () retourne void et le compilateur choisit cette méthode à la place de Linq qui retourne un IEnumerable. Suggestions?
Clinton Pierce

1
Vous pouvez résoudre ce problème en castant explicitement mystring en IEnumerable <String>: ((IEnumerable <String>) mystring) .Reverse (). Take (2) .Reverse ()
Jan Hettich

Facile et assez simple mais nécessite d'inverser complètement la commande deux fois. Cela peut être le meilleur moyen
shashwat

Je l'aime en plus de la réponse acceptée de kbrimington. Si vous ne vous souciez pas de la commande après avoir obtenu les derniers Nenregistrements, vous pouvez ignorer le second Reverse.
ZoolWay

@shashwat Il n'inverse pas l'ordre deux fois "complètement". Le deuxième renversement ne s'applique qu'à la collecte de N éléments. De plus, selon la façon dont Reverse () est implémenté, le premier appel à celui-ci peut uniquement inverser N éléments. (L'implémentation .NET 4.0 copiera la collection dans un tableau et l'indexera en arrière)
James Curran

47

Remarque : J'ai raté le titre de votre question qui disait Utilisation de Linq , donc ma réponse n'utilise pas en fait Linq.

Si vous voulez éviter de mettre en cache une copie non paresseuse de la collection entière, vous pouvez écrire une méthode simple qui le fait en utilisant une liste liée.

La méthode suivante ajoute chaque valeur trouvée dans la collection d'origine dans une liste liée et réduit la liste liée au nombre d'éléments requis. Puisqu'il conserve la liste chaînée ajustée à ce nombre d'éléments tout au long de l'itération de la collection, il ne conserve qu'une copie d'au plus N éléments de la collection d'origine.

Il ne vous oblige pas à connaître le nombre d'articles dans la collection d'origine, ni à le parcourir plus d'une fois.

Usage:

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

Méthode d'extension:

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

Je pense toujours que vous avez une bonne réponse valide même si elle n'utilise pas techniquement Linq, donc je vous donne toujours un +1 :)
Matthew Groves

propre, net et extensible +1!
Yasser Shaikh

1
Je pense que c'est la seule solution qui ne fait pas exécuter l'énumérateur source deux fois (ou plus), et ne force pas la matérialisation de l'énumération, donc dans la plupart des applications, je dirais que ce serait beaucoup plus efficace en termes de mémoire et de vitesse.
Sprotty

30

Voici une méthode qui fonctionne sur n'importe quel énumérable mais utilise uniquement le stockage temporaire 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];
        }
    }
}

Usage:

List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();

Il fonctionne en utilisant un tampon circulaire de taille N pour stocker les éléments tels qu'ils les voient, en remplaçant les anciens éléments par de nouveaux. Lorsque la fin de l'énumérable est atteinte, le tampon en anneau contient les N derniers éléments.


2
+1: Cela devrait avoir de meilleures performances que le mien, mais vous devez vous assurer qu'il fait la bonne chose lorsque la collection contient moins d'éléments que n.
Lasse V. Karlsen

Eh bien, la plupart du temps, je suppose que les gens prendront soin lors de la copie de code de SO à des fins de production d'ajouter eux-mêmes de telles choses, cela pourrait ne pas être un problème. Si vous voulez l'ajouter, pensez à vérifier également la variable de collection pour null. Sinon, excellente solution :) J'envisageais d'utiliser un anneau-tampon moi-même, car une liste chaînée ajoutera une pression GC, mais cela fait un moment que je n'en ai pas fait et je ne voulais pas me tracasser avec le code de test pour comprendre si je l'ai bien fait. Je dois dire que je tombe amoureux de LINQPad :) linqpad.net
Lasse V. Karlsen

2
Une optimisation possible serait de vérifier si l'énumérable IList implémenté, et d'utiliser la solution triviale si c'est le cas. L'approche du stockage temporaire ne serait alors nécessaire que pour un véritable `` streaming '' IEnumerables
piers7

1
insignifiant: vos arguments à ArgumentOutOfRangeException sont dans le mauvais ordre (R # dit)
piers7

28

.NET Core 2.0+ fournit la méthode LINQ TakeLast() :

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

exemple :

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

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

J'utilise: NET Standard 2.0 et je ne l'ai pas disponible. Qu'est-ce qui ne va pas? :(
SuperJMN

@SuperJMN Bien que vous puissiez référencer des bibliothèques .net standard 2.0, il se peut que vous ne cibliez pas la bonne version de dotnet core dans votre projet. Cette méthode n'est pas disponible pour v1.x ( netcoreapp1.x) mais uniquement pour les versions 2.0 et 2.1 de dotnetcore ( netcoreapp2.x). Il est possible que vous cibliez le framework complet (par exemple net472) qui n'est également pas pris en charge. (Les bibliothèques standard .net peuvent être utilisées par n'importe lequel des éléments ci-dessus mais ne peuvent exposer que certaines API spécifiques à un framework cible. voir docs.microsoft.com/en-us/dotnet/standard/frameworks )
Ray

1
Celles-ci doivent être plus élevées maintenant. Pas besoin de réinventer la roue
James Woodley

11

Je suis surpris que personne ne l'ait mentionné, mais SkipWhile a une méthode qui utilise l'index de l'élément .

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)

Le seul avantage perceptible que cette solution présente par rapport aux autres est que vous pouvez avoir la possibilité d'ajouter un prédicat pour créer une requête LINQ plus puissante et efficace, au lieu d'avoir deux opérations distinctes qui traversent deux fois l'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));
}

9

Utilisez EnumerableEx.TakeLast dans l'assembly System.Interactive de RX. C'est une implémentation O (N) comme @ Mark's, mais elle utilise une file d'attente plutôt qu'une construction de tampon en anneau (et supprime les éléments lorsqu'elle atteint la capacité de la mémoire tampon).

(NB: il s'agit de la version IEnumerable - pas de la version IObservable, bien que l'implémentation des deux soit à peu près identique)


C'est la meilleure réponse. Ne roulez pas vous-même s'il existe une bibliothèque appropriée qui fait le travail et que l'équipe RX est de haute qualité.
bradgonesurfing

Si vous allez avec cela, installez-le à partir de Nuget - nuget.org/packages/Ix-Async
nikib3ro

Le C # n'est-il pas Queue<T>implémenté à l'aide d'un tampon circulaire ?
tigrou

@tigrou. non, ce n'est pas circulaire
citykid


6

Si vous traitez une collection avec une clé (par exemple des entrées d'une base de données), une solution rapide (c'est-à-dire plus rapide que la réponse sélectionnée) serait

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

+1 fonctionne pour moi et c'est facile à lire, j'ai un petit nombre d'objets dans ma liste
fubo

5

Si cela ne vous dérange pas de plonger dans Rx dans le cadre de la monade, vous pouvez utiliser TakeLast:

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

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

2
Vous n'avez pas besoin d'AsObservable () si vous référencez System.Interactive de RX au lieu de System.Reactive (voir ma réponse)
piers7

2

Si l'utilisation d'une bibliothèque tierce est une option, MoreLinq définit TakeLast()qui fait exactement cela.


2

J'ai essayé de combiner efficacité et simplicité et je me suis retrouvé avec ceci:

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

À propos des performances: en C #, Queue<T>est implémenté à l'aide d'un tampon circulaire de sorte qu'aucune instanciation d'objet n'est effectuée à chaque boucle (uniquement lorsque la file d'attente augmente). Je n'ai pas défini la capacité de la file d'attente (en utilisant un constructeur dédié) car quelqu'un pourrait appeler cette extension avec count = int.MaxValue. Pour des performances supplémentaires, vous pouvez vérifier si l'implémentation source IList<T>et si oui, extraire directement les dernières valeurs à l'aide des index de tableau.


1

Il est un peu inefficace de prendre le dernier N d'une collection à l'aide de LINQ car toutes les solutions ci-dessus nécessitent une itération à travers la collection. TakeLast(int n)dansSystem.Interactive également ce problème.

Si vous avez une liste, une chose plus efficace à faire est de la découper en utilisant la méthode suivante

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

avec

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

et certains cas de 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

Je sais qu'il est trop tard pour répondre à cette question. Mais si vous travaillez avec une collection de type IList <> et que vous ne vous souciez pas de l'ordre de la collection retournée, cette méthode fonctionne plus rapidement. J'ai utilisé la réponse de Mark Byers et j'ai apporté quelques modifications. Alors maintenant, la méthode TakeLast est:

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

Pour le test, j'ai utilisé la méthode Mark Byers et la réponse de kbrimington . C'est 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();

Et voici les résultats pour prendre 10 éléments:

entrez la description de l'image ici

et pour prendre 1000001 éléments, les résultats sont: entrez la description de l'image ici


1

Voici ma solution:

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

Le code est un peu gros, mais en tant que composant réutilisable, il devrait fonctionner aussi bien que possible dans la plupart des scénarios, et il gardera le code qui l'utilise agréable et concis. :-)

Mon TakeLastpour nonIList`1 est basé sur le même algorithme de tampon en anneau que celui des réponses de @Mark Byers et @MackieChan plus haut. Il est intéressant de voir à quel point ils sont similaires - j'ai écrit le mien de manière totalement indépendante. Je suppose qu'il n'y a vraiment qu'une seule façon de faire un tampon en anneau correctement. :-)

En regardant la réponse de @ kbrimington, une vérification supplémentaire pourrait être ajoutée à cela pour IQuerable<T>revenir à l'approche qui fonctionne bien avec Entity Framework - en supposant que ce que j'ai à ce stade ne fonctionne pas.


0

Ci-dessous l'exemple réel comment prendre les 3 derniers éléments d'une collection (tableau):

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

Utilisation de cette méthode pour obtenir toute la plage sans erreur

 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

Petite implémentation différente avec l'utilisation d'un tampon circulaire. Les benchmarks montrent que la méthode est environ deux fois plus rapide que celles utilisant Queue (implémentation de TakeLast dans System.Linq ), mais pas sans coût - elle a besoin d'un tampon qui croît avec le nombre d'éléments demandé, même si vous avez un petite collection, vous pouvez obtenir une énorme allocation de mémoire.

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

    }
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.