将列表分组为每组X个项目组

时间:2014-05-28 20:26:58

标签: c# linq group-by grouping partitioning

我知道制作方法将项目列表分组(例如)不超过3项的最佳方法时遇到问题。我已经创建了下面的方法,但是在返回之前没有在组上执行ToList,如果列表被多次枚举,我就会遇到问题。

第一次枚举是正确的,但是任何额外的枚举都会被抛弃,因为两个变量(i和groupKey)似乎在迭代之间被记住了。

所以问题是:

  • 有没有更好的方法去做我想要实现的目标?
  • 在离开此方法之前,只需将结果组ToListing 真是个坏主意?

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

5 个答案:

答案 0 :(得分:10)

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

Live Demo

答案 1 :(得分:4)

我认为你正在寻找这样的东西:

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

您需要将退货类型更改为IEnumerable<IEnumerable<TSource>>

答案 2 :(得分:3)

使用GroupBy()的问题在于,除非它以某种方式知道输入按键值排序,否则它必须读取整个序列并将所有内容分配到其存储桶才能发出单个值组。在这种情况下,这是过度的,因为密钥是序列中序数位置的函数。

我喜欢source.Skip(m).Take(n)方法,但这可以假设source中的项目可以直接解决。如果那不是真的,或Skip()Take()不知道底层实现,那么每个组的生成将平均为O(n / 2)操作,因为它反复迭代超过source来制作小组。

这使得整个分区操作可能非常昂贵。

  • 如果产生一组是平均O(n / 2)操作,
  • 如果组的大小为 s ,则需要生成大约 n / s 组,

那么操作的总成本就像O(n 2 / 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 :(得分:2)

<强> .net Fiddle

基本上你有一个IEnumerable,你想把它组成一个IEnumerable的IGroupables,每个组合包含一个键作为索引,组作为值。你的版本似乎确实在第一次通过时完成,但我认为你肯定可以流一点。

在我看来,使用skip and 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的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);
}

答案 4 :(得分:0)

这是基于Selman Select的索引构思,但使用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://stackoverflow.com/a/11969517/7270462),这可能是也可能不是。