web-dev-qa-db-fra.com

Fractionner une liste en petites listes de taille N

J'essaye de scinder une liste en une série de listes plus petites.

Mon problème: Ma fonction pour diviser des listes ne les divise pas en listes de la taille correcte. Il devrait les scinder en listes de taille 30 mais plutôt en listes de taille 114?

Comment faire en sorte que ma fonction divise une liste en un nombre X de listes de taille 30 ou moins ?

public static List<List<float[]>> splitList(List <float[]> locations, int nSize=30) 
{       
    List<List<float[]>> list = new List<List<float[]>>();

    for (int i=(int)(Math.Ceiling((decimal)(locations.Count/nSize))); i>=0; i--) {
        List <float[]> subLocat = new List <float[]>(locations); 

        if (subLocat.Count >= ((i*nSize)+nSize))
            subLocat.RemoveRange(i*nSize, nSize);
        else subLocat.RemoveRange(i*nSize, subLocat.Count-(i*nSize));

        Debug.Log ("Index: "+i.ToString()+", Size: "+subLocat.Count.ToString());
        list.Add (subLocat);
    }

    return list;
}

Si j'utilise la fonction sur une liste de taille 144, le résultat est le suivant:

Indice: 4, Taille: 120
Indice: 3, Taille: 114
Indice: 2, Taille: 114
Indice: 1, Taille: 114
Indice: 0, Taille: 114

134
Jake M
public static List<List<float[]>> splitList(List<float[]> locations, int nSize=30)  
{        
    var list = new List<List<float[]>>(); 

    for (int i=0; i < locations.Count; i+= nSize) 
    { 
        list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i))); 
    } 

    return list; 
} 

Version générique:

public static IEnumerable<List<T>> splitList<T>(List<T> locations, int nSize=30)  
{        
    for (int i=0; i < locations.Count; i+= nSize) 
    { 
        yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); 
    }  
} 
167
Serj-Tm

Je suggérerais d'utiliser cette méthode d'extension pour réduire la liste source aux sous-listes en fonction de la taille de bloc spécifiée:

/// <summary>
/// Helper methods for the lists.
/// </summary>
public static class ListExtensions
{
    public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize) 
    {
        return source
            .Select((x, i) => new { Index = i, Value = x })
            .GroupBy(x => x.Index / chunkSize)
            .Select(x => x.Select(v => v.Value).ToList())
            .ToList();
    }
}

Par exemple, si vous réduisez la liste de 18 articles par 5 articles par morceau, vous obtenez la liste des 4 sous-listes contenant les articles suivants: 5-5-5-3.

300
Dmitry Pavlov

que diriez-vous:

while(locations.Any())
{    
    list.Add(locations.Take(nSize).ToList());
    locations= locations.Skip(nSize).ToList();
}
30
Rafal

La solution Serj-Tm convient, il s'agit également de la version générique comme méthode d'extension pour les listes (placez-la dans une classe statique):

public static List<List<T>> Split<T>(this List<T> items, int sliceSize = 30)
{
    List<List<T>> list = new List<List<T>>();
    for (int i = 0; i < items.Count; i += sliceSize)
        list.Add(items.GetRange(i, Math.Min(sliceSize, items.Count - i)));
    return list;
} 
10
equintas

Je trouve réponse acceptée (Serj-Tm) plus robuste, mais je voudrais suggérer une version générique.

   public static List<List<T>> splitList<T>(List<T> locations, int nSize = 30)
    {
        var list = new List<List<T>>();

        for (int i = 0; i < locations.Count; i += nSize)
        {
            list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i)));
        }

        return list;
    }
6
Linas

J'ai une méthode générique qui prendrait n'importe quel type, y compris float, et elle a été testée à l'unité, j'espère que cela aidera:

    /// <summary>
    /// Breaks the list into groups with each group containing no more than the specified group size
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="values">The values.</param>
    /// <param name="groupSize">Size of the group.</param>
    /// <returns></returns>
    public static List<List<T>> SplitList<T>(IEnumerable<T> values, int groupSize, int? maxCount = null)
    {
        List<List<T>> result = new List<List<T>>();
        // Quick and special scenario
        if (values.Count() <= groupSize)
        {
            result.Add(values.ToList());
        }
        else
        {
            List<T> valueList = values.ToList();
            int startIndex = 0;
            int count = valueList.Count;
            int elementCount = 0;

            while (startIndex < count && (!maxCount.HasValue || (maxCount.HasValue && startIndex < maxCount)))
            {
                elementCount = (startIndex + groupSize > count) ? count - startIndex : groupSize;
                result.Add(valueList.GetRange(startIndex, elementCount));
                startIndex += elementCount;
            }
        }


        return result;
    }
