Dividir lista em sublistas com LINQ


377

Existe alguma maneira de separar um List<SomeObject>em várias listas separadas SomeObject, usando o índice do item como o delimitador de cada divisão?

Deixe-me exemplificar:

Eu tenho um List<SomeObject>e preciso de um List<List<SomeObject>>ou List<SomeObject>[], para que cada uma dessas listas resultantes contenha um grupo de 3 itens da lista original (sequencialmente).

por exemplo.:

  • 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]

Eu também precisaria do tamanho da lista resultante para ser um parâmetro dessa função.

Respostas:


378

Tente o seguinte 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();
}

A idéia é primeiro agrupar os elementos por índices. Dividir por três tem o efeito de agrupá-los em grupos de 3. Em seguida, converta cada grupo em uma lista e o IEnumerablede Listem a Listde Lists


21
GroupBy faz uma classificação implícita. Isso pode prejudicar o desempenho. O que precisamos é de algum tipo de inverso do SelectMany.
yfeldblum

5
@ Justiça, o GroupBy pode ser implementado por hash. Como você sabe que a implementação do GroupBy "pode ​​prejudicar o desempenho"?
Amy B

5
GroupBy não retorna nada até que esteja enumerado todos os elementos. Por isso é lento. As listas que o OP deseja são contíguas, portanto, um método melhor poderia gerar a primeira sublist [a,g,e]antes de enumerar mais a lista original.
Coronel Panic

9
Veja o exemplo extremo de um IEnumerable infinito. GroupBy(x=>f(x)).First()nunca produzirá um grupo. O OP perguntou sobre listas, mas se escrevermos para trabalhar com o IEnumerable, fazendo apenas uma única iteração, colheremos a vantagem de desempenho.
Coronel Panic

8
@ Nick Order não é preservado seu caminho embora. Ainda é bom saber, mas você os agruparia em (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Se a ordem não importa, tudo bem, mas neste caso parece que importa.
Reafexus 5/09/2013

325

Essa pergunta é um pouco antiga, mas acabei de escrever isso e acho que é um pouco mais elegante que as outras soluções propostas:

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

14
Adoro esta solução. Eu recomendo adicionar esta verificação de sanidade para evitar um loop infinito: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
mroach

10
Eu gosto disso, mas não é super eficiente
Sam Saffron

51
Eu gosto deste, mas a eficiência do tempo é O(n²). Você pode percorrer a lista e obter um O(n)tempo.
Hippy

8
@hIpPy, como é n ^ 2? Parece linear para mim #
214 Vivek Maharajh

13
@vivekmaharajh sourceé substituído por um empacotado IEnumerablecada vez. Assim, tendo elementos de sourcepassa através de camadas de Skips
Lasse Espeholt

99

Em geral, a abordagem sugerida por CaseyB funciona bem; na verdade, se você está passando uma tarefa , List<T>é difícil culpá-la, talvez eu a mude para:

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

O que evitará enormes cadeias de chamadas. No entanto, essa abordagem tem uma falha geral. Ele materializa duas enumerações por bloco, para destacar o problema, tente executar:

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

Para superar isso, podemos tentar a abordagem de Cameron , que passa no teste acima em cores vivas, uma vez que apenas percorre a enumeração uma vez.

O problema é que ele tem uma falha diferente, materializa todos os itens de cada bloco, o problema dessa abordagem é que você fica com muita memória.

Para ilustrar isso, tente executar:

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

Por fim, qualquer implementação deve ser capaz de lidar com iteração fora de ordem de blocos, por exemplo:

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

Muitas soluções altamente ideais, como a minha primeira revisão desta resposta, falharam por lá. O mesmo problema pode ser visto na resposta otimizada do casperOne .

Para resolver todos esses problemas, você pode usar o seguinte:

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


        }

    }
}

Há também uma série de otimizações que você pode introduzir para iteração fora de ordem de blocos, que está fora do escopo aqui.

Quanto a qual método você deve escolher? Depende totalmente do problema que você está tentando resolver. Se você não está preocupado com a primeira falha, a resposta simples é incrivelmente atraente.

Observe que, como na maioria dos métodos, isso não é seguro para multiencadeamento, as coisas podem ficar estranhas se você desejar torná-lo seguro para threads, você precisará alterar EnumeratorWrapper.


O bug seria Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () está errado ou Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () lançando uma exceção?
Cameron MacFarland

@ SamSaffron Atualizei minha resposta e simplifiquei tremendamente o código, pois considero o caso de uso mais importante (e reconheço as advertências).
precisa saber é o seguinte

