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

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

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

写这个的优雅方式是什么?

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

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

编辑:这是一个更清洁的function版本:

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

使用LINQ,您可以在一行代码中剪切您的组,就像这样……

 var x = new List() { 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 

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

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

这将返回列表的枚举而不是列表列表,但您可以轻松地将结果包装在列表中:

 IList> listOfLists = new List(PartitionList(list, maxCount)); 

避免分组,数学和重复。

 IEnumerable> Partition(this IEnumerable source, int size) { var partition = new T[size]; var count = 0; foreach(var t in source) { partition[count] = t; count ++; if (count == size) { yield return partition; var partition = new T[size]; var count = 0; } } if (count > 0) { Array.Resize(ref partition, count); yield return partition; } } 
 var yourList = new List { 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 => ii / groupSize, x => xx); // 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>而不是IEnumerable>则更改查询,如下所示:

 var query = yourList .Select((x, i) => new { x, i }) .GroupBy(i => ii / groupSize, x => xx) .Select(g => g.ToList()) .ToList(); 

或者在.Net 2.0中你会这样做:

  static void Main(string[] args) { int[] values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; List items = new List(SplitArray(values, 4)); } static IEnumerable SplitArray(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; } } 
 public static IEnumerable> Partition(this IEnumerable list, int size) { while (list.Any()) { yield return list.Take(size); list = list.Skip(size); } } 

以及String的特例

 public static IEnumerable Partition(this string str, int size) { return str.Partition(size).Select(AsString); } public static string AsString(this IEnumerable charList) { return new string(charList.ToArray()); } 

您可以使用扩展方法:

 public static IList > Partition (此IEnumerable 输入,Func  partitionFunc)
 {
       Dictionary  partitions = new Dictionary >(); 

  object currentKey = null; foreach (T item in input ?? Enumerable.Empty()) { currentKey = partitionFunc(item); if (!partitions.ContainsKey(currentKey)) { partitions[currentKey] = new HashSet(); } partitions[currentKey].Add(item); } return partitions.Values.ToList(); 

}

使用ArraySegments可能是一个可读且简短的解决方案(需要将列表转换为数组):

 var list = new List() { 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 listSegments = new List(); for(int i = 0; i < array.Length; i+=step) { int[] segment = new ArraySegment(array, i, step).ToArray(); listSegments.Add(segment); } 

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

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

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

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

 namespace System.Collections.Generic { using Linq; using Runtime.CompilerServices; public static class EnumerableExtender { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsEmpty(this IEnumerable enumerable) => !enumerable?.GetEnumerator()?.MoveNext() ?? true; public static IEnumerable> Partition(this IEnumerable source, int size) { if (source == null) throw new ArgumentNullException(nameof(source)); if (size < 2) throw new ArgumentOutOfRangeException(nameof(size)); IEnumerable items = source; IEnumerable partition; while (true) { partition = items.Take(size); if (partition.IsEmpty()) yield break; else yield return partition; items = items.Skip(size); } } } }