5
Tianzhen Lin

Bien que beaucoup des réponses ci-dessus fassent le travail, elles échouent toutes horriblement sur une séquence sans fin (ou une très longue séquence). Ce qui suit est une implémentation entièrement en ligne qui garantit la meilleure complexité possible en temps et en mémoire. Nous n'itérons le nom de la source énumérable qu'une seule fois et utilisons le rendement du rendement pour une évaluation paresseuse. Le consommateur peut jeter la liste à chaque itération, ce qui rend l’empreinte mémoire égale à celle de la liste w/batchSize nombre d’éléments.

public static IEnumerable<List<T>> BatchBy<T>(this IEnumerable<T> enumerable, int batchSize)
{
    using (var enumerator = enumerable.GetEnumerator())
    {
        List<T> list = null;
        while (enumerator.MoveNext())
        {
            if (list == null)
            {
                list = new List<T> {enumerator.Current};
            }
            else if (list.Count < batchSize)
            {
                list.Add(enumerator.Current);
            }
            else
            {
                yield return list;
                list = new List<T> {enumerator.Current};
            }
        }

        if (list?.Count > 0)
        {
            yield return list;
        }
    }
}

EDIT: Je viens juste de comprendre que le PO demande de briser un List<T> en un List<T> plus petit. Par conséquent, mes commentaires concernant les enumerables infinis ne sont pas applicables au PO mais peuvent aider les autres. Ces commentaires faisaient suite à d'autres solutions publiées qui utilisaient IEnumerable<T> comme entrée de leur fonction, mais énuméraient la source énumérable plusieurs fois.

3
mhand

Ajout après le commentaire très utile de mhand à la fin

Réponse originale

Bien que la plupart des solutions fonctionnent, je pense qu’elles ne sont pas très efficaces. Supposons que vous souhaitiez uniquement les quelques premiers éléments des premiers morceaux. Dans ce cas, vous ne voudriez pas parcourir tous les éléments (zillions) de votre séquence.

Au maximum, les éléments suivants seront énumérés deux fois: une pour la prise et une autre pour le saut. Il n'énumérera pas plus d'éléments que vous n'utiliserez:

public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
    (this IEnumerable<TSource> source, int chunkSize)
{
    while (source.Any())                     // while there are elements left
    {   // still something to chunk:
        yield return source.Take(chunkSize); // return a chunk of chunkSize
        source = source.Skip(chunkSize);     // skip the returned chunk
    }
}

Combien de fois cela va-t-il énumérer la séquence?

Supposons que vous divisiez votre source en morceaux de chunkSize. Vous n'énumérez que les N premiers morceaux. À partir de chaque morceau énuméré, vous n'énumérerez que les premiers M éléments.

While(source.Any())
{
     ...
}

n'importe quel obtiendra l'énumérateur, faites 1 MoveNext () et retourne la valeur renvoyée après la mise au rebut de l'énumérateur. Ce sera fait N fois

yield return source.Take(chunkSize);

Selon la source de référence cela fera quelque chose comme:

public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
    return TakeIterator<TSource>(source, count);
}

static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
    foreach (TSource element in source)
    {
        yield return element;
        if (--count == 0) break;
    }
}

Cela ne fait pas beaucoup jusqu'à ce que vous commenciez à énumérer sur le Chunk récupéré. Si vous récupérez plusieurs morceaux, mais décidez de ne pas énumérer le premier morceau, foreach n'est pas exécuté, comme le débogueur vous le montrera.

Si vous décidez de prendre les M premiers éléments du premier bloc, le rendement est exécuté exactement M fois. Ça signifie:

  • obtenir l'énumérateur 
  • appelez MoveNext () et Current M fois. 
  • Éliminer l'énumérateur

Une fois que le premier morceau a été renvoyé, nous sautons ce premier morceau:

source = source.Skip(chunkSize);

Encore une fois: nous allons jeter un oeil à source de référence pour trouver le skipiterator

static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
    using (IEnumerator<TSource> e = source.GetEnumerator()) 
    {
        while (count > 0 && e.MoveNext()) count--;
        if (count <= 0) 
        {
            while (e.MoveNext()) yield return e.Current;
        }
    }
}

Comme vous le voyez, la SkipIterator appelle MoveNext() une fois pour chaque élément du bloc. Il n'appelle pas Current.

Donc, par morceau, nous voyons que ce qui suit est fait:

  • Any (): GetEnumerator; 1 MoveNext (); Dispose Enumerator;
  • Prendre(): 

    • rien si le contenu du bloc n'est pas énuméré. 
    • Si le contenu est énuméré: GetEnumerator (), un MoveNext et un courant par élément énuméré, Dispose enumerator;

    • Skip (): pour chaque bloc énuméré (PAS le contenu du bloc): GetEnumerator (), MoveNext () chunkSize fois, pas de courant! Éliminer l'énumérateur

