web-dev-qa-db-fra.com

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

Avec 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, quel serait le meilleur moyen d'écrire une méthode d'extension pour le faire?

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

Cette approche préserve l'ordre des articles sans dépendre d'aucun tri et offre une compatibilité étendue entre plusieurs fournisseurs LINQ.

Il est important de veiller à ne pas appeler Skip avec un nombre négatif. Certains fournisseurs, tels qu'Entity Framework, généreront une exception ArgumentException lorsqu'ils seront présentés avec un argument négatif. L'appel à Math.Max évite nettement cette situation.

La classe ci-dessous contient tous les éléments essentiels pour les méthodes d'extension, à savoir: une classe statique, une méthode statique et l'utilisation du mot clé this.

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 la performance:

Comme l'appel à Count() peut provoquer l'énumération de certaines structures de données, cette approche 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 listes, les tableaux et même les requêtes EF afin d'évaluer l'opération Count() dans O(1) temps.

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

382
kbrimington
coll.Reverse().Take(N).Reverse().ToList();


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

UPDATE: 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, vous devez simplement reconnaître que, même 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();
56
James Curran

Note: J'ai raté le titre de votre question qui disait tilisation de Linq, ma réponse n'utilise donc pas Linq.

Si vous souhaitez éviter de mettre en cache une copie non paresseuse de la collection entière, vous pouvez écrire une méthode simple qui le fait à l'aide d'une liste liée.

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

Cela ne vous oblige pas à connaître le nombre d'éléments de la collection d'origine, ni à les 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("collection");
        if (n < 0)
            throw new ArgumentOutOfRangeException("n", "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;
    }
}
43

Voici une méthode qui fonctionne sur tous les types énumérables 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();

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

28
Mark Byers

.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
16
Ray Vega

Je suis surpris que personne ne l’ait mentionné, mais SkipWhile a une méthode qui tilise 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 solutions est que vous pouvez avoir la possibilité d’ajouter un prédicat pour créer une requête LINQ plus puissante et plus efficace, au lieu d’avoir deux opérations distinctes qui traversent deux fois 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));
}
11
Nick Babcock

Utilisez EnumerableEx.TakeLast dans l’assemblage 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 ring-buffer (et supprime les éléments quand elle atteint la capacité de la mémoire tampon).

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

8
piers7

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);
5
dav_i

Si cela ne vous dérange pas de puiser dans Rx en tant que membre de la monade, vous pouvez utiliser TakeLast:

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

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

J'ai essayé de combiner efficacité et simplicité pour aboutir à 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'il n'y a pas d'instanciation d'objet effectuée à chaque boucle (uniquement lorsque la file d'attente est en train de grandir). Je n'ai pas défini la capacité de la file d'attente (à l'aide d'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 la source implémente IList<T> et si oui, extrayez directement les dernières valeurs à l'aide d'index de tableau.

2
tigrou

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

2
s.m.

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é Mark Byers répond et 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 etswer . 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:

enter image description here

et pour prendre 1000001 éléments, les résultats sont: enter image description here

1
Sasha

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 volumineux, mais en tant que composant réutilisable, il devrait fonctionner aussi bien qu'il le peut dans la plupart des scénarios, et il gardera le code qui l'utilise bien et concis. :-)

Mon TakeLast pour non -IList`1 est basé sur le même algorithme de mémoire tampon en anneau que dans les réponses de @Mark Byers et @MackieChan plus haut. C'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’ya qu’une façon de créer correctement un tampon circulaire. :-)

En regardant la réponse de @ kbrimington, un contrôle supplémentaire pourrait être ajouté à ceci pour que IQuerable<T> puisse revenir à l'approche qui fonctionne bien avec Entity Framework - en supposant que ce que j'ai à ce stade ne fonctionne pas.

1
Jonathan Gilbert

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 dans la collection. TakeLast(int n) dans System.Interactive a également ce problème.

Si vous avez une liste, il est plus efficace 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 quelques 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
bradgonesurfing

Peu d'implémentation différente avec l'utilisation du tampon circulaire. Les tests montrent que la méthode est environ deux fois plus rapide que celle utilisant File d'attente (implémentation de TakeLast dans System.Linq), mais pas sans coût - il faut un tampon qui augmente avec le nombre d'éléments requis, même si vous avez une 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];

    }
}
0
Romasz

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
Aleksey Timkov

Utilisation de cette méthode pour obtenir toutes les plages 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