C# - 分区列表的优雅方式?

时间:2009-09-08 20:06:17

标签: c# list data-partitioning

我想通过指定每个分区中的元素数量将列表分区为列表列表。

例如,假设我有列表{1,2,... 11},并且想要对其进行分区,使得每个集合具有4个元素,最后一个集合尽可能多地填充元素。生成的分区看起来像{{1..4},{5..8},{9..11}}

写这篇文章的优雅方式是什么?

11 个答案:

答案 0 :(得分:49)

这是一个可以执行您想要的扩展方法:

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

答案 1 :(得分:27)

使用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

答案 2 :(得分:11)

类似(未经测试的航空代码):

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

答案 3 :(得分:6)

避免分组,数学和重复。

该方法避免了不必要的计算,比较和分配。包括参数验证。

这是working demonstration on fiddle

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

答案 4 :(得分:1)

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

答案 5 :(得分:1)

或者在.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;
        }
    }

答案 6 :(得分:1)

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

答案 7 :(得分:1)

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

答案 8 :(得分:1)

我不确定为什么Jochems使用 ArraySegment 回答被拒绝。只要您不需要扩展段(转换为IList),它就非常有用。例如,假设您要做的是将段传递到TPL DataFlow管道以进行并发处理。将段作为IList实例传递允许相同的代码非法地处理数组和列表。

当然,这引出了一个问题:为什么不通过调用ToArray()来派生出一个不需要浪费内存的 ListSegment 类?答案是,在某些情况下,数组实际上可以稍微快速地处理(索引速度稍快)。但是你必须做一些相当硬核的处理来注意到很大的不同。更重要的是,没有好的方法来防止随机插入和删除操作的其他代码持有对列表的引用。

在一个百万价值数字列表上调用ToArray()在我的工作站上大约需要3毫秒。当您使用它来获得并发操作中更强大的线程安全性的好处时,通常不会付出太高的代价,而不会产生沉重的锁定成本。

答案 9 :(得分:0)

您可以使用扩展方法:

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

}

答案 10 :(得分:0)

为了避免多次检查,不必要的实例化和重复迭代,您可以使用代码:

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