Dividir una lista en listas más pequeñas de tamaño N


210

Estoy intentando dividir una lista en una serie de listas más pequeñas.

Mi problema: mi función para dividir listas no las divide en listas del tamaño correcto. ¿Debería dividirlos en listas de tamaño 30 pero en su lugar los divide en listas de tamaño 114?

¿Cómo puedo hacer que mi función divida una lista en X número de listas de tamaño 30 o menos ?

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 uso la función en una lista de tamaño 144, la salida es:

Índice: 4, Tamaño: 120
Índice: 3, Tamaño: 114
Índice: 2, Tamaño: 114
Índice: 1, Tamaño: 114
Índice: 0, Tamaño: 114


1
Si una solución LINQ es aceptable, esta pregunta puede ser de alguna ayuda .

Específicamente la respuesta de Sam Saffron a esa pregunta anterior. Y a menos que sea para una tarea escolar, solo usaría su código y me detendría.
jcolebrand

Respuestas:


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

Versión genérica:

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

Entonces, si tengo un billón de longitud de Lista, y quiero dividirlo en listas más pequeñas, Longitud 30, y de cada lista más pequeña que solo quiero Tomar (1), entonces sigo creando listas de 30 elementos, de los cuales descarto 29 elementos. ¡Esto se puede hacer de manera más inteligente!
Harald Coppoolse

¿Esto realmente funciona? ¿No fallaría en la primera división porque está obteniendo el rango nSize a nSize? Por ejemplo, si nSize es 3 y mi matriz es de tamaño 5, entonces el primer rango de índice devuelto esGetRange(3, 3)
Matthew Pigram

2
@MatthewPigram probado y está funcionando. Math.Min toma el valor mínimo, por lo que si el último fragmento es menor que nSize (2 <3), crea una lista con los elementos restantes.
Phate01

1
@HaraldCoppoolse, el OP no solicitó selección, solo para dividir listas
Phate01

@MatthewPigram Primera iteración - GetRange (0,3), segunda iteración - GetRange (3,2)
Serj-Tm

381

Sugeriría usar este método de extensión para fragmentar la lista de origen en las sublistas por tamaño de fragmento especificado:

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

Por ejemplo, si divide la lista de 18 elementos por 5 elementos por fragmento, le da la lista de 4 sublistas con los siguientes elementos dentro: 5-5-5-3.


25
Antes de usar esto en producción, asegúrese de comprender cuáles son las implicaciones del tiempo de ejecución para la memoria y el rendimiento. El hecho de que LINQ pueda ser breve, no significa que sea una buena idea.
Nick

44
Definitivamente, @Nick sugeriría en general pensar antes de hacer nada. La fragmentación con LINQ no debería ser una operación que se repite miles de veces. Por lo general, debe fragmentar las listas para procesar elementos lote por lote y / o en paralelo.
Dmitry Pavlov

66
No creo que la memoria y el rendimiento sean un gran problema aquí. Tuve el requisito de dividir una lista con más de 200,000 registros en listas más pequeñas con aproximadamente 3000 cada uno, lo que me llevó a este hilo, y probé ambos métodos y descubrí que el tiempo de ejecución es casi el mismo. Después de eso probé dividir esa lista en listas con 3 registros cada una y aún así el rendimiento está bien. Creo que la solución de Serj-Tm es más sencilla y tiene una mejor capacidad de mantenimiento.
Sojourner silencioso

2
Tenga en cuenta que podría ser mejor dejar de lado el ToList()s, y dejar que la evaluación perezosa haga su magia.
Yair Halberstadt

3
@DmitryPavlov Durante todo este tiempo, ¡nunca supe sobre poder proyectar el índice de esa manera en una declaración select! Pensé que era una función nueva hasta que noté que publicaste esto en 2014, ¡eso realmente me sorprendió! Gracias por compartir esto. Además, ¿no sería mejor tener este método de extensión disponible para un IEnumerable y también devolver un IEnumerable?
Aydin

37

qué tal si:

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

¿Consumirá esto mucha memoria? Cada vez que localizaciones.Skip.ToList sucede Me pregunto si se asigna más memoria y una nueva lista hace referencia a elementos no compilados.
Zasz

