Lista dividida en sublistas con LINQ

377

¿Hay alguna manera de separar un List<SomeObject>en varias listas separadas SomeObject, usando el índice del elemento como delimitador de cada división?

Déjame ejemplificar:

Tengo un List<SomeObject>y necesito un List<List<SomeObject>>o List<SomeObject>[], de modo que cada una de estas listas resultantes contendrá un grupo de 3 elementos de la lista original (secuencialmente).

p.ej.:

  • Lista original: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Listas resultantes: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

También necesitaría que el tamaño de las listas resultantes sea un parámetro de esta función.

Felipe Lima
fuente

Respuestas:

378

Prueba el siguiente código.

public static IList<IList<T>> Split<T>(IList<T> source)
{
    return  source
        .Select((x, i) => new { Index = i, Value = x })
        .GroupBy(x => x.Index / 3)
        .Select(x => x.Select(v => v.Value).ToList())
        .ToList();
}

La idea es agrupar primero los elementos por índices. Dividir por tres tiene el efecto de agruparlos en grupos de 3. Luego, convertir cada grupo en una lista y el IEnumerablede Listen un Listde Lists

JaredPar
fuente
21
GroupBy hace una ordenación implícita. Eso puede matar el rendimiento. Lo que necesitamos es algún tipo de inverso de SelectMany.
yfeldblum
55
@ Justicia, GroupBy podría implementarse mediante hashing. ¿Cómo sabes que la implementación de GroupBy "puede matar el rendimiento"?
Amy B
55
GroupBy no devuelve nada hasta que se enumeran todos los elementos. Por eso es lento. Las listas que OP quiere son contiguas, por lo que un método mejor podría generar la primera sublista [a,g,e]antes de enumerar más de la lista original.
Coronel Panic
99
Tome el ejemplo extremo de un IEnumerable infinito. GroupBy(x=>f(x)).First()nunca cederá un grupo. OP preguntó sobre listas, pero si escribimos para trabajar con IEnumerable, haciendo solo una iteración, cosechamos la ventaja de rendimiento.
Coronel Panic
8
Sin embargo, @Nick Order no se conserva a tu manera. Todavía es bueno saberlo, pero los estaría agrupando en (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Si el orden no importa, entonces está bien, pero en este caso parece que importa.
Reafexus
325

Esta pregunta es un poco vieja, pero acabo de escribirla y creo que es un poco más elegante que las otras soluciones propuestas:

/// <summary>
/// Break a list of items into chunks of a specific size
/// </summary>
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}
CaseyB
fuente
14
Amo esta solución. Recomiendo agregar esta comprobación de cordura para evitar un bucle infinito: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
mroach
10
Me gusta esto, pero no es súper eficiente
Sam Saffron
51
Me gusta este pero la eficiencia del tiempo es O(n²). Puede recorrer la lista y obtener un O(n)tiempo.
FELIZ
8
@hIpPy, ¿cómo es n ^ 2? Me parece lineal
Vivek Maharajh
13
@vivekmaharajh sourcese reemplaza por un envuelto IEnumerablecada vez. Entonces, tomar elementos de sourceatraviesa capas de Skips
Lasse Espeholt
99

En general, el enfoque sugerido por CaseyB funciona bien, de hecho, si lo está pasando List<T>es difícil criticarlo, tal vez lo cambiaría a:

public static IEnumerable<IEnumerable<T>> ChunkTrivialBetter<T>(this IEnumerable<T> source, int chunksize)
{
   var pos = 0; 
   while (source.Skip(pos).Any())
   {
      yield return source.Skip(pos).Take(chunksize);
      pos += chunksize;
   }
}

Lo que evitará cadenas de llamadas masivas. No obstante, este enfoque tiene un defecto general. Se materializan dos enumeraciones por fragmento, para resaltar el problema intente ejecutar:

foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First())
{
   Console.WriteLine(item);
}
// wait forever 

Para superar esto, podemos probar Cameron's enfoque , que pasa la prueba anterior con gran éxito, ya que solo recorre la enumeración una vez.

El problema es que tiene un defecto diferente, materializa cada elemento en cada fragmento, el problema con ese enfoque es que te queda poca memoria.

Para ilustrar eso intente ejecutar:

foreach (var item in Enumerable.Range(1, int.MaxValue)
               .Select(x => x + new string('x', 100000))
               .Clump(10000).Skip(100).First())
{
   Console.Write('.');
}
// OutOfMemoryException

Finalmente, cualquier implementación debería ser capaz de manejar la iteración de fragmentos fuera de orden, por ejemplo:

Enumerable.Range(1,3).Chunk(2).Reverse().ToArray()
// should return [3],[1,2]

