Dividir uma lista em listas menores de tamanho N


210

Estou tentando dividir uma lista em uma série de listas menores.

Meu problema: minha função de dividir listas não as divide em listas do tamanho correto. Deve dividi-los em listas de tamanho 30, mas em vez disso os divide em listas de tamanho 114?

Como posso fazer minha função dividir uma lista em um número X de listas de tamanho 30 ou 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;
}

Se eu usar a função em uma lista de tamanho 144, a saída será:

Índice: 4, Tamanho: 120
Índice: 3, Tamanho: 114
Índice: 2, Tamanho: 114
Índice: 1, Tamanho: 114
Índice: 0, Tamanho: 114


1
Se uma solução LINQ for aceitável, essa pergunta pode ser útil .

Especificamente, a resposta de Sam Saffron à pergunta anterior. E, a menos que seja para um trabalho escolar, eu apenas usaria o código dele e pararia.
jcolebrand

Respostas:


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

Versão 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)); 
    }  
} 

Portanto, se eu tenho um zilhão de comprimento de lista e quero dividir em listas menores, comprimento 30, e de todas as listas menores que quero apenas tirar (1), ainda crio listas de 30 itens, dos quais jogarei fora 29 itens. Isso pode ser feito de maneira mais inteligente!
Harald Coppoolse 5/03

Isso realmente funciona? Não iria falhar na primeira divisão porque você está obtendo o intervalo nSize para nSize? Por exemplo, se nSize for 3 e minha matriz for tamanho 5, o primeiro intervalo de índice retornado seráGetRange(3, 3)
Matthew Pigram

2
@MatthewPigram testado e está funcionando. Math.Min aceita o valor mínimo, portanto, se o último pedaço for menor que nSize (2 <3), ele criará uma lista com os itens restantes.
Phate01

1
@HaraldCoppoolse o OP não pediu para selecionar, apenas para listas de divisão
Phate01

@MatthewPigram Primeira iteração - GetRange (0,3), segunda iteração - GetRange (3,2)
Serj-Tm

381

Eu sugeriria usar esse método de extensão para dividir a lista de fontes nas sub-listas pelo tamanho de bloco 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 exemplo, se você dividir a lista de 18 itens por 5 itens por bloco, ela fornecerá a lista de 4 sub-listas com os seguintes itens: 5-5-5-3.


25
Antes de usar isso na produção, certifique-se de entender quais são as implicações do tempo de execução para memória e desempenho. Só porque o LINQ pode ser sucinto, não significa que é uma boa ideia.
Nick

4
Definitivamente, @Nick, eu sugiro que, em geral, pense antes de fazer qualquer coisa. Chunking com LINQ não deve ser uma operação frequentemente repetida milhares de vezes. Geralmente, você precisa dividir as listas para processar itens por lote e / ou em paralelo.
Dmitry Pavlov

6
Eu não acho que a memória e o desempenho devam ser um grande problema aqui. Por acaso, eu tinha o requisito de dividir uma lista com mais de 200.000 registros em listas menores com cerca de 3000 cada, o que me levou a esse segmento, e testei os dois métodos e constatei que o tempo de execução é quase o mesmo. Depois disso, testei a divisão dessa lista em listas com 3 registros cada e ainda assim o desempenho está bom. Eu acho que a solução da Serj-Tm é mais direta e tem melhor manutenção.
Sojourner silencioso

2
Observe que talvez seja melhor deixar de fora os ToList()e deixe a avaliação preguiçosa fazer a mágica.
Yair Halberstadt

3
@DmitryPavlov Durante todo esse tempo, eu nunca soube ser capaz de projetar o índice assim em uma instrução select! Eu pensei que era um novo recurso até perceber que você postou isso em 2014, o que realmente me surpreendeu! Obrigado por compartilhar isso. Além disso, não seria melhor ter esse método de extensão disponível para um IEnumerable e também retornar um IEnumerable?
Aydin

37

e quanto a:

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

Isso vai consumir muita memória? Cada vez que o location.Skip.ToList acontece, pergunto-me se mais memória é alocada e se os itens não ignorados são referenciados por uma nova lista.
Zasz 12/02

2
sim nova lista é criada em cada loop. Sim, consome memória. Mas se você estiver com problemas de memória, este não é o lugar para otimizar, pois as instâncias dessas listas estão prontas para serem coletadas no próximo loop. Você pode trocar desempenho por memória pulando o, ToListmas eu não me incomodaria em tentar otimizá-lo - é tão trivial e improvável é um gargalo. O principal ganho dessa implementação é sua trivialidade, que é fácil de entender. Se você quiser, pode usar a resposta aceita, ela não cria essas listas, mas é um pouco mais complexa.
Rafal

