Разделить список на меньшие списки размера N

Я пытаюсь разбить список на ряд меньших списков.

Моя проблема: Моя функция разбиения списков не разбивает их на списки нужного размера. Он должен разбить их на списки размером 30, но вместо этого он разбивает их на списки размером 114?

Как я могу сделать свою функцию разбитой список на X число списков размером 30 или меньше?

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

Если я использую функцию в списке размером 144, то вывод будет следующим:

Индекс: 4, Размер: 120
Индекс: 3, Размер: 114
Индекс: 2, Размер: 114
Индекс: 1, Размер: 114
Индекс: 0, Размер: 114

Ответ 1

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

Общая версия:

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

Ответ 2

Я бы предложил использовать этот метод расширения, чтобы разделить исходный список на подсписки с указанным размером фрагмента:

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

Например, если вы разбиваете список из 18 элементов на 5 элементов на блок, он дает вам список из 4 подсписков со следующими элементами внутри: 5-5-5-3.

Ответ 3

как насчет:

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

Ответ 4

Решение Serj-Tm прекрасное, также это универсальная версия как метод расширения для списков (поместите его в статический класс):

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

Ответ 5

Я нахожу принятый ответ (Serj-Tm) наиболее надежным, но я хотел бы предложить общую версию.

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

Ответ 6

У меня есть общий метод, который принимает любые типы, включая float, и он был проверен на единицу, надеюсь, что это поможет:

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

Ответ 7

Библиотека MoreLinq имеет метод под названием 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++;
}

Результат

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 разделены на 5 кусков с двумя элементами.

Ответ 8

Хотя многие из приведенных выше ответов выполняют свою работу, все они ужасно терпят неудачу в бесконечной последовательности (или очень длинной последовательности). Следующее является полностью онлайн-реализацией, которая гарантирует лучшее время и возможную сложность памяти. Мы только один раз повторяем перечислимый источник и используем ленивый возврат для ленивых вычислений. Потребитель может выбрасывать список на каждой итерации, делая объем памяти равным количеству элементов списка /batchSize.

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

РЕДАКТИРОВАТЬ: Только что реализация ОП требует разбить List<T> на меньший List<T>, поэтому мои комментарии относительно бесконечных перечислимых чисел неприменимы к OP, но могут помочь другим, кто окажется здесь. Эти комментарии были в ответ на другие опубликованные решения, которые используют IEnumerable<T> в качестве входных данных для своей функции, но перечисляют источник, перечисляемый несколько раз.

Ответ 9

Дополнение после очень полезного комментария mhand в конце

Оригинальный ответ

Хотя большинство решений могут работать, я думаю, что они не очень эффективны. Предположим, если вы хотите только первые несколько элементов из первых нескольких кусков. Тогда вы не захотите перебирать все (zillion) элементы в вашей последовательности.

Следующее будет, в крайнем случае, перечисляться дважды: один раз для дубля и один раз для пропуска. Он не будет перечислять больше элементов, чем вы будете использовать:

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

Сколько раз это будет Перечислять последовательность?

Предположим, вы поделили свой источник на куски chunkSize. Вы перечисляете только первые N кусков. Из каждого перечисленного фрагмента вы будете перечислять только первые М элементов.

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

Any получит Enumerator, выполнит 1 MoveNext() и вернет возвращенное значение после удаления Enumerator. Это будет сделано N раз

yield return source.Take(chunkSize);

Согласно справочному источнику это будет делать что-то вроде:

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

Это не делает ничего, пока вы не начнете перечислять по извлеченному чанку. Если вы выбираете несколько чанков, но решаете не перечислять первый чанк, то foreach не выполняется, как вам покажет ваш отладчик.

Если вы решите взять первые M элементов первого блока, то возврат доходности будет выполнен ровно M раз. Это означает:

  • получить счетчик
  • вызовите MoveNext() и текущий M раз.
  • Утилизировать перечислитель

После возврата первого чанка мы пропускаем этот первый чанк:

source = source.Skip(chunkSize);

Еще раз: мы посмотрим на справочный источник, чтобы найти skipiterator

static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
    using (IEnumerator<TSource> e = source.GetEnumerator()) 
    {
        while (count > 0 && e.MoveNext()) count--;
        if (count <= 0) 
        {
            while (e.MoveNext()) yield return e.Current;
        }
    }
}

Как видите, SkipIterator вызывает MoveNext() один раз для каждого элемента в чанке. Это не вызывает Current.

Итак, для каждого чанка мы видим, что сделано следующее:

  • Any(): GetEnumerator; 1 MoveNext(); Утилизировать перечислитель;
  • Take():

    • ничего, если содержимое чанка не перечислено.
    • Если содержимое перечислено: GetEnumerator(), один MoveNext и один Current для каждого перечисляемого элемента, Dispose enumerator;

    • Skip(): для каждого перечисляемого чанка (НЕ для содержимого чанка): GetEnumerator(), MoveNext() chunkSize times, без Current! Распорядиться перечислителем

Если вы посмотрите на то, что происходит с перечислителем, вы увидите, что существует много вызовов MoveNext(), и только вызовы Current для элементов TSource, к которым вы фактически решаете обратиться.

Если вы возьмете N Chunks размером chunkSize, то вызовите MoveNext()

  • N раз для любого()
  • пока нет времени для Take, пока вы не перечислите чанки
  • N раз chunkSize для Пропустить()

Если вы решили перечислить только первые M элементов каждого извлеченного фрагмента, то вам нужно вызывать MoveNext M раз для каждого перечисленного блока.

Общая

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

Так что если вы решили перечислить все элементы всех кусков:

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

Будет ли MoveNext работать много или нет, зависит от типа исходной последовательности. Для списков и массивов это простое увеличение индекса, возможно, с проверкой вне диапазона.

Но если ваш IEnumerable является результатом запроса к базе данных, убедитесь, что данные действительно материализованы на вашем компьютере, в противном случае данные будут выбираться несколько раз. DbContext и Dapper правильно передадут данные в локальный процесс, прежде чем они будут доступны. Если вы перечислите одну и ту же последовательность несколько раз, она не будет выбрана несколько раз. Dapper возвращает объект, который является списком, DbContext запоминает, что данные уже получены.

От вашего репозитория зависит, целесообразно ли вызывать AsEnumerable() или ToLists(), прежде чем вы начнете делить элементы на куски

Ответ 10

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

Ответ 11

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

Ответ 12

Как насчет этого? Идея заключалась в том, чтобы использовать только один цикл. И, кто знает, может быть, вы используете только реализации IList для своего кода и не хотите приводить к List.

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

Ответ 13

Еще один

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

Ответ 14

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