Muchas soluciones altamente óptimas como mi primera revisión de esta respuesta fallaron allí. El mismo problema se puede ver en la respuesta optimizada de casperOne .

Para abordar todos estos problemas, puede utilizar lo siguiente:

namespace ChunkedEnumerator
{
    public static class Extensions 
    {
        class ChunkedEnumerable<T> : IEnumerable<T>
        {
            class ChildEnumerator : IEnumerator<T>
            {
                ChunkedEnumerable<T> parent;
                int position;
                bool done = false;
                T current;


                public ChildEnumerator(ChunkedEnumerable<T> parent)
                {
                    this.parent = parent;
                    position = -1;
                    parent.wrapper.AddRef();
                }

                public T Current
                {
                    get
                    {
                        if (position == -1 || done)
                        {
                            throw new InvalidOperationException();
                        }
                        return current;

                    }
                }

                public void Dispose()
                {
                    if (!done)
                    {
                        done = true;
                        parent.wrapper.RemoveRef();
                    }
                }

                object System.Collections.IEnumerator.Current
                {
                    get { return Current; }
                }

                public bool MoveNext()
                {
                    position++;

                    if (position + 1 > parent.chunkSize)
                    {
                        done = true;
                    }

                    if (!done)
                    {
                        done = !parent.wrapper.Get(position + parent.start, out current);
                    }

                    return !done;

                }

                public void Reset()
                {
                    // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx
                    throw new NotSupportedException();
                }
            }

            EnumeratorWrapper<T> wrapper;
            int chunkSize;
            int start;

            public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start)
            {
                this.wrapper = wrapper;
                this.chunkSize = chunkSize;
                this.start = start;
            }

            public IEnumerator<T> GetEnumerator()
            {
                return new ChildEnumerator(this);
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

        }

        class EnumeratorWrapper<T>
        {
            public EnumeratorWrapper (IEnumerable<T> source)
            {
                SourceEumerable = source;
            }
            IEnumerable<T> SourceEumerable {get; set;}

            Enumeration currentEnumeration;

            class Enumeration
            {
                public IEnumerator<T> Source { get; set; }
                public int Position { get; set; }
                public bool AtEnd { get; set; }
            }

            public bool Get(int pos, out T item) 
            {

                if (currentEnumeration != null && currentEnumeration.Position > pos)
                {
                    currentEnumeration.Source.Dispose();
                    currentEnumeration = null;
                }

                if (currentEnumeration == null)
                {
                    currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false };
                }

                item = default(T);
                if (currentEnumeration.AtEnd)
                {
                    return false;
                }

                while(currentEnumeration.Position < pos) 
                {
                    currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext();
                    currentEnumeration.Position++;

                    if (currentEnumeration.AtEnd) 
                    {
                        return false;
                    }

                }

                item = currentEnumeration.Source.Current;

                return true;
            }

            int refs = 0;

            // needed for dispose semantics 
            public void AddRef()
            {
                refs++;
            }

            public void RemoveRef()
            {
                refs--;
                if (refs == 0 && currentEnumeration != null)
                {
                    var copy = currentEnumeration;
                    currentEnumeration = null;
                    copy.Source.Dispose();
                }
            }
        }

        public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
        {
            if (chunksize < 1) throw new InvalidOperationException();

            var wrapper =  new EnumeratorWrapper<T>(source);

            int currentPos = 0;
            T ignore;
            try
            {
                wrapper.AddRef();
                while (wrapper.Get(currentPos, out ignore))
                {
                    yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos);
                    currentPos += chunksize;
                }
            }
            finally
            {
                wrapper.RemoveRef();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int i = 10;
            foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3))
            {
                foreach (var n in group)
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
                if (i-- == 0) break;
            }


            var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray();

            foreach (var idx in new [] {3,2,1})
            {
                Console.Write("idx " + idx + " ");
                foreach (var n in stuffs[idx])
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
            }

            /*

10000001 10000002 10000003
10000004 10000005 10000006
10000007 10000008 10000009
10000010 10000011 10000012
10000013 10000014 10000015
10000016 10000017 10000018
10000019 10000020 10000021
10000022 10000023 10000024
10000025 10000026 10000027
10000028 10000029 10000030
10000031 10000032 10000033
idx 3 7 8
idx 2 5 6
idx 1 3 4
             */

            Console.ReadKey();


        }

    }
}

También hay una ronda de optimizaciones que podría introducir para la iteración de fragmentos fuera de orden, que está fuera de alcance aquí.

¿En cuanto a qué método debes elegir? Depende totalmente del problema que intente resolver. Si no le preocupa la primera falla, la respuesta simple es increíblemente atractiva.