2
.Skip(n)itera os nelementos toda vez que é chamado, enquanto isso pode ser bom, é importante considerar o código crítico de desempenho. stackoverflow.com/questions/20002975/…
Chakrava

@Chakrava claro, minha solução não deve ser usada em código crítico de desempenho, mas, em minha experiência, você primeiro escreve código de trabalho e depois determina o que é crítico em desempenho e raramente onde meu linq para operações de objetos realizadas em, digamos, 50 objetos. Isso deve ser avaliado caso a caso.
Rafal

@ Rafal Eu concordo, eu encontrei vários .Skip()s na base de código da minha empresa e, embora não sejam "ideais", eles funcionam muito bem. Coisas como operações de banco de dados levam muito mais tempo. Mas acho importante notar que .Skip()"toca" cada elemento <n em seu caminho, em vez de pular diretamente para o enésimo elemento (como você poderia esperar). Se o seu iterador tiver efeitos colaterais ao tocar em um elemento, .Skip()pode ser a causa de erros difíceis de encontrar.
Chakrava

11

A solução Serj-Tm é boa, também esta é a versão genérica como método de extensão para listas (coloque-a em uma classe 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

Acho a resposta aceita (Serj-Tm) mais robusta, mas gostaria de sugerir uma versão 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

A biblioteca MoreLinq tem o método chamado 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++;
}

O resultado é

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 são divididos em 5 pedaços com 2 elementos.


Essa precisa ser a resposta aceita. Ou pelo menos muito mais alto nesta página.
Zar Shardan

7

Eu tenho um método genérico que poderia levar qualquer tipo, incluindo float, e foi testado por unidade, espero que ajude:

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

Obrigado. Gostaria de saber se você poderia atualizar os comentários com a definição de parâmetro maxCount? Uma rede de segurança?
Andrew Jens

2
tenha cuidado com várias enumerações do enumerável. values.Count()causará uma enumeração completa e depois values.ToList()outra. Mais seguro values = values.ToList(), já está materializado.
mhand

7

Embora muitas das respostas acima façam o trabalho, todas elas falham terrivelmente em uma sequência sem fim (ou uma sequência realmente longa). A seguir, uma implementação totalmente on-line que garante a melhor complexidade possível de tempo e memória. Apenas iteramos a fonte enumerável exatamente uma vez e usamos o retorno de rendimento para uma avaliação lenta. O consumidor pode jogar fora a lista em cada iteração, tornando o espaço de memória igual ao da lista com 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;
        }
    }
}

Edição: Agora, percebendo que o OP pergunta sobre dividir um List<T>em menor List<T>, então meus comentários sobre enumeráveis ​​infinitos não são aplicáveis ​​ao OP, mas podem ajudar outras pessoas que acabam aqui. Esses comentários foram uma resposta a outras soluções postadas que usam IEnumerable<T>como entrada para sua função, mas enumeram a fonte enumerável várias vezes.


Eu acho que a IEnumerable<IEnumerable<T>>versão é melhor, pois não envolve muita Listconstrução.
NetMage

@NetMage - um problema IEnumerable<IEnumerable<T>>é que a implementação provavelmente dependerá do consumidor enumerar completamente cada enumerável interno gerado. Tenho certeza de que uma solução pode ser formulada de maneira a evitar esse problema, mas acho que o código resultante pode ficar complexo rapidamente. Além disso, como é preguiçoso, estamos gerando apenas uma lista por vez e a alocação de memória acontece exatamente uma vez por lista, pois sabemos o tamanho antecipadamente.
mhand

Você está certo - minha implementação usa um novo tipo de enumerador (um enumerador de posição) que rastreia sua posição atual envolvendo um enumerador padrão e vamos para uma nova posição.
NetMage 12/11/19

6

Adição após comentário muito útil de mhand no final

Resposta original

Embora a maioria das soluções possa funcionar, acho que não são muito eficientes. Suponha que você queira apenas os primeiros itens dos primeiros pedaços. Então você não gostaria de iterar sobre todos os (zilhões) itens em sua sequência.

O seguinte irá, no máximo, enumerar duas vezes: uma para o Take e outra para o Skip. Não enumerará mais elementos do que você 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
    }
}

Quantas vezes isso enumerará a sequência?

Suponha que você divida sua fonte em pedaços de chunkSize. Você enumera apenas os primeiros N pedaços. De cada pedaço enumerado, você enumerará apenas os primeiros M elementos.

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

o Any receberá o Enumerador, faça 1 MoveNext () e retornará o valor retornado após Disposing the Enumerator. Isso será feito N vezes

yield return source.Take(chunkSize);

De acordo com a fonte de referência, isso fará 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;
    }
}

