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


210

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

Моя проблема: Моя функция разбивать списки не разбивает их на списки правильного размера. Он должен разбить их на списки размером 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
Если решение LINQ является приемлемым, этот вопрос может помочь .

В частности, ответ Сэма Шафрона на этот предыдущий вопрос. И если это не для школьного задания, я бы просто использовал его код и остановился.
Jcolebrand

Ответы:


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

Универсальная версия:

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

Поэтому, если у меня есть zillion длины List, и я хочу разделить на более мелкие списки Length 30, и из каждого меньшего списка я хочу взять только Take (1), тогда я все равно создаю списки из 30 предметов, из которых я выбрасываю 29 предметов. Это можно сделать умнее!
Харальд Коппулс

Это на самом деле работает? Разве он не потерпит неудачу при первом разделении, потому что вы получаете диапазон от nSize до nSize? Например, если nSize равен 3, а мой массив имеет размер 5, то возвращается первый индексный диапазонGetRange(3, 3)
Мэтью Пиграм,

2
@MatthewPigram протестирован, и он работает. Math.Min принимает значение min, поэтому, если последний кусок меньше nSize (2 <3), он создает список с оставшимися элементами.
Phate01

1
@HaraldCoppoolse ОП не просил о выборе, только чтобы разделить списки
Phate01

@MatthewPigram Первая итерация - GetRange (0,3), вторая итерация - GetRange (3,2)
Serj-Tm

381

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

/// <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.


25
Прежде чем использовать это в работе, убедитесь, что вы понимаете, каковы последствия времени для памяти и производительности. То, что LINQ может быть кратким, не означает, что это хорошая идея.
Ник

4
Определенно, @Nick, я бы предложил вообще подумать, прежде чем что-то делать. Разделение на части с помощью LINQ не должно часто повторяться тысячу раз. Обычно вам нужно составлять списки блоков для обработки элементов в пакетном режиме и / или параллельно.
Дмитрий Павлов

6
Я не думаю, что память и производительность должны быть большой проблемой здесь. У меня возникло требование разбить список с более чем 200 000 записей на более мелкие списки по около 3000 каждый, что привело меня к этой теме, и я протестировал оба метода и обнаружил, что время выполнения почти одинаково. После этого я протестировал разбиение этого списка на списки по 3 записи в каждом, и все же производительность в порядке. Я считаю, что решение Serj-Tm более простое и лучше обслуживаемое.
Silent Sojourner

2
Обратите внимание, что, возможно, было бы лучше отказаться от ToList()s, и пусть ленивая оценка сделает это волшебством.
Яир Хальберштадт

3
@DmitryPavlov В течение всего этого времени я никогда не знал о возможности проецировать индекс таким образом в операторе выбора! Я думал, что это новая функция, пока я не заметил, что вы опубликовали это в 2014 году, что меня очень удивило! Спасибо, что поделились этим. Кроме того, не лучше ли иметь этот метод расширения, доступный для IEnumerable, а также возвращать IEnumerable?
Айдын

37

как насчет:

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

Это будет занимать много памяти? Каждый раз, когда происходит location.Skip.ToList, мне интересно, выделено ли больше памяти, а ссылки на не пропущенные элементы указаны в новом списке.
Zasz

2
да новый список создается в каждом цикле. Да, он потребляет память. Но если у вас есть проблемы с памятью, это не место для оптимизации, так как экземпляры этих списков готовы для сбора в следующем цикле. Вы можете обменять производительность на память, пропустив, ToListно я не стал бы пытаться оптимизировать ее - она ​​настолько тривиальна и вряд ли является узким местом. Основным преимуществом этой реализации является ее тривиальность, которую легко понять. Если вы хотите, вы можете использовать принятый ответ, он не создает эти списки, но немного сложнее.
Рафал

2
.Skip(n)перебирает nэлементы каждый раз, когда он вызывается, хотя это может быть нормально, это важно учитывать для кода, критичного к производительности. stackoverflow.com/questions/20002975/…
Чакрава

@Chakrava, конечно, мое решение не должно использоваться в коде, критичном к производительности, но по моему опыту вы сначала пишете рабочий код, а затем определяете, что критично к производительности, и редко когда мои операции linq to objects выполняются, скажем, на 50 объектах. Это следует оценивать в каждом конкретном случае.
Рафал