Tenga en cuenta que, como con la mayoría de los métodos, esto no es seguro para subprocesos múltiples, las cosas pueden volverse extrañas si desea que sea seguro para subprocesos que necesitaría modificar EnumeratorWrapper.

Sam Azafrán
fuente
¿Sería el error Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () está equivocado, o Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () lanzando una excepción?
Cameron MacFarland
@SamSaffron He actualizado mi respuesta y simplificado enormemente el código para lo que siento es el caso de uso prominente (y reconozco las advertencias).
casperOne
¿Qué pasa con trocear IQueryable <>? Supongo que un enfoque Take / Skip sería óptimo si queremos delegar un máximo de las operaciones al proveedor
Guillaume86,
@ Guillaume86 Estoy de acuerdo, si tienes un IList o IQueryable puedes tomar todo tipo de atajos que lo harán mucho más rápido (Linq hace esto internamente para todo tipo de otros métodos)
Sam Saffron
1
Esta es, con mucho, la mejor respuesta para la eficiencia. Tengo un problema al usar SqlBulkCopy con un IEnumerable que ejecuta procesos adicionales en cada columna, por lo que debe ejecutarse de manera eficiente con solo una pasada. Esto me permitirá dividir el IEnumerable en trozos de tamaño manejable. (Para aquellos que se preguntan, habilité el modo de transmisión de SqlBulkCopy, que parece estar roto).
Brain2000
64

Usted podría utilizar una serie de consultas que utilizan TakeySkip , pero eso sería añadir demasiadas iteraciones en la lista original, creo.

Más bien, creo que debería crear un iterador propio, así:

public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
  IEnumerable<T> enumerable, int groupSize)
{
   // The list to return.
   List<T> list = new List<T>(groupSize);

   // Cycle through all of the items.
   foreach (T item in enumerable)
   {
     // Add the item.
     list.Add(item);

     // If the list has the number of elements, return that.
     if (list.Count == groupSize)
     {
       // Return the list.
       yield return list;

       // Set the list to a new list.
       list = new List<T>(groupSize);
     }
   }

   // Return the remainder if there is any,
   if (list.Count != 0)
   {
     // Return the list.
     yield return list;
   }
}

Luego puede llamar a esto y está habilitado para LINQ para que pueda realizar otras operaciones en las secuencias resultantes.


A la luz de la respuesta de Sam , sentí que había una manera más fácil de hacer esto sin:

  • Iterando de nuevo la lista (que no hice originalmente)
  • Materializar los elementos en grupos antes de liberar el fragmento (para grandes fragmentos de elementos, habría problemas de memoria)
  • Todo el código que Sam publicó

Dicho esto, aquí hay otro pase, que he codificado en un método de extensión IEnumerable<T>llamado Chunk:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException("chunkSize",
        "The chunkSize parameter must be a positive value.");

    // Call the internal implementation.
    return source.ChunkInternal(chunkSize);
}

No hay nada sorprendente allí, solo una comprobación básica de errores.

Pasando a ChunkInternal:

private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
    this IEnumerable<T> source, int chunkSize)
{
    // Validate parameters.
    Debug.Assert(source != null);
    Debug.Assert(chunkSize > 0);

    // Get the enumerator.  Dispose of when done.
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    do
    {
        // Move to the next element.  If there's nothing left
        // then get out.
        if (!enumerator.MoveNext()) yield break;

        // Return the chunked sequence.
        yield return ChunkSequence(enumerator, chunkSize);
    } while (true);
}

Básicamente, obtiene el IEnumerator<T> e itera manualmente a través de cada elemento. Comprueba si hay algún elemento actualmente para enumerar. Después de enumerar cada fragmento, si no queda ningún elemento, se desglosa.

Una vez que detecta que hay elementos en la secuencia, delega la responsabilidad de la IEnumerable<T>implementación interna a ChunkSequence:

private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator, 
    int chunkSize)
{
    // Validate parameters.
    Debug.Assert(enumerator != null);
    Debug.Assert(chunkSize > 0);

    // The count.
    int count = 0;

    // There is at least one item.  Yield and then continue.
    do
    {
        // Yield the item.
        yield return enumerator.Current;
    } while (++count < chunkSize && enumerator.MoveNext());
}

Como MoveNextya se llamó al IEnumerator<T>pasado ChunkSequence, produce el elemento devuelto por Currenty luego incrementa el conteo, asegurándose de no devolver nunca más que chunkSizeelementos y moviéndose al siguiente elemento en la secuencia después de cada iteración (pero en cortocircuito si el número de los artículos producidos exceden el tamaño del fragmento).

