С# - элегантный способ разделения списка?

Я хотел бы разделить список на список списков, указав количество элементов в каждом разделе.

Например, предположим, что у меня есть список {1, 2,... 11}, и я хотел бы разделить его таким образом, чтобы каждый набор имел 4 элемента, а последний набор заполнял как можно больше элементов. Результирующий раздел будет выглядеть как {{1..4}, {5..8}, {9..11}}

Что было бы элегантным способом написать это?


person David Hodgson    schedule 08.09.2009    source источник
comment
Я уверен, что кто-то опубликует хорошее заявление linqy.   -  person Preet Sangha    schedule 09.09.2009
comment
@Preet - я отправил ответ linq по вашему запросу;)   -  person Scott Ivey    schedule 09.09.2009


Ответы (10)


Вот метод расширения, который будет делать то, что вы хотите:

public static IEnumerable<List<T>> Partition<T>(this IList<T> source, Int32 size)
{
    for (int i = 0; i < (source.Count / size) + (source.Count % size > 0 ? 1 : 0); i++)
        yield return new List<T>(source.Skip(size * i).Take(size));
}

Редактировать. Вот более чистая версия функции:

public static IEnumerable<List<T>> Partition<T>(this IList<T> source, Int32 size)
{
    for (int i = 0; i < Math.Ceiling(source.Count / (Double)size); i++)
        yield return new List<T>(source.Skip(size * i).Take(size));
}
person Andrew Hare    schedule 08.09.2009
comment
for (int i = 0; i ‹ source.Count; i += размер) { /* ... */ } - person Roger Lipscombe; 30.12.2009
comment
Неблагоприятный эффект этого метода заключается в том, что данный массив недоступен по индексу. Здесь есть метод, который вместо vcskicks.com/partition-list.php возвращает список. - person George; 19.10.2010
comment
Имейте в виду, что в фактической реализации LINQ Skip и Take просто зацикливаются на заданной последовательности, нет проверки/оптимизации в случае, если источник реализует IList и, следовательно, может быть доступен по индексу. Из-за этого они O(m) (где m — это количество элементов, которые вы хотите пропустить или взять), и это расширение Partition() может не дать ожидаемой производительности. - person tigrou; 26.06.2015
comment
@George: (по крайней мере сейчас) вы можете вызвать .ToList() для перечислимого, чтобы получить индексируемый список. - person mklement0; 14.11.2018

Используя LINQ, вы можете сократить свои группы в одной строке кода, как это...

var x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };

var groups = x.Select((i, index) => new
{
    i,
    index
}).GroupBy(group => group.index / 4, element => element.i);

Затем вы можете перебирать группы, как показано ниже...

foreach (var group in groups)
{
    Console.WriteLine("Group: {0}", group.Key);

    foreach (var item in group)
    {
        Console.WriteLine("\tValue: {0}", item);
    }
}

и вы получите вывод, который выглядит так...

Group: 0
        Value: 1
        Value: 2
        Value: 3
        Value: 4
Group: 1
        Value: 5
        Value: 6
        Value: 7
        Value: 8
Group: 2
        Value: 9
        Value: 10
        Value: 11
person Scott Ivey    schedule 08.09.2009
comment
Не совсем соответствует требованиям вопроса, но +1 за то, что подумал об этом немного по-другому. - person RichardOD; 09.09.2009
comment
RichardOD — вы правы — я обновил пример, чтобы на выходе была группа целых чисел, а не группа анонимных типов. - person Scott Ivey; 09.09.2009
comment
Я думаю, ты просто взорвал мой мозг. Мне очень любопытно узнать, где вы выучили такой синтаксис (мне он очень нравится). Все документы LINQ, которые я видел, хороши, но они не очень хорошо охватывают группировку. - person Dan Esparza; 10.09.2009
comment
Много возни + чтение SO вопросов. LINQ определенно является одной из моих любимых новых функций в версии 3.5, и я довольно много узнал о ней, просто зависая здесь. Эта перегрузка для GroupBy была чем-то, что я раньше не использовал, так что это тоже было для меня новым :) - person Scott Ivey; 10.09.2009
comment
@ScottIvey очень хорошая логика группировки и идеально подходит для некоторой логики, которая мне нужна для разделения исходящих команд UDP на несколько пакетов на основе внутреннего List‹›.Count(). хороший! Спасибо, что поделился. - person ; 22.11.2013