2
Sí, se crea una nueva lista en cada bucle. Sí, consume memoria. Pero si tiene problemas de memoria, este no es el lugar para optimizar, ya que las instancias de esas listas están listas para ser recopiladas en el próximo ciclo. Puede cambiar el rendimiento por memoria omitiendo el, ToListpero no me molestaría en tratar de optimizarlo: es tan trivial y poco probable que sea un cuello de botella. La principal ganancia de esta implementación es su trivialidad, es fácil de entender. Si lo desea, puede usar la respuesta aceptada, no crea esas listas, pero es un poco más compleja.
Rafal

2
.Skip(n)itera sobre los nelementos cada vez que se llama, aunque esto puede estar bien, es importante tener en cuenta el código crítico para el rendimiento. stackoverflow.com/questions/20002975/…
Chakrava

@Chakrava seguro, mi solución no se debe utilizar en el código crítico de rendimiento, sin embargo, en mi experiencia, primero se escribe el código de trabajo y luego se determina qué es el rendimiento crítico y rara vez se realizan las operaciones de linq to object en, digamos, 50 objetos. Esto debe evaluarse caso por caso.
Rafal

@Rafal Estoy de acuerdo, he encontrado numerosos correos electrónicos .Skip()en la base de código de mi empresa y, aunque pueden no ser "óptimos", funcionan bien. Cosas como las operaciones de DB tardan mucho más de todos modos. Pero creo que es importante tener en cuenta que .Skip()"toca" cada elemento <n en su camino en lugar de saltar directamente al enésimo elemento (como es de esperar). Si su iterador tiene efectos secundarios al tocar un elemento, .Skip()puede ser la causa de errores difíciles de encontrar.
Chakrava

11

La solución Serj-Tm está bien, también esta es la versión genérica como método de extensión para listas (póngala en una clase estática):

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

Encuentro la respuesta aceptada (Serj-Tm) más robusta, pero me gustaría sugerir una versión genérica.

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

8

La biblioteca MoreLinq tiene un método llamado 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++;
}

El resultado es

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 se dividen en 5 trozos con 2 elementos.


Esta debe ser la respuesta aceptada. O al menos mucho más arriba en esta página.
Zar Shardan

7

Tengo un método genérico que tomaría cualquier tipo, incluido flotante, y ha sido probado por unidad, espero que ayude:

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

Gracias. ¿Se pregunta si podría actualizar los comentarios con la definición del parámetro maxCount? ¿Una red de seguridad?
Andrew Jens

2
tenga cuidado con las enumeraciones múltiples de los enumerables. values.Count()causará una enumeración completa y luego values.ToList()otra. Es más seguro hacerlo values = values.ToList(), ya está materializado.
Mhand

7

Si bien muchas de las respuestas anteriores hacen el trabajo, todas fallan horriblemente en una secuencia interminable (o una secuencia realmente larga). La siguiente es una implementación completamente en línea que garantiza la mejor complejidad de tiempo y memoria posible. Solo iteramos la fuente enumerable exactamente una vez y utilizamos el rendimiento para la evaluación diferida. El consumidor podría tirar la lista en cada iteración haciendo que la huella de la memoria sea igual a la de la lista con el batchSizenúmero de elementos.

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

EDITAR: justo ahora, al darme cuenta de que el OP pregunta por dividir a un List<T>más pequeño List<T>, mis comentarios sobre infinitos enumerables no son aplicables al OP, pero pueden ayudar a otros que terminan aquí. Estos comentarios fueron en respuesta a otras soluciones publicadas que se utilizan IEnumerable<T>como entrada para su función, pero enumeran la fuente enumerable varias veces.


Creo que la IEnumerable<IEnumerable<T>>versión es mejor ya que no implica tanta Listconstrucción.
NetMage

@NetMage: uno de los problemas IEnumerable<IEnumerable<T>>es que es probable que la implementación dependa de que el consumidor enumere por completo cada enumerable interno producido. Estoy seguro de que se podría formular una solución para evitar ese problema, pero creo que el código resultante podría complicarse bastante rápido. Además, dado que es vago, solo estamos generando una sola lista a la vez y la asignación de memoria ocurre exactamente una vez por lista, ya que conocemos el tamaño por adelantado.
Mhand