Si no quedan elementos, entonces el InternalChunkmétodo hará otra pasada en el bucle externo, pero cuando MoveNextse llama por segunda vez, aún devolverá falso, según la documentación (énfasis mío):

Si MoveNext pasa el final de la colección, el enumerador se coloca después del último elemento de la colección y MoveNext devuelve falso. Cuando el enumerador está en esta posición, las llamadas posteriores a MoveNext también devuelven falso hasta que se llama a Reset.

En este punto, el bucle se romperá y la secuencia de secuencias terminará.

Esta es una prueba simple:

static void Main()
{
    string s = "agewpsqfxyimc";

    int count = 0;

    // Group by three.
    foreach (IEnumerable<char> g in s.Chunk(3))
    {
        // Print out the group.
        Console.Write("Group: {0} - ", ++count);

        // Print the items.
        foreach (char c in g)
        {
            // Print the item.
            Console.Write(c + ", ");
        }

        // Finish the line.
        Console.WriteLine();
    }
}

Salida:

Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,

Una nota importante, esto no funcionará si no drena toda la secuencia secundaria o se rompe en cualquier punto de la secuencia principal. Esta es una advertencia importante, pero si su caso de uso es que consumirá cada elemento de la secuencia de secuencias, esto funcionará para usted.

Además, hará cosas extrañas si juegas con el orden, tal como lo hizo Sam en un momento .

casperOne
fuente
Creo que esta es la mejor solución ... el único problema es que la lista no tiene Longitud ... tiene Cuenta. Pero eso es fácil de cambiar. Podemos mejorar esto ni siquiera construyendo Listas, sino devolviendo ienumerables que contienen referencias a la lista principal con una combinación de desplazamiento / longitud. Entonces, si el tamaño del grupo es grande, no desperdiciamos memoria. Comenta si quieres que lo escriba.
Amir
@Amir, me gustaría ver eso escrito
samandmoore
Esto es bueno y rápido: Cameron publicó uno muy similar después del tuyo, la única advertencia es que amortigua los fragmentos, esto puede llevar a la falta de memoria si los fragmentos y los tamaños de los elementos son grandes. Vea mi respuesta para una alternativa, aunque mucho más complicada, respuesta.
Sam Saffron
@SamSaffron Sí, si tiene una gran cantidad de elementos en el List<T>, obviamente tendrá problemas de memoria debido al almacenamiento en búfer. En retrospectiva, debería haberlo notado en la respuesta, pero parecía que en ese momento el foco estaba en demasiadas iteraciones. Dicho esto, su solución es realmente más peluda. No lo he probado, pero ahora me hace preguntarme si hay una solución menos peluda.
casperOne
@casperOne sí ... Google me dio esta página cuando estaba buscando una manera de dividir los enumerables, para mi caso de uso específico, estoy dividiendo una lista increíblemente grande de registros que se devuelven de la base de datos, si los materializo en un lista explotaría (de hecho, Dapper tiene un buffer: opción falsa solo para este caso de uso)
Sam Saffron
48

Ok, aquí está mi opinión al respecto:

  • completamente vago: funciona en infinitos enumerables
  • sin copia intermedia / almacenamiento en búfer
  • O (n) tiempo de ejecución
  • funciona también cuando las secuencias internas solo se consumen parcialmente

public static IEnumerable<IEnumerable<T>> Chunks<T>(this IEnumerable<T> enumerable,
                                                    int chunkSize)
{
    if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive");

    using (var e = enumerable.GetEnumerator())
    while (e.MoveNext())
    {
        var remaining = chunkSize;    // elements remaining in the current chunk
        var innerMoveNext = new Func<bool>(() => --remaining > 0 && e.MoveNext());

        yield return e.GetChunk(innerMoveNext);
        while (innerMoveNext()) {/* discard elements skipped by inner iterator */}
    }
}

private static IEnumerable<T> GetChunk<T>(this IEnumerator<T> e,
                                          Func<bool> innerMoveNext)
{
    do yield return e.Current;
    while (innerMoveNext());
}

Ejemplo de uso

var src = new [] {1, 2, 3, 4, 5, 6}; 

var c3 = src.Chunks(3);      // {{1, 2, 3}, {4, 5, 6}}; 
var c4 = src.Chunks(4);      // {{1, 2, 3, 4}, {5, 6}}; 

var sum   = c3.Select(c => c.Sum());    // {6, 15}
var count = c3.Count();                 // 2
var take2 = c3.Select(c => c.Take(2));  // {{1, 2}, {4, 5}}

Explicaciones

El código funciona anidando dos yielditeradores basados.