Que tal chunking IQueryable <>? Meu palpite é que uma abordagem Take / Skip seria ideal se queremos delegar um máximo de operações para o provedor
Guillaume86

@ Guillaume86 Concordo, se você tem um IList ou IQueryable você pode tomar todos os tipos de atalhos que tornam este muito mais rápido (Linq faz isso internamente para todos os tipos de outros métodos)
Sam Saffron

11
Essa é de longe a melhor resposta para eficiência. Estou tendo um problema ao usar o SqlBulkCopy com um IEnumerable que executa processos adicionais em cada coluna, portanto, ele deve ser executado de maneira eficiente com apenas uma passagem. Isso me permitirá dividir o IEnumerable em pedaços de tamanho gerenciável. (Para quem está se perguntando, ativei o modo de streaming do SqlBulkCopy, que parece estar quebrado).
Brain2000

64

Você pode usar várias consultas que usam Takee Skip, mas acredito que adicionariam muitas iterações à lista original.

Em vez disso, acho que você deve criar um iterador próprio, assim:

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

Você pode chamar isso e ele está ativado para LINQ, para que você possa executar outras operações nas seqüências resultantes.


À luz da resposta de Sam , senti que havia uma maneira mais fácil de fazer isso sem:

  • Iterando a lista novamente (o que não fiz originalmente)
  • Materializar os itens em grupos antes de liberar o pedaço (para pedaços grandes de itens, haveria problemas de memória)
  • Todo o código que Sam postou

Dito isto, aqui está outra passagem, que eu codifiquei em um método de extensão IEnumerable<T>chamado 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);
}

Nada de surpreendente lá em cima, apenas uma verificação básica de erros.

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

Basicamente, ele obtém IEnumerator<T>e itera manualmente através de cada item. Ele verifica se há algum item a ser enumerado no momento. Depois que cada pedaço é enumerado, se não houver nenhum item, ele será interrompido.

Depois de detectar que há itens na sequência, ele delega a responsabilidade pela IEnumerable<T>implementação interna para 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 MoveNextjá foi chamado no IEnumerator<T>passado ChunkSequence, ele gera o item retornado Currente depois incrementa a contagem, certificando-se de nunca retornar mais do que chunkSizeitens e passar para o próximo item na sequência após cada iteração (mas em curto-circuito se o número de itens produzidos excede o tamanho do pedaço).

Se não houver itens restantes, o InternalChunkmétodo fará outra passagem no loop externo, mas quando MoveNextfor chamado pela segunda vez, ainda retornará falso, conforme a documentação (ênfase minha):

Se MoveNext passar o final da coleção, o enumerador será posicionado após o último elemento da coleção e MoveNext retornará false. Quando o enumerador está nessa posição, as chamadas subseqüentes para MoveNext também retornam false até que Reset seja chamado.

Nesse ponto, o loop será interrompido e a sequência de sequências será encerrada.

Este é um teste simples:

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

Resultado:

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

Uma observação importante: isso não funcionará se você não drenar a sequência filho inteira ou interromper em qualquer ponto da sequência pai. Essa é uma ressalva importante, mas se o seu caso de uso for o de consumir todos os elementos da sequência de sequências, isso funcionará para você.

Além disso, ele fará coisas estranhas se você jogar com a ordem, assim como Sam fez em um ponto .


Eu acho que essa é a melhor solução ... o único problema é que a lista não tem Comprimento ... tem Contagem. Mas isso é fácil de mudar. Podemos melhorar isso nem construindo Listas, mas retornando inumeráveis ​​que contêm referências à lista principal com uma combinação deslocamento / comprimento. Então, se o tamanho do grupo é grande, não desperdiçamos memória. Comente se você quer que eu escreva.
Amir

@Amir eu gostaria de ver que escrito
samandmoore

Isso é legal e rápido - Cameron postou um bem parecido com o seu, mas só ressalta que ele armazena pedaços, isso pode levar à falta de memória se os pedaços e o tamanho dos itens forem grandes. Veja minha resposta para uma resposta alternativa, embora muito mais cabeluda.
Sam Saffron

@ SamSaffron Sim, se você tiver um grande número de itens List<T>, obviamente terá problemas de memória por causa do buffer. Em retrospecto, eu deveria ter notado isso na resposta, mas parecia que no momento o foco estava em muitas iterações. Dito isto, sua solução é realmente mais cabeluda. Não testei, mas agora me pergunto se há uma solução menos cabeluda.
CasperOne

@casperOne yeah ... O Google me deu esta página quando eu estava procurando uma maneira de dividir enumeráveis, para meu caso de uso específico, estou dividindo uma lista incrivelmente grande de registros retornados do banco de dados, se eu os materializar para um lista que iria explodir (na verdade dapper tem um buffer: false opção apenas para este caso de uso)
Sam Saffron