@Rafal Я согласен, я нашел множество .Skip()s в кодовой базе моей компании, и, хотя они не могут быть «оптимальными», они работают просто отлично. Такие вещи, как операции с БД, в любом случае занимают гораздо больше времени. Но я думаю, что важно отметить, что .Skip()«касается» каждого элемента <n на своем пути, вместо того, чтобы переходить непосредственно к n-му элементу (как вы могли ожидать). Если у вашего итератора есть побочные эффекты от прикосновения к элементу, это .Skip()может быть причиной труднодоступных ошибок.
Чакрава

11

Решение 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;
} 

10

Я считаю принятый ответ (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;
}

8

В библиотеке 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 кусков с 2 элементами.


Это должен быть принятый ответ. Или, по крайней мере, намного выше на этой странице.
Зар Шардан

7

У меня есть универсальный метод, который принимает любые типы, включая 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;
    }

Спасибо. Интересно, могли бы вы обновить комментарии с помощью определения параметра maxCount? Сеть безопасности?
Эндрю Дженс

2
будьте осторожны с несколькими перечислениями перечислимых. values.Count()вызовет полное перечисление и потом values.ToList()другое. Безопаснее сделать, values = values.ToList()это уже материализовано.
mhand

7

Хотя многие из приведенных выше ответов выполняют свою работу, все они ужасно терпят неудачу в бесконечной последовательности (или действительно длинной последовательности). Следующее является полностью онлайновой реализацией, которая гарантирует лучшее время и возможную сложность памяти. Мы только один раз повторяем перечисляемый источник и используем ленивый возврат для ленивых вычислений. Потребитель может выбрасывать список на каждой итерации, делая объем памяти равным 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>, поэтому мои комментарии относительно бесконечных перечислимых не применимы к ОП, но могут помочь другим, кто в конечном итоге здесь. Эти комментарии были в ответ на другие опубликованные решения, которые используют IEnumerable<T>в качестве входных данных для своих функций, но перечисляют источник, перечисляемый несколько раз.


Я думаю, что IEnumerable<IEnumerable<T>>версия лучше, так как в ней не так много Listконструкций.
NetMage

@NetMage - одна из проблем IEnumerable<IEnumerable<T>>заключается в том, что реализация, вероятно, будет полагаться на потребителя, полностью перечисляющего каждый внутренний перечислимый получаемый результат. Я уверен, что решение может быть сформулировано таким образом, чтобы избежать этой проблемы, но я думаю, что полученный код может довольно быстро усложниться. Кроме того, поскольку он ленив, мы генерируем только один список за раз, и распределение памяти происходит ровно один раз для каждого списка, так как мы заранее знаем размер.
mhand

Вы правы - моя реализация использует перечислитель нового типа (Перечислитель позиции), который отслеживает вашу текущую позицию, оборачивая стандартный перечислитель, и позволяет вам перейти на новую позицию.
NetMage

6

Дополнение после очень полезного комментария 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 для Skip ()

Если вы решили перечислить только первые 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 возвращает объект List, DbContext запоминает, что данные уже получены.

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


не будет ли это перечисляться дважды в партии? так мы действительно перечисляем время источника 2*chunkSize? Это смертельно опасно в зависимости от источника перечисляемого (возможно, БД или другого незарегистрированного источника). Представьте это перечисляемое в качестве входных данных Enumerable.Range(0, 10000).Select(i => DateTime.UtcNow)- вы будете получать разное время при каждом перечислении перечислимого, поскольку оно не запоминается
mhand

Рассмотрим: Enumerable.Range(0, 10).Select(i => DateTime.UtcNow). При вызове Anyвы будете пересчитывать текущее время каждый раз. Не так уж и плохо DateTime.UtcNow, но рассмотрим перечисляемый, поддерживаемый курсором соединения с базой данных / sql или подобным. Я видел случаи, когда были
сделаны

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

Как насчет этого? Идея заключалась в том, чтобы использовать только один цикл. И, кто знает, может быть, вы используете только реализации 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;
            }
        }
    }   
}

1

Еще один

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
Желательно объяснить свой ответ, а не только предоставить фрагмент кода
Кевин

0

Я столкнулся с этой же потребностью и использовал комбинацию методов Linq Skip () и Take () . Я умножаю число, которое я беру, на количество итераций, и это дает мне количество пропущенных элементов, затем я беру следующую группу.

        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

На основании Дмитрия Павлова я бы снял .ToList(). А также избегайте анонимного класса. Вместо этого мне нравится использовать структуру, которая не требует выделения памяти в куче. (А ValueTupleтакже сделал бы работу.)

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

Это можно использовать подобно следующему, который выполняет итерацию по коллекции только один раз, а также не выделяет значительную память.

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

Если конкретный список действительно необходим, я бы сделал это так:

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);
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.