Подтвердить что ты не робот

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

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

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

Каким будет элегантный способ написать это?

4b9b3361

Ответ 1

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

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

Ответ 2

Используя 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

Ответ 3

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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

Ответ 6

Или в .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;
        }
    }

Ответ 7

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

Ответ 8

Использование 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);
}

Ответ 9

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

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

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

Ответ 10

Вы можете использовать метод расширения:


public static IList<HashSet<T>> Partition<T>(this IEnumerable<T> input, Func<T, object> partitionFunc)
{
      Dictionary<object, HashSet> partitions = new Dictionary<object, HashSet<T>>();
  object currentKey = null;
  foreach (T item in input ?? Enumerable.Empty<T>())
  {
      currentKey = partitionFunc(item);

      if (!partitions.ContainsKey(currentKey))
      {
          partitions[currentKey] = new HashSet<T>();
      }

      partitions[currentKey].Add(item);
  }

  return partitions.Values.ToList();

} код >

Ответ 11

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

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