48

Ok, aqui está a minha opinião:

  • completamente preguiçoso: funciona em enumeráveis ​​infinitos
  • sem cópia / buffer intermediário
  • O (n) tempo de execução
  • funciona também quando seqüências internas são consumidas apenas 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());
}

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

Explicações

O código funciona aninhando dois yielditeradores baseados.

O iterador externo deve acompanhar quantos elementos foram efetivamente consumidos pelo iterador interno (parte). Isso é feito encerrando remainingcom innerMoveNext(). Os elementos não consumidos de um pedaço são descartados antes que o próximo pedaço seja produzido pelo iterador externo. Isso é necessário porque, caso contrário, você obtém resultados inconsistentes, quando os enumeráveis ​​internos não são (completamente) consumidos (por exemplo c3.Count(), retornariam 6).

Nota: A resposta foi atualizada para solucionar as deficiências apontadas por @aolszowka.


2
Muito agradável. Minha solução "correta" era muito mais complicada do que isso. Esta é a resposta nº 1 do IMHO.
CaseyB

Isso sofre um comportamento inesperado (do ponto de vista da API) quando ToArray () é chamado, também não é seguro para threads.
Aolszowka

@aolszowka: você poderia, por favor, elaborar?
3dGrabber 14/05

@ 3dGrabber Talvez tenha sido como eu refatorou seu código (desculpe, é um pouco longo demais para passar aqui, basicamente, em vez de um método de extensão que eu passei no sourceEnumerator). O caso de teste que usei foi algo nesse 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 (); Resultou em Origem, indicando que havia 13 blocos (o número de elementos). Isso fazia sentido para mim, a menos que você consultasse as enumerações internas, o Enumerador não foi incrementado.
Aolszowka

11
@aolszowka: pontos muito válidos. Adicionei um aviso e uma seção de uso. O código pressupõe que você itere sobre o enumerável interno. Com sua solução, você perde a preguiça. Eu acho que deveria ser possível obter o melhor dos dois mundos com um IEnumerator em cache personalizado. Se eu encontrar uma solução, vou publicá-la aqui ...
3dGrabber

18

completamente preguiçoso, sem contar ou 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;
     }
  }
}

Esta solução é tão elegante que lamento não poder votar mais de uma vez nesta resposta.
Mark

3
Eu acho que isso nunca iria falhar exatamente. Mas certamente poderia ter algum comportamento estranho. Se você tivesse 100 itens, e você dividir em lotes de 10, e você enumerou todos os lotes sem enumerar nenhum item desses lotes, você pode acabar com 100 lotes de 1.
CaseyB

11
Como o @CaseyB mencionou, isso sofre com o mesmo 3dGrabber que falhou aqui , stackoverflow.com/a/20953521/1037948 , mas o homem é rápido!
drzaus

11
Esta é uma solução bonita. Faz exatamente o que promete.
Rod Hartzell 16/02

De longe, a solução mais elegante e objetiva. A única coisa é, você deve adicionar um cheque para números negativos, e substituir o ArgumentNullException por um ArgumentException
Romain Vergnory

13

Eu acho que a sugestão a seguir seria a mais rápida. Estou sacrificando a preguiça da fonte Enumerable pela capacidade de usar Array.Copy e sabendo antecipadamente a duração de cada uma das minhas 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;
    }
}

Não apenas mais rápida, ele também lida corretamente outras operações enumeráveis no resultado, ou seja, items.Chunk (5) .reverse () SelectMany (x => x).
também

9

Podemos melhorar a solução do @ JaredPar para fazer uma verdadeira avaliação preguiçosa. Usamos um GroupAdjacentBymétodo que gera grupos de elementos consecutivos com a mesma chave:

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

Como os grupos são gerados um por um, essa solução trabalha eficientemente com sequências longas ou infinitas.


8

Eu escrevi um método de extensão Clump há vários anos. Funciona muito bem e é a implementação mais rápida aqui. : P

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

deveria funcionar, mas está protegendo 100% dos pedaços, eu estava tentando evitar isso ... mas acaba sendo incrivelmente peludo.
Sam Saffron

@SamSaffron Yep. Especialmente se você colocar coisas como o plinq na mistura, e foi para isso que minha implementação foi originalmente.
Cameron MacFarland

expandiu a minha resposta, deixe-me saber o que você pensa
Sam Saffron

@CameronMacFarland - você pode explicar por que a segunda verificação do tamanho da contagem == é necessária? Obrigado.
dugas 26/09

8

