将列表分成每组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;
    }
    

将列表分成每组X个项目的组

这里有一种方法可以使用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

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

这是基于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),这可能是也可能不是期望的。