【问题标题】:Grouping lists into groups of X items per group将列表分组为每组 X 项的组
【发布时间】:2014-05-28 20:26:58
【问题描述】:

我不知道如何将项目列表分组为(例如)不超过 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;
    }
    

【问题讨论】:

  • 看看MoreLINQBatch方法(NuGet提供的库)

标签: c# linq group-by grouping partitioning


【解决方案1】:

这是使用 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

【讨论】:

  • 感谢 Anthony,我也一直在试验这个解决方案,因为它保留了 IGrouping。我对它所做的唯一更改是在最后删除 .ToList 。这是必要的吗?诚然,如果没有这一行,我会收到错误“Results View = The type 'f__AnonymousType0' exists in both 'Microsoft.VisualStudio.TestPlatform.Extensions.VSTestIntegration.dll' and 'MyAssembly.Core .dll'" 在调试时显示每组枚举的结果。我试图在没有 ToList 的情况下解决这个问题,但没有成功。有什么想法吗?
【解决方案2】:

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

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

您需要将返回类型更改为IEnumerable&lt;IEnumerable&lt;TSource&gt;&gt;

【讨论】:

  • 除非有任何理由采取另一种解决方案,否则我会将此作为答案。我已经非常粗略地对这个和 Anthony 的 .NET fiddle 进行了基准测试,但这似乎要快一些。
  • 这实际上是一个很好的解决方案——虽然不像安东尼的那样直接。如果可能的话,你应该描述一下这个巫术是如何完成的。我对.GroupBy(x =&gt; x.idx / itemsPerGroup) 的行为特别感兴趣。它利用了计算将被四舍五入并导致多个项目具有相同“桶”的事实。非常高效。
【解决方案3】:

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

}

【讨论】:

    【解决方案4】:

    .net Fiddle

    本质上,您有一个 IEnumerable,并且您希望将其分组为 IGroupables 的 IEnumerable,每个 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);
    }
    

    【讨论】:

      【解决方案5】:

      这是基于 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),这可能需要也可能不需要。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-02-22
        • 1970-01-01
        • 1970-01-01
        • 2021-03-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多