El iterador externo debe realizar un seguimiento de cuántos elementos ha consumido efectivamente el iterador interno (fragmento). Esto se hace cerrando remainingcon innerMoveNext(). Los elementos no consumidos de un fragmento se descartan antes de que el iterador externo produzca el siguiente fragmento. Esto es necesario porque de lo contrario se obtienen resultados inconsistentes, cuando los enumerables internos no se consumen (por completo) (por ejemplo c3.Count(), devolverían 6).

Nota: La respuesta se ha actualizado para abordar las deficiencias señaladas por @aolszowka.

3dGrabber
fuente
2
Muy agradable. Mi solución "correcta" fue mucho más complicada que eso. Esta es la respuesta # 1 en mi humilde opinión.
CaseyB
Esto sufre un comportamiento inesperado (desde el punto de vista de la API) cuando se llama a ToArray (), tampoco es seguro para subprocesos.
aolszowka
@aolszowka: ¿podrías por favor elaborar?
3dGrabber
@ 3dGrabber Quizás fue así como re-factoricé su código (lo siento, es demasiado largo para pasar aquí, básicamente en lugar de un método de extensión que pasé en el SourceEnumerator). El caso de prueba que utilicé fue algo en este sentido: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (arrayToSort, 3) .ToArray (); Resultó en Fuente que indica que había 13 fragmentos (el número de elementos). Esto tenía sentido para mí, ya que a menos que haya consultado las enumeraciones internas, el enumerador no se incrementó.
aolszowka
1
@aolszowka: puntos muy válidos. He agregado una advertencia y una sección de uso. El código asume que iteras sobre el enumerable interno. Sin embargo, con su solución pierde la pereza. Creo que debería ser posible obtener lo mejor de ambos mundos con un Enumerador de almacenamiento en caché personalizado. Si encuentro una solución, la publicaré aquí ...
3dGrabber
18

completamente vago, sin contar ni copiar:

public static class EnumerableExtensions
{

  public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len)
  {
     if (len == 0)
        throw new ArgumentNullException();

     var enumer = source.GetEnumerator();
     while (enumer.MoveNext())
     {
        yield return Take(enumer.Current, enumer, len);
     }
  }

  private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len)
  {
     while (true)
     {
        yield return head;
        if (--len == 0)
           break;
        if (tail.MoveNext())
           head = tail.Current;
        else
           break;
     }
  }
}
xtofs
fuente
Esta solución es tan elegante que lamento no poder votar esta respuesta más de una vez.
Mark
3
No creo que esto pueda fallar, exactamente. Pero ciertamente podría tener un comportamiento extraño. Si tuviera 100 elementos, y se dividiera en lotes de 10, y enumerara todos los lotes sin enumerar ninguno de esos lotes, terminaría con 100 lotes de 1.
CaseyB
1
Como @CaseyB mencionó, esto sufre de la misma falla 3dGrabber abordada aquí stackoverflow.com/a/20953521/1037948 , ¡pero hombre, es rápido!
drzaus
1
Esta es una hermosa solución. Hace exactamente lo que promete.
Rod Hartzell
Con mucho, la solución más elegante y precisa. Lo único es que debe agregar un cheque para números negativos y reemplazar ArgumentNullException por ArgumentException
Romain Vergnory
13

Creo que la siguiente sugerencia sería la más rápida. Estoy sacrificando la pereza de la fuente Enumerable por la capacidad de usar Array.Copy y sabiendo de antemano la duración de cada una de mis sublistas.

public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> items, int size)
{
    T[] array = items as T[] ?? items.ToArray();
    for (int i = 0; i < array.Length; i+=size)
    {
        T[] chunk = new T[Math.Min(size, array.Length - i)];
        Array.Copy(array, i, chunk, 0, chunk.Length);
        yield return chunk;
    }
}
Marc-André Bertrand
fuente
No solo el más rápido, también maneja correctamente más operaciones enumerables en el resultado, es decir, items.Chunk (5) .Reverse (). SelectMany (x => x)
también
9

Podemos mejorar la solución de @ JaredPar para hacer una verdadera evaluación perezosa. Utilizamos un GroupAdjacentBymétodo que produce grupos de elementos consecutivos con la misma clave:

sequence
.Select((x, i) => new { Value = x, Index = i })
.GroupAdjacentBy(x=>x.Index/3)
.Select(g=>g.Select(x=>x.Value))

Debido a que los grupos se obtienen uno por uno, esta solución funciona eficientemente con secuencias largas o infinitas.

Coronel Panic
fuente
8

Escribí un método de extensión Clump hace varios años. Funciona muy bien, y es la implementación más rápida aquí. :PAGS