Tiene razón: mi implementación utiliza un nuevo tipo de enumerador (un enumerador de posición) que rastrea su posición actual envolviendo un enumerador estándar y le permite pasar a una nueva posición.
NetMage

6

Adición después del comentario muy útil de mhand al final

Respuesta original

Aunque la mayoría de las soluciones podrían funcionar, creo que no son muy eficientes. Supongamos que solo desea los primeros elementos de los primeros fragmentos. Entonces no querrás iterar sobre todos los elementos (miles de millones) en tu secuencia.

A continuación, se enumerarán dos veces como máximo: una para Take y otra para Skip. No enumerará más elementos de los que usará:

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

¿Cuántas veces enumerará esto la secuencia?

Supongamos que divide su fuente en trozos de chunkSize. Enumera solo los primeros N fragmentos. De cada fragmento enumerado solo enumerará los primeros elementos M.

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

Any obtendrá el enumerador, realiza 1 MoveNext () y devuelve el valor devuelto después de desechar el enumerador. Esto se hará N veces

yield return source.Take(chunkSize);

Según la fuente de referencia, esto hará algo como:

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

Esto no hace mucho hasta que comience a enumerar sobre el fragmento recuperado. Si obtiene varios Chunks, pero decide no enumerar el primer Chunk, el foreach no se ejecutará, como le mostrará su depurador.

Si decide tomar los primeros M elementos del primer fragmento, el rendimiento se ejecutará exactamente M veces. Esto significa:

  • obtener el enumerador
  • llame a MoveNext () y Current M veces.
  • Deshazte del enumerador

Después de que se haya devuelto el primer fragmento, omitimos este primer fragmento:

source = source.Skip(chunkSize);

Una vez más: echaremos un vistazo a la fuente de referencia para encontrar elskipiterator

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

Como puede ver, las SkipIteratorllamadas MoveNext()una vez para cada elemento en el fragmento. No llamaCurrent .

Entonces, por fragmento, vemos que se hace lo siguiente:

  • Cualquiera (): GetEnumerator; 1 MoveNext (); Desechar enumerador;
  • Tomar():

    • nada si el contenido del fragmento no se enumera.
    • Si se enumera el contenido: GetEnumerator (), un MoveNext y un Current por elemento enumerado, Dispose enumerator;

    • Saltar (): para cada fragmento que se enumera (NO el contenido del fragmento): GetEnumerator (), MoveNext () chunkSize veces, ¡no actual! Eliminar enumerador

Si observa lo que sucede con el enumerador, verá que hay muchas llamadas a MoveNext (), y solo llamadas a Current los elementos de TSource a los que realmente decide acceder.

Si toma N trozos de tamaño chunkSize, llama a MoveNext ()

  • N veces para Any ()
  • todavía no hay tiempo para Take, siempre y cuando no enumeres los Chunks
  • N veces fragmento Tamaño para Saltar ()

Si decide enumerar solo los primeros elementos M de cada fragmento recuperado, debe llamar a MoveNext M veces por fragmento enumerado.

El total

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

Entonces, si decides enumerar todos los elementos de todos los fragmentos:

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

Si MoveNext es mucho trabajo o no, depende del tipo de secuencia de origen. Para listas y matrices es un simple incremento de índice, con quizás una verificación fuera de rango.

Pero si su IEnumerable es el resultado de una consulta a la base de datos, asegúrese de que los datos realmente se materialicen en su computadora, de lo contrario, los datos se recuperarán varias veces. DbContext y Dapper transferirán correctamente los datos al proceso local antes de poder acceder a ellos. Si enumera la misma secuencia varias veces, no se obtiene varias veces. Dapper devuelve un objeto que es una Lista, DbContext recuerda que los datos ya están recuperados.

Depende de su repositorio si es aconsejable llamar a AsEnumerable () o ToLists () antes de comenzar a dividir los elementos en fragmentos


¿No enumerará esto dos veces por lote? Entonces, ¿estamos realmente enumerando los 2*chunkSizetiempos fuente ? Esto es mortal dependiendo de la fuente del enumerable (tal vez respaldado por DB u otra fuente no memorizada). Imagínese esto enumerable como entrada Enumerable.Range(0, 10000).Select(i => DateTime.UtcNow): obtendrá diferentes tiempos cada vez que enumere el enumerable, ya que no está memorizado
mano del

