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


209

Я намагаюся розділити список на ряд менших списків.

Моя проблема: Моя функція розділити списки не розділяє їх на списки правильного розміру. Він повинен розділити їх на списки розміром 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)); 
    }  
} 

Отже, якщо у мене є довжина списку на мільйон, і я хочу розділити на менші списки Довжина 30, і з кожного меншого списку я хочу лише взяти (1), то я все одно створюю списки з 30 предметів, з яких я викидаю 29 елементів. Це можна зробити розумнішим!
Харальд Коппулз

Це насправді працює? Хіба це не вдалося б при першому розділі, оскільки ви отримуєте діапазон nSize до nSize? Наприклад, якщо nSize дорівнює 3, а мій масив розміром 5, то повертається перший діапазон індексівGetRange(3, 3)
Matthew Pigram,

2
@MatthewPigram перевірений і він працює. Math.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 є більш простим і має кращу ремонтопридатність.
Мовчазний прикордонник

2
Зауважте, що, можливо, найкраще залишити цю ToList()проблему, і нехай ледачі оцінки роблять це магією.
Yair Halberstadt

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

37

як щодо:

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

Це збирається споживати багато пам’яті? Кожен раз, коли трапляється location.Skip.ToList, мені цікаво, чи виділено більше пам’яті та нерозкриті елементи посилаються на новий список.
Заш

2
так, новий список створюється для кожного циклу. Так, вона споживає пам'ять. Але якщо у вас виникають проблеми з пам'яттю, це не місце для оптимізації, оскільки екземпляри списків готові збиратись у наступному циклі. Ви можете торгувати продуктивністю пам’яті, пропускаючи, ToListале я б не намагався її оптимізувати - це настільки банально і малоймовірно, це вузьке місце. Основна вигода від цієї реалізації - це її тривіальність, яку легко зрозуміти. Якщо ви хочете, можете скористатися прийнятою відповіддю, це не створює цих списків, але є трохи складнішим.
Рафал

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

@Chakrava впевнена, що моє рішення не використовуватиметься у критичному виконанні коду, але, на моєму досвіді, ви спочатку пишете робочий код, а потім визначаєте, що є критичним для продуктивності, і це рідко, коли мій посилання на операції з об'єктами, що виконуються на 50 об'єктах. Це слід оцінювати у кожному конкретному випадку.
Рафал

@ Рафал Я згоден, я знайшов численні .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;
        }
    }
}

EDIT: Щойно зрозумівши, що ОП запитує про розбиття List<T>на менші List<T>, тому мої коментарі щодо нескінченних перелічених даних не застосовуються до ОП, але можуть допомогти іншим, хто опинився тут. Ці коментарі були у відповідь на інші розміщені рішення, які використовують IEnumerable<T>як вхід до їх функції, але перераховують джерело безліч разів.


Я думаю, що IEnumerable<IEnumerable<T>>версія є кращою, оскільки вона не передбачає стільки Listпобудови.
NetMage

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

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

6

Доповнення після дуже корисного коментаря mhand в кінці

Оригінальна відповідь

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

Наступне буде перераховувати якнайкраще двічі: один раз для Take і один раз для Skip. Він не буде перераховувати більше елементів, ніж ви будете використовувати:

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 шматок. З кожного переліченого фрагменту ви будете перераховувати лише перші M елементи.

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

будь-який отримає перелік, зробить 1 MoveNext () і поверне повернене значення після розпорядження перелік. Це буде зроблено 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;
    }
}

Це не робить багато, поки ви не почнете перераховувати над отриманим фрагментом. Якщо ви отримаєте кілька фрагментів, але вирішите не перелічувати перший фрагмент, передбачення не виконується, як вам покаже ваш налагоджувач.

Якщо ви вирішили взяти перші елементи 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.

Отже, за Чанком ми бачимо, що робиться наступне:

  • Будь-яке (): GetEnumerator; 1 MoveNext (); Розпоряджається перерахувачем;
  • Приймати():

    • нічого, якщо зміст шматка не перелічено.
    • Якщо вміст перераховано: GetEnumerator (), один MoveNext та один Поточний на перерахований елемент, розпорядження перерахуйте;

    • Пропустити (): для кожного перерахованого фрагмента (НЕ вміст шматка): GetEnumerator (), MoveNext () фрагментРазмер часу, без поточного! Утилізуйте перелік

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

Якщо ви приймаєте N Chunks розміру chunkSize, тоді дзвінки на MoveNext ()

  • N разів для будь-якого ()
  • ще немає часу для Take, доки ви не перерахуєте шматки
  • N разів фрагментРозмір для 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 повертає об'єкт, що представляє собою Список, DbContext пам'ятає, що дані вже отримані.

Від вашого сховища залежить, чи розумно викликати AsEnumerable () або ToLists () перед тим, як почати ділити елементи в Chunks


не буде це перераховувати двічі за партію? значить, ми справді перераховуємо початкові 2*chunkSizeчаси? Це смертельно, залежно від джерела перелічених даних (можливо, підтримуваних БД або іншого джерела, що не запам'ятовується). Уявіть, що це число, як вхід Enumerable.Range(0, 10000).Select(i => DateTime.UtcNow)- ви будете отримувати різні рази щоразу, коли перераховувати це число, оскільки воно не запам'ятовується
mhand

Розглянемо: Enumerable.Range(0, 10).Select(i => DateTime.UtcNow). Після виклику Anyви кожен раз будете перераховувати поточний час. Не так вже й погано DateTime.UtcNow, але розглянемо безліч підкріплених підключенням до бази даних / sql-курсором чи подібним. Я бачив випадки , коли були випущені тисячі БД дзвінків , тому що розробник не зрозуміти потенційні наслідки «множинних перерахування перечислимого» - ReSharper дає натяк на це , а також
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

Як щодо цього? Ідея полягала в тому, щоб використовувати лише одну петлю. І, хто знає, можливо, ви використовуєте лише реалізацію IList, ретельно вводячи свій код, і ви не хочете переходити до списку.

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

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

        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.