Что-то вроде (непроверенный код воздуха):

IEnumerable<IList<T>> PartitionList<T>(IList<T> list, int maxCount)
{
    List<T> partialList = new List<T>(maxCount);
    foreach(T item in list)
    {
        if (partialList.Count == maxCount)
        {
           yield return partialList;
           partialList = new List<T>(maxCount);
        }
        partialList.Add(item);
    }
    if (partialList.Count > 0) yield return partialList;
}

Это возвращает перечисление списков, а не список списков, но вы можете легко обернуть результат в список:

IList<IList<T>> listOfLists = new List<T>(PartitionList<T>(list, maxCount));
person Joe    schedule 08.09.2009
comment
Мне нравится это решение, но оно может вызвать проблемы, если в maxCount передается большое число (например: PartitionList(list, enablePartition ? 500 : int.MaxValue) Возможное улучшение состоит в том, чтобы установить емкость списка только в том случае, если исходный код реализует ICollection и зафиксирует maxCount в количестве элементов внутри коллекции. - person tigrou; 18.04.2018
comment
@tigrou - я не уверен, что защитил бы вызывающего абонента от последствий передачи чрезмерно большого числа, но чтобы иметь возможность обрабатывать произвольно большие разделы, вы, вероятно, использовали бы перечисления, а не списки - например. метод IEnumerable<IEnumerable<T>> PartitionEnumeration<T> (IEnumerable<T> enumeration, int maxCount), который можно легко реализовать без выделения списка. - person Joe; 18.04.2018
comment
Если вы возвращаете IEnumerable<IEnumerable<T>> и полагаетесь на реализацию, которая никогда ничего не выделяет (например, она дает только элементы из источника), у вас будут проблемы, если результат не перечисляется последовательно (например: раздел 4 перечисляется перед разделом 2 или некоторые разделы только частично перечислено). Я думаю, что списки безопаснее. - person tigrou; 18.04.2018

Чтобы избежать группировки, математики и повторения.

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

Вот рабочая демонстрация скрипки.

public static IEnumerable<IList<T>> Partition<T>(
    this IEnumerable<T> source,
    int size)
{
    if (size < 2)
    {
        throw new ArgumentOutOfRangeException(
            nameof(size),
            size,
            "Must be greater or equal to 2.");  
    }

    T[] partition;
    int count;

    using (var e = source.GetEnumerator())
    {
        if (e.MoveNext())
        {
            partition = new T[size];
            partition[0] = e.Current;
            count = 1;
        }
        else
        {
            yield break;    
        }

        while(e.MoveNext())
        {
            partition[count] = e.Current;
            count++;

            if (count == size)
            {
                yield return partition;
                count = 0;
                partition = new T[size];
            }
        }
    }

    if (count > 0)
    {
        Array.Resize(ref partition, count);
        yield return partition;
    }
}
person Jodrell    schedule 13.08.2013
comment
Ваше самое изящное и наименее ресурсоемкое из всех возможных решений, не знаю почему у него меньше плюсов - person Paleta; 07.01.2019
comment
Мне это нравится, зачем ArgumentOutOfRangeException вместо 1? вы можете изменить это на size < 1, затем добавить if (size == 1) yield return partition; else count = 1; в блок if (e.MoveNext() после назначения на partition[0]. - person Brett Caswell; 09.02.2020
comment
Я думал об этом, но если вы хотите, чтобы разделы были меньше 2, вызывать функцию довольно расточительно: просто перечислите список, но я согласен, что это делает функцию хрупкой или информативной. - person Jodrell; 11.02.2020
comment
спасибо, что поделились своими мыслями, я предположил, что вы обдумали это и пришли к выводу, что для этого сценария существует лучшая реализация, и что определение будет зависеть от вызывающей функции (сферы ответственности). - person Brett Caswell; 11.02.2020

var yourList = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
var groupSize = 4;

// here's the actual query that does the grouping...
var query = yourList
    .Select((x, i) => new { x, i })
    .GroupBy(i => i.i / groupSize, x => x.x);

// and here's a quick test to ensure that it worked properly...
foreach (var group in query)
{
    foreach (var item in group)
    {
        Console.Write(item + ",");
    }
    Console.WriteLine();
}

Если вам нужно фактическое List<List<T>>, а не IEnumerable<IEnumerable<T>>, измените запрос следующим образом:

var query = yourList
    .Select((x, i) => new { x, i })
    .GroupBy(i => i.i / groupSize, x => x.x)
    .Select(g => g.ToList())
    .ToList();
person LukeH    schedule 08.09.2009

Или в .Net 2.0 вы бы сделали это:

    static void Main(string[] args)
    {
        int[] values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
        List<int[]> items = new List<int[]>(SplitArray(values, 4));
    }

    static IEnumerable<T[]> SplitArray<T>(T[] items, int size)
    {
        for (int index = 0; index < items.Length; index += size)
        {
            int remains = Math.Min(size, items.Length-index);
            T[] segment = new T[remains];
            Array.Copy(items, index, segment, 0, remains);
            yield return segment;
        }
    }
person csharptest.net    schedule 08.09.2009

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> list, int size)
{
    while (list.Any()) { yield return list.Take(size); list = list.Skip(size); }
}

и для особого случая String

public static IEnumerable<string> Partition(this string str, int size)
{
    return str.Partition<char>(size).Select(AsString);
}

public static string AsString(this IEnumerable<char> charList)
{
    return new string(charList.ToArray());
}
person Scroog1    schedule 07.03.2012

Использование ArraySegments может быть читаемым и коротким решением (требуется приведение списка к массиву):

var list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; //Added 0 in front on purpose in order to enhance simplicity.
int[] array = list.ToArray();
int step = 4;
List<int[]> listSegments = new List<int[]>();

for(int i = 0; i < array.Length; i+=step)
{
     int[] segment = new ArraySegment<int>(array, i, step).ToArray();
     listSegments.Add(segment);
}
person Jochem Geussens    schedule 23.09.2013

Я не уверен, почему ответ Jochems с использованием ArraySegment был отклонен. Это может быть очень полезно, если вам не нужно расширять сегменты (приводить к IList). Например, представьте, что вы пытаетесь передать сегменты в конвейер TPL DataFlow для параллельной обработки. Передача сегментов в качестве экземпляров IList позволяет одному и тому же коду независимо работать с массивами и списками.

Конечно, возникает вопрос: почему бы просто не создать класс ListSegment, который не требует траты памяти на вызов ToArray()? Ответ заключается в том, что в некоторых ситуациях массивы на самом деле могут обрабатываться немного быстрее (чуть более быстрая индексация). Но вам нужно было бы сделать довольно жесткую обработку, чтобы заметить большую разницу. Что еще более важно, нет хорошего способа защититься от случайных операций вставки и удаления другим кодом, содержащим ссылку на список.

Вызов ToArray() для числового списка с миллионом значений занимает на моей рабочей станции около 3 миллисекунд. Обычно это не слишком большая цена, когда вы используете его, чтобы получить преимущества более надежной безопасности потоков в параллельных операциях, не неся больших затрат на блокировку.

person Ben Stabile    schedule 08.05.2015

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

namespace System.Collections.Generic
{
    using Linq;
    using Runtime.CompilerServices;

    public static class EnumerableExtender
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool IsEmpty<T>(this IEnumerable<T> enumerable) => !enumerable?.GetEnumerator()?.MoveNext() ?? true;

        public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (size < 2)
                throw new ArgumentOutOfRangeException(nameof(size));
            IEnumerable<T> items = source;
            IEnumerable<T> partition;
            while (true)
            {
                partition = items.Take(size);
                if (partition.IsEmpty())
                    yield break;
                else
                    yield return partition;
                items = items.Skip(size);
            }
        }
    }
}
person Michalis Sarigiannidis    schedule 27.12.2016