/// <summary>
/// Clumps items into same size lots.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">The source list of items.</param>
/// <param name="size">The maximum size of the clumps to make.</param>
/// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns>
public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (size < 1)
        throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

    return ClumpIterator<T>(source, size);
}

private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
{
    Debug.Assert(source != null, "source is null.");

    T[] items = new T[size];
    int count = 0;
    foreach (var item in source)
    {
        items[count] = item;
        count++;

        if (count == size)
        {
            yield return items;
            items = new T[size];
            count = 0;
        }
    }
    if (count > 0)
    {
        if (count == size)
            yield return items;
        else
        {
            T[] tempItems = new T[count];
            Array.Copy(items, tempItems, count);
            yield return tempItems;
        }
    }
}
Cameron MacFarland
fuente
debería funcionar pero está amortiguando el 100% de los trozos, estaba tratando de evitar eso ... pero resulta ser increíblemente peludo.
Sam Saffron
@SamSaffron Sí. Especialmente si arrojas cosas como plinq a la mezcla, que es para lo que fue originalmente mi implementación.
Cameron MacFarland
amplié mi respuesta, hágame saber lo que piensa
Sam Saffron
@CameronMacFarland: ¿puede explicar por qué es necesaria la segunda verificación de count == size? Gracias.
dugas
8

System.Interactive proporciona Buffer()para este propósito. Algunas pruebas rápidas muestran que el rendimiento es similar a la solución de Sam.

dahlbyk
fuente
1
¿Conoces la semántica de amortiguación? por ejemplo: si tiene un enumerador que escupe cadenas que son 300k grandes e intenta dividirlo en trozos de 10,000 tamaños, ¿se quedará sin memoria?
Sam Saffron
Buffer()vuelve IEnumerable<IList<T>>así que sí, probablemente tengas un problema allí, no se transmite como el tuyo.
dahlbyk
7

Aquí hay una rutina de división de listas que escribí hace un par de meses:

public static List<List<T>> Chunk<T>(
    List<T> theList,
    int chunkSize
)
{
    List<List<T>> result = theList
        .Select((x, i) => new {
            data = x,
            indexgroup = i / chunkSize
        })
        .GroupBy(x => x.indexgroup, x => x.data)
        .Select(g => new List<T>(g))
        .ToList();

    return result;
}
Amy B
fuente
6

Creo que este pequeño fragmento hace el trabajo bastante bien.

public static IEnumerable<List<T>> Chunked<T>(this List<T> source, int chunkSize)
{
    var offset = 0;

    while (offset < source.Count)
    {
        yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize));
        offset += chunkSize;
    }
}
erlando
fuente
5

¿Qué hay de este?

var input = new List<string> { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };
var k = 3

var res = Enumerable.Range(0, (input.Count - 1) / k + 1)
                    .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k)))
                    .ToList();

Hasta donde yo sé, GetRange () es lineal en términos de número de elementos tomados. Entonces esto debería funcionar bien.

Roman Pekar
fuente
5

Esta es una vieja pregunta, pero esto es con lo que terminé; enumera lo enumerable solo una vez, pero crea listas para cada una de las particiones. No sufre un comportamiento inesperado cuando ToArray()se llama como algunas de las implementaciones:

    public static IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int chunkSize)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        if (chunkSize < 1)
        {
            throw new ArgumentException("Invalid chunkSize: " + chunkSize);
        }

        using (IEnumerator<T> sourceEnumerator = source.GetEnumerator())
        {
            IList<T> currentChunk = new List<T>();
            while (sourceEnumerator.MoveNext())
            {
                currentChunk.Add(sourceEnumerator.Current);
                if (currentChunk.Count == chunkSize)
                {
                    yield return currentChunk;
                    currentChunk = new List<T>();
                }
            }

            if (currentChunk.Any())
            {
                yield return currentChunk;
            }
        }
    }
aolszowka
fuente
Sería bueno convertir esto en un método de Extensión:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn
+1 por tu respuesta. Sin embargo, recomiendo dos cosas: 1. usar foreach en lugar de while y usar block. 2. Pase chunkSize en el constructor de List para que esa lista conozca su tamaño máximo esperado.
Usman Zafar
4

Encontramos que la solución de David B funcionó mejor. Pero lo adaptamos a una solución más general:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();
mwjackson
fuente
3
Esto es bueno, pero bastante diferente de lo que estaba pidiendo el autor de la pregunta original.
Amy B
4

La siguiente solución es la más compacta que se me ocurre: O (n).

