将列表分成每组X个项目的组
本文关键字:项目 列表 | 更新日期: 2023-09-27 18:05:33
我有一个问题,知道最好的方法来组的项目列表到组(例如)不超过3个项目。我已经创建了下面的方法,但是在我返回它之前没有对组做ToList
,如果列表被枚举多次,我就会遇到问题。
第一次枚举是正确的,但是任何额外的枚举都会被丢弃,因为两个变量(i和groupKey)似乎在迭代之间被记住了。
问题是:
- 有更好的方法来做我想要达到的目标吗?
-
只是在它离开这个方法之前列出结果组真的是个坏主意吗?
public static IEnumerable<IGrouping<int, TSource>> GroupBy<TSource> (this IEnumerable<TSource> source, int itemsPerGroup) { const int initial = 1; int i = initial; int groupKey = 0; var groups = source.GroupBy(x => { if (i == initial) { groupKey = 0; } if (i > initial) { //Increase the group key if we've counted past the items per group if (itemsPerGroup == initial || i % itemsPerGroup == 1) { groupKey++; } } i++; return groupKey; }); return groups; }
这里有一种方法可以使用LINQ…
public static IEnumerable<IGrouping<int, TSource>> GroupBy<TSource>
(this IEnumerable<TSource> source, int itemsPerGroup)
{
return source.Zip(Enumerable.Range(0, source.Count()),
(s, r) => new { Group = r / itemsPerGroup, Item = s })
.GroupBy(i => i.Group, g => g.Item)
.ToList();
}
现场演示
我想你在找这样的东西:
return source.Select((x, idx) => new { x, idx })
.GroupBy(x => x.idx / itemsPerGroup)
.Select(g => g.Select(a => a.x));
您需要将返回类型更改为IEnumerable<IEnumerable<TSource>>
使用GroupBy()
的问题是,除非它以某种方式知道输入是按键值排序的,否则它必须读取整个序列并将所有内容分配到其桶中,然后才能发出单个组。在这种情况下,这是多余的,因为键是其在序列中的序数位置的函数。
我喜欢source.Skip(m).Take(n)
方法,但这假设source
中的项目可以直接解决。如果这不是真的,或者Skip()
和Take()
不知道底层实现,那么每个组的生成平均将是一个O(n/2)的操作,因为它在source
上重复迭代以生成组。
这使得整个分区操作,可能相当昂贵。
- IF生成一个组平均为O(n/2)操作,
- 给定s组的大小,则需要生产约n/s组,
那么操作的总成本大概是O(n2/2s)对吧?
所以,我会这样做,一个O(n)操作(如果你愿意,可以随意使用IGrouping
实现):
public static IEnumerable<KeyValuePair<int,T[]>> Partition<T>( this IEnumerable<T> source , int partitionSize )
{
if ( source == null ) throw new ArgumentNullException("source") ;
if ( partitionSize < 1 ) throw new ArgumentOutOfRangeException("partitionSize") ;
int i = 0 ;
List<T> partition = new List<T>( partitionSize ) ;
foreach( T item in source )
{
partition.Add(item) ;
if ( partition.Count == partitionSize )
{
yield return new KeyValuePair<int,T[]>( ++i , partition.ToArray() ) ;
partition.Clear() ;
}
}
// return the last partition if necessary
if ( partition.Count > 0 )
{
yield return new Partition<int,T>( ++i , items.ToArray() ) ;
}
}
.net Fiddle
本质上你有一个IEnumerable,你想把它分组成一个IEnumerable的IGroupables,每个IGroupables包含一个键作为索引,组作为值。你的版本似乎第一次通过就完成了,但我认为你绝对可以稍微流一点。
在我看来,使用skip和take是最理想的完成方式,但是分组的自定义键是存在问题的地方。有一种方法可以解决这个问题,那就是创建自己的类作为分组模板(见此答案:https://stackoverflow.com/a/5073144/1026459)。最终结果如下:
public static class GroupExtension
{
public static IEnumerable<IGrouping<int, T>> GroupAt<T>(this IEnumerable<T> source, int itemsPerGroup)
{
for(int i = 0; i < (int)Math.Ceiling( (double)source.Count() / itemsPerGroup ); i++)
{
var currentGroup = new Grouping<int,T>{ Key = i };
currentGroup.AddRange(source.Skip(itemsPerGroup*i).Take(itemsPerGroup));
yield return currentGroup;
}
}
private class Grouping<TKey, TElement> : List<TElement>, IGrouping<TKey, TElement>
{
public TKey Key { get; set; }
}
}
这里是小提琴中的演示它在一个简单的字符串
上消耗它public class Program
{
public void Main(){
foreach(var p in getLine().Select(s => s).GroupAt(3))
Console.WriteLine(p.Aggregate("",(s,val) => s += val));
}
public string getLine(){ return "Hello World, how are you doing, this just some text to show how the grouping works"; }
}
编辑
也可以作为IEnumerable
的IEnumerablepublic static IEnumerable<IEnumerable<T>> GroupAt<T>(this IEnumerable<T> source, int itemsPerGroup)
{
for(int i = 0; i < (int)Math.Ceiling( (double)source.Count() / itemsPerGroup ); i++)
yield return source.Skip(itemsPerGroup*i).Take(itemsPerGroup);
}
这是基于Selman的Select
与索引的想法,但使用ToLookup
将GroupBy
和Select
结合在一起:
public static IEnumerable<IEnumerable<TSource>> GroupBy<TSource>
(this IEnumerable<TSource> source, int itemsPerGroup)
{
return source.Select((x, idx) => new { x, idx })
.ToLookup(q => q.idx / itemsPerGroup, q => q.x);
}
主要的区别是ToLookup
实际上是立即计算结果的(正如这里简要解释的那样:https://stackoverflow.com/a/11969517/7270462),这可能是也可能不是期望的。