Considere lo siguiente: Enumerable.Range(0, 10).Select(i => DateTime.UtcNow). Al invocar Any, volverá a calcular la hora actual cada vez. No es tan malo para DateTime.UtcNow, pero considere un enumerable respaldado por una conexión de base de datos / cursor sql o similar. He visto casos en los que se emitieron miles de llamadas de base de datos debido a que el desarrollador no comprendía las repercusiones potenciales de 'múltiples enumeraciones de un enumerable' - ReSharper proporciona una pista para esto también
Mhand

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

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

2

¿Que tal este? La idea era usar solo un bucle. Y, quién sabe, tal vez solo esté usando implementaciones de IList a través de su código y no quiera enviarlo a la Lista.

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

Uno mas

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

1
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
List<int> list =new List<int>(){1,2,3,4,5,6,7,8,9,10,12};
Dictionary<int,List<int>> dic = new Dictionary <int,List<int>> ();
int batchcount = list.Count/2; //To List into two 2 parts if you want three give three
List<int> lst = new List<int>();
for (int i=0;i<list.Count; i++)
{
lstdocs.Add(list[i]);
if (i % batchCount == 0 && i!=0)
{
Dic.Add(threadId, lstdocs);
lst = new List<int>();**strong text**
threadId++;
}
}
Dic.Add(threadId, lstdocs);

2
es preferible explicar su respuesta en lugar de solo proporcionar un fragmento de código
Kevin

0

Había encontrado esta misma necesidad, y usé una combinación de los métodos Skip () y Take () de Linq . Multiplico el número que tomo por el número de iteraciones hasta el momento, y eso me da el número de elementos para saltar, luego tomo el siguiente grupo.

        var categories = Properties.Settings.Default.MovementStatsCategories;
        var items = summariesWithinYear
            .Select(s =>  s.sku).Distinct().ToList();

        //need to run by chunks of 10,000
        var count = items.Count;
        var counter = 0;
        var numToTake = 10000;

        while (count > 0)
        {
            var itemsChunk = items.Skip(numToTake * counter).Take(numToTake).ToList();
            counter += 1;

            MovementHistoryUtilities.RecordMovementHistoryStatsBulk(itemsChunk, categories, nLogger);

            count -= numToTake;
        }

0

Basado en Dimitry Pavlov, respondí que lo eliminaría .ToList(). Y también evita la clase anónima. En cambio, me gusta usar una estructura que no requiere una asignación de memoria de montón. (A ValueTupletambién haría trabajo).

public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    if (source is null)
    {
        throw new ArgumentNullException(nameof(source));
    }
    if (chunkSize <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(chunkSize), chunkSize, "The argument must be greater than zero.");
    }

    return source
        .Select((x, i) => new ChunkedValue<TSource>(x, i / chunkSize))
        .GroupBy(cv => cv.ChunkIndex)
        .Select(g => g.Select(cv => cv.Value));
} 

[StructLayout(LayoutKind.Auto)]
[DebuggerDisplay("{" + nameof(ChunkedValue<T>.ChunkIndex) + "}: {" + nameof(ChunkedValue<T>.Value) + "}")]
private struct ChunkedValue<T>
{
    public ChunkedValue(T value, int chunkIndex)
    {
        this.ChunkIndex = chunkIndex;
        this.Value = value;
    }

    public int ChunkIndex { get; }

    public T Value { get; }
}

Esto se puede usar como el siguiente, que solo itera sobre la colección una vez y tampoco asigna ninguna memoria significativa.

int chunkSize = 30;
foreach (var chunk in collection.ChunkBy(chunkSize))
{
    foreach (var item in chunk)
    {
        // your code for item here.
    }
}

Si realmente se necesita una lista concreta, lo haría así:

int chunkSize = 30;
var chunkList = new List<List<T>>();
foreach (var chunk in collection.ChunkBy(chunkSize))
{
    // create a list with the correct capacity to be able to contain one chunk
    // to avoid the resizing (additional memory allocation and memory copy) within the List<T>.
    var list = new List<T>(chunkSize);
    list.AddRange(chunk);
    chunkList.Add(list);
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.