Isso não faz muito até você começar a enumerar sobre o Chunk buscado. Se você buscar vários Chunks, mas decidir não enumerar o primeiro Chunk, o foreach não será executado, pois seu depurador mostrará a você.

Se você decidir pegar os primeiros M elementos do primeiro pedaço, o retorno do rendimento será executado exatamente M vezes. Isso significa:

  • obter o enumerador
  • chame MoveNext () e M atual.
  • Descarte o enumerador

Depois que o primeiro pedaço tiver sido retornado, pulamos esse primeiro pedaço:

source = source.Skip(chunkSize);

Mais uma vez: vamos dar uma olhada na fonte de referência para encontrar oskipiterator

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 você vê, as SkipIteratorchamadas são feitas MoveNext()uma vez para cada elemento no Chunk. Não chamaCurrent .

Portanto, por Chunk, vemos o seguinte:

  • Qualquer um (): GetEnumerator; 1 MoveNext (); Dispose Enumerator;
  • Leva():

    • nada se o conteúdo do pedaço não for enumerado.
    • Se o conteúdo estiver enumerado: GetEnumerator (), um MoveNext e um Current por item enumerado, Dispose enumerator;

    • Skip (): para cada pedaço enumerado (NÃO o conteúdo do pedaço): GetEnumerator (), MoveNext () chunkSize times, no Current! Dispose enumerator

Se você observar o que acontece com o enumerador, verá muitas chamadas para MoveNext () e apenas chamadas para Current para os itens do TSource que você realmente decide acessar.

Se você usar N Chunks de tamanho chunkSize, chamará MoveNext ()

  • N vezes para Qualquer ()
  • ainda não há tempo para o Take, desde que você não enumere os Chunks
  • N times chunkSize for Skip ()

Se você decidir enumerar apenas os primeiros M elementos de cada pedaço buscado, precisará chamar MoveNext M vezes por pedaço enumerado.

O total

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

Portanto, se você decidir enumerar todos os elementos de todos os pedaços:

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

Se o MoveNext é muito trabalhoso ou não, depende do tipo de sequência de origem. Para listas e matrizes, é um simples incremento de índice, talvez com uma verificação fora do intervalo.

Mas se o seu IEnumerable for o resultado de uma consulta ao banco de dados, verifique se os dados estão realmente materializados no seu computador, caso contrário, os dados serão buscados várias vezes. O DbContext e o Dapper transferirão adequadamente os dados para o processo local antes que possam ser acessados. Se você enumerar a mesma sequência várias vezes, ela não será buscada várias vezes. Dapper retorna um objeto que é uma Lista, o DbContext lembra que os dados já foram buscados.

Depende do seu Repositório se é sensato chamar AsEnumerable () ou ToLists () antes de começar a dividir os itens em Chunks.


isso não será enumerado duas vezes por lote? então estamos realmente enumerando os 2*chunkSizetempos de origem ? Isso é mortal, dependendo da fonte do enumerável (talvez com suporte a DB ou outra fonte não memorizada). Imagine isso enumeráveis como entrada Enumerable.Range(0, 10000).Select(i => DateTime.UtcNow)- você vai ter horários diferentes a cada vez que você enumerar a enumeráveis vez que não é memoized
mhand

Considere o seguinte: Enumerable.Range(0, 10).Select(i => DateTime.UtcNow). Invocando, Anyvocê recalculará a hora atual de cada vez. Não é tão ruim assim DateTime.UtcNow, mas considere um enumerável apoiado por uma conexão com o banco de dados / cursor sql ou similar. Já vi casos em que milhares de chamadas DB foram emitidos porque o desenvolvedor não entendia as potenciais repercussões de 'vários enumerações de um enumeráveis' - ReSharper fornece uma dica para isso também
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? A ideia era usar apenas um loop. E, quem sabe, talvez você esteja usando apenas implementações do IList no seu código e não queira transmitir para a 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

Mais um

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
é preferível explicar sua resposta, em vez de fornecer apenas um trecho de código
Kevin

0

Eu havia encontrado essa mesma necessidade e usei uma combinação dos métodos Skip () e Take () do Linq . Eu multiplico o número que recebo pelo número de iterações até agora, e isso me dá o número de itens a serem ignorados, então eu pego o próximo 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

Com base em Dimitry Pavlov respondere eu removeria .ToList(). E também evite a classe anônima. Em vez disso, gosto de usar uma estrutura que não requer uma alocação de memória heap. (A ValueTupletambém faria o trabalho.)

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

Isso pode ser usado como o seguinte, que apenas repete a coleção uma vez e também não aloca nenhuma memória significativa.

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

Se uma lista concreta for realmente necessária, eu faria assim:

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