将列表分组,每组包含X个项目

8
我有一个问题,不知道如何最好地编写一种方法,以便将一系列项目分组为(例如)不超过3个项目的组。我创建了下面的方法,但在返回之前没有对组执行ToList,如果多次枚举列表,则会出现问题。
第一次枚举时是正确的,但任何其他枚举都会出错,因为两个变量(i和groupKey)似乎在迭代之间被记住。
所以问题是:
  • Is there a better way to do what I'm trying to achieve?
  • Is simply ToListing the resulting group before it leaves this method really such a bad idea?

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

请查看NuGet提供的MoreLINQ库中的Batch方法。 - Sergey Berezovskiy
5个回答

17

以下是使用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();
}

在线演示


谢谢Anthony,我也一直在尝试这个解决方案,因为它保留了IGrouping。我对它做的唯一更改是删除了末尾的.ToList。这是必要的吗?不可否认,没有这行代码,当调试时显示每个组的枚举结果时,我会得到错误“**Results View = The type '<>f__AnonymousType0<Source,Tests>' exists in both 'Microsoft.VisualStudio.TestPlatform.Extensions.VSTestIntegration.dll' and 'MyAssembly.Core.dll'**”。我已经尝试过在没有ToList的情况下解决这个问题,但没有成功。有什么想法吗? - ajbrun

10

我想你正在寻找类似于这样的内容:

return source.Select((x, idx) => new { x, idx })
      .GroupBy(x => x.idx / itemsPerGroup)
      .Select(g => g.Select(a => a.x));

你需要将返回类型更改为 IEnumerable<IEnumerable<TSource>>


除非有其他更好的解决方案,否则我会接受这个答案。我已经在.NET fiddle上对这个和安东尼的代码进行了粗略的基准测试,但这个似乎稍微快一些。 - ajbrun
这实际上是一个不错的解决方案 - 虽然不像Anthony的那么直接。如果可能的话,您应该描述一下这个魔法是如何完成的...我很感兴趣,特别是关于.GroupBy(x => x.idx / itemsPerGroup)的行为。它利用了计算将被舍入并导致多个项目具有相同“桶”的事实。非常高效。 - Bagus Tesa

4
使用GroupBy()的问题在于,除非它在幕后拥有输入按键值排序的知识,否则必须读取整个序列并将所有内容分配到其桶中,然后才能发出单个组。在这种情况下过度了,因为关键字是序列内序数位置的函数。
我喜欢source.Skip(m).Take(n)的方法,但这意味着假设可以直接寻址source中的项。如果不是这样或Skip()Take()没有了解底层实现,那么每个组的生成将成为平均O(n/2)操作,因为它反复迭代source以产生该组。
这使得整个分区操作可能会非常昂贵。
  • 如果生成一个组是平均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() ) ;
  }

}

3

.net Fiddle

基本上,您有一个IEnumerable,并且想将其分组为包含键作为索引和值作为组的IGroupable的IEnumerable。您的版本似乎在第一次通过时实现了这一目标,但我认为您可以做得更加流畅。

在我看来,使用skip和take是最理想的方法,但自定义分组的关键是有问题的。有一种解决方法是创建自己的类作为分组模板(在此答案中可见:https://dev59.com/l2445IYBdhLWcg3wBl2J#5073144)。

最终结果如下:

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

这里是在fiddle中使用一个简单字符串的演示

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的IEnumerable。

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

0

这是基于Selman的索引思想,但使用ToLookupGroupBySelect结合在一起:

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://dev59.com/Dmkv5IYBdhLWcg3w_Vqn#11969517),可能是需要的,也可能不是。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接