public static IEnumerable<T[]> Chunk<T>(IEnumerable<T> source, int chunksize)
{
    var list = source as IList<T> ?? source.ToList();
    for (int start = 0; start < list.Count; start += chunksize)
    {
        T[] chunk = new T[Math.Min(chunksize, list.Count - start)];
        for (int i = 0; i < chunk.Length; i++)
            chunk[i] = list[start + i];

        yield return chunk;
    }
}
Marc-André Bertrand
fuente
4

Código antiguo, pero esto es lo que he estado usando:

    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
    {
        var toReturn = new List<T>(max);
        foreach (var item in source)
        {
            toReturn.Add(item);
            if (toReturn.Count == max)
            {
                yield return toReturn;
                toReturn = new List<T>(max);
            }
        }
        if (toReturn.Any())
        {
            yield return toReturn;
        }
    }
Robert McKee
fuente
Después de publicar, me di cuenta de que este es exactamente el mismo código que casperOne publicó hace 6 años con el cambio de usar .Any () en lugar de .Count () ya que no necesito el recuento completo, solo necesito saber si existe .
Robert McKee
3

Si la lista es del tipo system.collections.generic, puede usar el método "CopyTo" disponible para copiar elementos de su matriz a otras sub-matrices. Usted especifica el elemento inicial y el número de elementos para copiar.

También puede hacer 3 clones de su lista original y usar el "RemoveRange" en cada lista para reducir el tamaño de la lista al tamaño que desee.

O simplemente cree un método auxiliar para hacerlo por usted.

Jobo
fuente
2

Es una solución antigua pero tenía un enfoque diferente. Utilizo Skippara moverme al desplazamiento deseado y Takeextraer el número deseado de elementos:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
                                                   int chunkSize)
{
    if (chunkSize <= 0)
        throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0");

    var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize);

    return Enumerable.Range(0, nbChunks)
                     .Select(chunkNb => source.Skip(chunkNb*chunkSize)
                     .Take(chunkSize));
}
Bertrand
fuente
1
Muy similar a un enfoque que utilicé, pero recomiendo que la fuente no sea IEnumerable. Por ejemplo, si el origen es el resultado de una consulta LINQ, Skip / Take desencadenaría enumeraciones nbChunk de la consulta. Podría ser costoso. Mejor sería usar IList o ICollection como el tipo de fuente. Eso evita el problema por completo.
RB Davidson
2

Para cualquier persona interesada en una solución empaquetada / mantenida, la biblioteca MoreLINQ proporciona el Batchmétodo de extensión que coincide con el comportamiento solicitado:

IEnumerable<char> source = "Example string";
IEnumerable<IEnumerable<char>> chunksOfThreeChars = source.Batch(3);

La Batchimplementación es similar a la respuesta de Cameron MacFarland , con la adición de una sobrecarga para transformar el fragmento / lote antes de regresar, y funciona bastante bien.

Kevinoid
fuente
Esta debería ser la respuesta aceptada. En lugar de reinventar la rueda, se debe usar
morelinq
1

Usando particiones modulares:

public IEnumerable<IEnumerable<string>> Split(IEnumerable<string> input, int chunkSize)
{
    var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize);
    return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id));
}
Janosz G.
fuente
1

Solo pongo mis dos centavos. Si desea "agrupar" la lista (visualizar de izquierda a derecha), puede hacer lo siguiente:

 public static List<List<T>> Buckets<T>(this List<T> source, int numberOfBuckets)
    {
        List<List<T>> result = new List<List<T>>();
        for (int i = 0; i < numberOfBuckets; i++)
        {
            result.Add(new List<T>());
        }

        int count = 0;
        while (count < source.Count())
        {
            var mod = count % numberOfBuckets;
            result[mod].Add(source[count]);
            count++;
        }
        return result;
    }
mattylantz
fuente
1

Otra forma es usar el operador Rx Buffer

//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;

var observableBatches = anAnumerable.ToObservable().Buffer(size);

var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();
frhack
fuente
En mi humilde opinión la respuesta más adecuada.
Stanislav Berkov
1
public static List<List<T>> GetSplitItemsList<T>(List<T> originalItemsList, short number)
    {
        var listGroup = new List<List<T>>();
        int j = number;
        for (int i = 0; i < originalItemsList.Count; i += number)
        {
            var cList = originalItemsList.Take(j).Skip(i).ToList();
            j += number;
            listGroup.Add(cList);
        }
        return listGroup;
    }
Joy Zhu
fuente
0

Tomé la respuesta principal y lo convertí en un contenedor de COI para determinar dónde dividir. (¿ Para quién realmente busca dividirse solo en 3 elementos, al leer esta publicación mientras busca una respuesta? )

Este método permite dividir en cualquier tipo de elemento según sea necesario.

