Diviser une liste en listes plus petites de taille N
J'essaie de diviser 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 bonne taille. Il devrait les diviser en listes de taille 30 mais à la place il les divise en listes de taille 114?
Comment faire en sorte que ma fonction divise une liste en X nombre 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, alors la sortie est:
Index: 4, Taille: 120
Indice: 3, Taille: 114
Index: 2, Taille: 114
Index: 1, Taille: 114
Index: 0, Taille: 114
11 réponses
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));
}
}
Je suggère d'utiliser cette méthode d'extension pour découper la liste source dans les sous-Listes par taille de morceau 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 supprimez la liste de 18 éléments par 5 éléments par bloc, elle vous donne la liste de 4 sous-listes avec les éléments suivants à l'intérieur: 5-5-5-3.
Que diriez-vous de:
while(locations.Any())
{
list.Add(locations.Take(nSize).ToList());
locations= locations.Skip(nSize).ToList();
}
La solution Serj-Tm est bien, c'est aussi la version générique comme méthode d'extension pour les listes (mettez-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;
}
Je trouve la réponse acceptée (Serj-Tm) la 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;
}
J'ai une méthode générique qui prendrait n'importe quel type include float, et elle a été testée à l'unité, j'espère que ça aide:
/// <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;
}
Ajout après très utile commentaire de mhand à la fin
Réponse originale
Bien que la plupart des solutions puissent fonctionner, je pense qu'elles ne sont pas très efficaces. Supposons que vous ne voulez que les premiers éléments des premiers morceaux. Ensuite, vous ne voudriez pas itérer sur tous les éléments (zillion) de votre séquence.
Ce qui suit sera au maximum énumérer deux fois: une fois pour la prise et une fois pour le saut. Il n'énumérera pas plus d'éléments que vous ne le ferez utilisation:
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 énumérera-t-il la séquence?
Supposons que vous divisez votre source en morceaux de chunkSize
. Vous n'énumérez que les N premiers morceaux. De chaque morceau énuméré, vous n'énumérerez que les premiers M éléments.
While(source.Any())
{
...
}
L'Any obtiendra L'énumérateur, fera 1 MoveNext () et retournera la valeur retournée après avoir éliminé l'énumérateur. Cela 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 morceau récupéré. Si vous récupérez plusieurs morceaux, mais décidez de ne pas énumérer sur le premier morceau, le foreach n'est pas exécuté, comme votre débogueur vous le montrera.
Si vous décidez de prendre les m premiers éléments du premier morceau, le rendement du rendement est exécuté exactement M fois. Cela signifie:
- récupère l'énumérateur
- appelez MoveNext () et M fois en cours.
- éliminez le énumérateur
Après que le premier morceau a été retourné, nous sautons ce premier morceau:
source = source.Skip(chunkSize);
Encore une Fois: nous allons jeter un oeil à source de référence pour trouver l' 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, le 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:
- Tout(): GetEnumerator; 1 MoveNext(); Disposer Agent Recenseur;
-
Prendre():
- rien si le contenu du morceau n'est pas énuméré.
-
Si le contenu est énuméré: GetEnumerator (), un MoveNext et un Current par élément énuméré, Dispose enumerator;
-
Skip (): pour chaque morceau énuméré (pas le contenu du morceau): 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 il y a beaucoup d'appels à MoveNext(), et seulement des appels à Current
pour les éléments TSource auxquels vous décidez réellement d'accéder.
Si vous prenez N morceaux de taille chunkSize, alors appelle MoveNext ()
- N fois pour tout ()
- pas encore de temps pour prendre, tant que vous n'énumérez pas les morceaux
- N fois chunkSize pour Skip ()
Si vous décidez d'énumérer uniquement les m premiers éléments de chaque morceau récupéré, vous devez appeler MoveNext M fois par morceau é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 soit 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 peut-être une vérification hors plage.
, Mais si votre IEnumerable est le résultat d'une requête de base de données, assurez-vous que les données sont matérialisées sur votre ordinateur, sinon les données seront récupérées à plusieurs reprises. DbContext et Dapper transféreront correctement les données au processus local avant qu'il ne soit accessible. Si vous énumérez la même séquence plusieurs fois, elle n'est pas récupérée plusieurs fois. Dapper renvoie un objet qui est une liste, DbContext se souvient que les données sont déjà récupérées.
Cela dépend de votre référentiel s'il est sage d'appeler AsEnumerable() ou ToLists () avant de commencer à diviser les éléments en Morceaux
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.
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);
}
}
Alors que beaucoup de réponses ci-dessus font le travail, elles échouent toutes horriblement sur une séquence sans fin (ou une séquence très longue). Ce qui suit est une implémentation entièrement en ligne qui garantit le meilleur temps et la complexité de la mémoire possible. Nous n'itérons la source énumérable qu'une seule fois et utilisons yield return pour une évaluation paresseuse. Le consommateur pourrait jeter la liste à chaque itération rendant l'empreinte mémoire égale à celle de la liste w / batchSize
Nombre de élément.
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 de réaliser que L'OP demande de casser un List<T>
en plus petit List<T>
, donc mes commentaires concernant les énumérables infinis ne sont pas applicables à L'OP, mais peuvent aider les autres qui finissent ici. Ces commentaires étaient en réponse à d'autres solutions publiées qui utilisent IEnumerable<T>
comme entrée à leur fonction, mais énumèrent la source énumérable plusieurs fois.
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));
}