Si vous regardez ce qui se passe avec l'énumérateur, vous verrez qu'il y a beaucoup d'appels à MoveNext () et uniquement à Current pour les éléments TSource auxquels vous décidez réellement d'accéder.

Si vous prenez N morceaux de taille chunkSize, alors appelez MoveNext ()

  • N fois pour Any ()
  • pas encore de temps pour Take, tant que vous n'énumérez pas les morceaux
  • N fois chunkSize for Skip ()

Si vous décidez de n'énumérer que les M premiers éléments de chaque bloc extrait, vous devez appeler MoveNext M fois par bloc énuméré.

Le total

MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)

Donc, si vous décidez d’énumérer tous les éléments de tous les morceaux:

MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once

Que MoveNext nécessite beaucoup de travail ou non dépend du type de séquence source. Pour les listes et les tableaux, il s’agit d’un simple incrément d’index, avec éventuellement une vérification hors limites.

Mais si votre IEnumerable est le résultat d’une interrogation de base de données, assurez-vous que les données sont bien matérialisées sur votre ordinateur, sinon les données seront extraites plusieurs fois. DbContext et Dapper transféreront correctement les données au processus local avant de pouvoir y accéder. Si vous énumérez plusieurs fois la même séquence, elle ne sera pas extraite plusieurs fois. Dapper retourne un objet qui est une liste, DbContext se souvient que les données sont déjà extraites.

Cela dépend de votre référentiel s'il est sage d'appeler AsEnumerable () ou ToLists () avant de commencer à diviser les éléments de Chunks.

2
Harald Coppoolse
public static IEnumerable<IEnumerable<T>> SplitIntoSets<T>
    (this IEnumerable<T> source, int itemsPerSet) 
{
    var sourceList = source as List<T> ?? source.ToList();
    for (var index = 0; index < sourceList.Count; index += itemsPerSet)
    {
        yield return sourceList.Skip(index).Take(itemsPerSet);
    }
}
2
Scott Hannen

La bibliothèque MoreLinq a une méthode appelée Batch

List<int> ids = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; // 10 elements
int counter = 1;
foreach(var batch in ids.Batch(2))
{
    foreach(var eachId in batch)
    {
        Console.WriteLine("Batch: {0}, Id: {1}", counter, eachId);
    }
    counter++;
}

Le résultat est

Batch: 1, Id: 1
Batch: 1, Id: 2
Batch: 2, Id: 3
Batch: 2, Id: 4
Batch: 3, Id: 5
Batch: 3, Id: 6
Batch: 4, Id: 7
Batch: 4, Id: 8
Batch: 5, Id: 9
Batch: 5, Id: 0

ids sont divisés en 5 morceaux avec 2 éléments.

2
Sidron

Celui-ci, ça va? L'idée était d'utiliser une seule boucle. Et, qui sait, vous utilisez peut-être uniquement des implémentations IList dans votre code et vous ne souhaitez pas utiliser la conversion vers List.

private IEnumerable<IList<T>> SplitList<T>(IList<T> list, int totalChunks)
{
    IList<T> auxList = new List<T>();
    int totalItems = list.Count();

    if (totalChunks <= 0)
    {
        yield return auxList;
    }
    else 
    {
        for (int i = 0; i < totalItems; i++)
        {               
            auxList.Add(list[i]);           

            if ((i + 1) % totalChunks == 0)
            {
                yield return auxList;
                auxList = new List<T>();                
            }

            else if (i == totalItems - 1)
            {
                yield return auxList;
            }
        }
    }   
}
1
Diego Romar
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
    return items.Select((item, index) => new { item, index })
                .GroupBy(x => x.index / maxItems)
                .Select(g => g.Select(x => x.item));
}
1
Codester
public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize)
    {           
        var result = new List<List<T>>();
        for (int i = 0; i < source.Count; i += chunkSize)
        {
            var rows = new List<T>();
            for (int j = i; j < i + chunkSize; j++)
            {
                if (j >= source.Count) break;
                rows.Add(source[j]);
            }
            result.Add(rows);
        }
        return result;
    }
0
Baskovli3

Un de plus

public static IList<IList<T>> SplitList<T>(this IList<T> list, int chunkSize)
{
    var chunks = new List<IList<T>>();
    List<T> chunk = null;
    for (var i = 0; i < list.Count; i++)
    {
        if (i % chunkSize == 0)
        {
            chunk = new List<T>(chunkSize);
            chunks.Add(chunk);
        }
        chunk.Add(list[i]);
    }
    return chunks;
}
0
Gabriel Medeiros