public static List<List<T>> SplitOn<T>(List<T> main, Func<T, bool> splitOn)
{
    int groupIndex = 0;

    return main.Select( item => new 
                             { 
                               Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), 
                               Value = item 
                             })
                .GroupBy( it2 => it2.Group)
                .Select(x => x.Select(v => v.Value).ToList())
                .ToList();
}

Entonces, para el OP, el código sería

var it = new List<string>()
                       { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };

int index = 0; 
var result = SplitOn(it, (itm) => (index++ % 3) == 0 );
ΩmegaMan
fuente
0

Tan performatico como el enfoque de Sam Saffron .

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero.");

    return BatchImpl(source, size).TakeWhile(x => x.Any());
}

static IEnumerable<IEnumerable<T>> BatchImpl<T>(this IEnumerable<T> source, int size)
{
    var values = new List<T>();
    var group = 1;
    var disposed = false;
    var e = source.GetEnumerator();

    try
    {
        while (!disposed)
        {
            yield return GetBatch(e, values, group, size, () => { e.Dispose(); disposed = true; });
            group++;
        }
    }
    finally
    {
        if (!disposed)
            e.Dispose();
    }
}

static IEnumerable<T> GetBatch<T>(IEnumerator<T> e, List<T> values, int group, int size, Action dispose)
{
    var min = (group - 1) * size + 1;
    var max = group * size;
    var hasValue = false;

    while (values.Count < min && e.MoveNext())
    {
        values.Add(e.Current);
    }

    for (var i = min; i <= max; i++)
    {
        if (i <= values.Count)
        {
            hasValue = true;
        }
        else if (hasValue = e.MoveNext())
        {
            values.Add(e.Current);
        }
        else
        {
            dispose();
        }

        if (hasValue)
            yield return values[i - 1];
        else
            yield break;
    }
}

}

Leandromoh
fuente
0

Puede trabajar con generadores infinitos:

a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1)))
 .Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1)))
 .Where((x, i) => i % 3 == 0)

Código de demostración: https://ideone.com/GKmL7M

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
  private static void DoIt(IEnumerable<int> a)
  {
    Console.WriteLine(String.Join(" ", a));

    foreach (var x in a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))).Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))).Where((x, i) => i % 3 == 0))
      Console.WriteLine(String.Join(" ", x));

    Console.WriteLine();
  }

  public static void Main()
  {
    DoIt(new int[] {1});
    DoIt(new int[] {1, 2});
    DoIt(new int[] {1, 2, 3});
    DoIt(new int[] {1, 2, 3, 4});
    DoIt(new int[] {1, 2, 3, 4, 5});
    DoIt(new int[] {1, 2, 3, 4, 5, 6});
  }
}
1

1 2

1 2 3
1 2 3

1 2 3 4
1 2 3

1 2 3 4 5
1 2 3

1 2 3 4 5 6
1 2 3
4 5 6

Pero en realidad preferiría escribir el método correspondiente sin linq.

Qwertiy
fuente
0

¡Mira esto! Tengo una lista de elementos con un contador de secuencia y fecha. Cada vez que se reinicia la secuencia, quiero crear una nueva lista.

Ex. lista de mensajes.

 List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

Quiero dividir la lista en listas separadas a medida que se reinicia el contador. Aquí está el código:

var arraylist = new List<List<dynamic>>();

        List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

        //group by FcntUp and CommTimestamp
        var query = messages.GroupBy(x => new { x.FcntUp, x.CommTimestamp });

        //declare the current item
        dynamic currentItem = null;

        //declare the list of ranges
        List<dynamic> range = null;

        //loop through the sorted list
        foreach (var item in query)
        {
            //check if start of new range
            if (currentItem == null || item.Key.FcntUp < currentItem.Key.FcntUp)
            {
                //create a new list if the FcntUp starts on a new range
                range = new List<dynamic>();

                //add the list to the parent list
                arraylist.Add(range);
            }

            //add the item to the sublist
            range.Add(item);

            //set the current item
            currentItem = item;
        }
Claes-Philip Staiger
fuente
-1

Para insertar mis dos centavos ...

Al usar el tipo de lista para la fuente a fragmentar, encontré otra solución muy compacta:

public static IEnumerable<IEnumerable<TSource>> Chunk<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    // copy the source into a list
    var chunkList = source.ToList();

    // return chunks of 'chunkSize' items
    while (chunkList.Count > chunkSize)
    {
        yield return chunkList.GetRange(0, chunkSize);
        chunkList.RemoveRange(0, chunkSize);
    }

    // return the rest
    yield return chunkList;
}
Patricio
fuente