System.Interactive fornece Buffer()para esta finalidade. Alguns testes rápidos mostram que o desempenho é semelhante à solução de Sam.


11
você conhece a semântica de buffer? por exemplo: se você tiver um enumerador que cospe strings com 300k de tamanho e tente dividi-lo em 10.000 pedaços de tamanho, ficará sem memória?
Sam Saffron

Buffer()retorna, IEnumerable<IList<T>>então sim, você provavelmente terá um problema lá - ele não é transmitido como o seu.
Dahlbyk

7

Aqui está uma rotina de divisão de listas que escrevi há alguns 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;
}

6

Acho que esse pequeno trecho faz o trabalho muito bem.

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

5

Que tal 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();

Tanto quanto eu sei, GetRange () é linear em termos de número de itens obtidos. Portanto, isso deve ter um bom desempenho.


5

Essa é uma pergunta antiga, mas foi com isso que acabei; enumera o enumerável apenas uma vez, mas cria listas para cada uma das partições. Não sofre comportamento inesperado quando ToArray()é chamado, como algumas das implementações:

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

Seria bom para converter isso em um método de extensão:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn

+1 para sua resposta. No entanto, eu recomendo duas coisas 1. use foreach em vez de while e usando o bloco. 2. Passe chunkSize no construtor de List para que a lista conheça seu tamanho máximo esperado.
precisa saber é o seguinte

4

Descobrimos que a solução de David B funcionou melhor. Mas nós o adaptamos a uma solução mais geral:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();

3
Isso é legal, mas bem diferente do que o solicitante original estava pedindo.
Amy B

4

Esta solução a seguir é a mais compacta que eu pude apresentar que é 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;
    }
}

4

Código antigo, mas é isso que eu tenho usado:

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

Depois de postar, percebi que esse é exatamente o mesmo código que o casperOne postou há 6 anos com a alteração do uso de .Any () em vez de .Count () porque eu não preciso da contagem inteira, só preciso saber se existe alguma .
Robert McKee

3

Se a lista for do tipo system.collections.generic, você poderá usar o método "CopyTo" disponível para copiar elementos da sua matriz para outras sub-matrizes. Você especifica o elemento inicial e o número de elementos a serem copiados.

Você também pode criar 3 clones da sua lista original e usar o "RemoveRange" em cada lista para reduzir a lista para o tamanho desejado.

Ou apenas crie um método auxiliar para fazer isso por você.


2

É uma solução antiga, mas eu tinha uma abordagem diferente. Eu uso Skippara mover para o deslocamento desejado e Takeextrair o número desejado 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));
}

11
Muito semelhante a uma abordagem que usei, mas recomendo que a fonte não seja IEnumerable. Por exemplo, se source for o resultado de uma consulta LINQ, o Skip / Take acionará as enumerações nbChunk da consulta. Pode ficar caro. Melhor seria usar IList ou ICollection como o tipo de fonte. Isso evita o problema completamente.
RB Davidson

2

Para qualquer pessoa interessada em uma solução empacotada / mantida, a biblioteca MoreLINQ fornece o Batchmétodo de extensão que corresponde ao comportamento solicitado:

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

A Batchimplementação é semelhante à resposta de Cameron MacFarland , com a adição de uma sobrecarga para transformar o bloco / lote antes de retornar e apresenta um bom desempenho.


essa deve ser a resposta aceita. Em vez de reinventar a roda, MoreLINQ deve ser usado
Otabek Kholikov

1

Usando particionamento modular:

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

1

Apenas colocando meus dois centavos. Se você quiser "agrupar" a lista (visualize da esquerda para a direita), faça o seguinte:

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

1

Outra maneira é usar o 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();

IMHO resposta mais porper.
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;
    }

0

Peguei a resposta principal e fiz com que fosse um contêiner do COI para determinar onde dividir. ( Para quem realmente deseja dividir apenas três itens, ao ler esta postagem enquanto procura uma resposta? )

Este método permite dividir qualquer tipo de item, conforme necessário.

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

Portanto, para o OP, o código seria

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

0

Tão performático quanto a abordagem 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;
    }
}

}


0

Pode trabalhar com geradores 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 demonstração: 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

Mas na verdade eu preferiria escrever o método correspondente sem linq.


0

Veja isso! Eu tenho uma lista de elementos com um contador de seqüência e data. Para cada vez que a sequência é reiniciada, desejo criar uma nova lista.

Ex. lista de mensagens.

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

Quero dividir a lista em listas separadas quando o contador reiniciar. Aqui está o 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;
        }

-1

Para inserir meus dois centavos ...

Usando o tipo de lista para a fonte ser dividida, encontrei outra solução muito 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;
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.