【问题标题】:Efficiently partition list into chunks of a fixed size有效地将列表划分为固定大小的块
【发布时间】:2013-12-19 01:17:02
【问题描述】:

我真的很喜欢下面显示的algorithm,用于将列表拆分为固定大小的子列表。它可能不是一种有效的算法(编辑:根本)

我想要在可读性、优雅和性能之间取得良好平衡的东西。问题是,我在 C# 中找到的大多数算法都需要 yield 关键字,如果您在 Visual Studio 2010 中使用 .NET 3.5,则该关键字不可用;)

public IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int size)
{
    if (source == null)
        throw new ArgumentNullException("list");

    if (size < 1)
        throw new ArgumentOutOfRangeException("size");

    int index = 1;
    IEnumerable<T> partition = source.Take(size).AsEnumerable();

    while (partition.Any())
    {
        yield return partition;
        partition = source.Skip(index++ * size).Take(size).AsEnumerable();
    }
}

我尝试在 VB 中重写它,但不得不使用第二个列表来收集结果,这最终比上面的实现花费了更多的时间。

我正在寻找可以在 VB.NET 中使用的另一种算法,但是大多数结果都遇到了必须将所有内容基本上加载到内存中而不是能够动态生成结果的问题a la python 中的生成器。这不是一个大问题,但不如yield return 那样理想。

在 VB.NET 中是否有一个好的推荐算法来执行此操作?我是否必须创建一些实现 IEnumerator 的东西才能按需生成结果?

【问题讨论】:

标签: vb.net algorithm partitioning


【解决方案1】:

这可能是一种解决方法。将子例程设为 Sub 并将目标列表传入。现在您可以直接将子列表添加到其中,而无需先创建整个中间对象。

Dim testlist As New List(Of List(Of Integer))
Partition(Of Integer)({1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 4, testlist)

Public Sub Partition(Of T)(source As IEnumerable(Of T), size As Integer, ByRef input As List(Of List(Of T)))
    If source Is Nothing Then
        Throw New ArgumentNullException("list")
    End If
    If size < 1 Then
        Throw New ArgumentOutOfRangeException("size")
    End If
    For i = 0 To source.Count - 1 Step size
        input.Add(source.Skip(i).Take(size).ToList())
    Next
End Sub

【讨论】:

    【解决方案2】:

    由于我在本地使用分区,我最终选择反转情况:我可以将操作传递给执行分区的函数,而不是将分区传递给使用它的操作。

    Public Sub DoForPartition(Of T)(source As IEnumerable(Of T), 
                                    size As Integer, 
                                    doThis As Action(Of IEnumerable(Of T)))
    
        Dim partition(size - 1) As T
        Dim count = 0
    
        For Each t in source
            partition(count) = t
            count += 1
    
            If count = size Then
                doThis(partition)
                count = 0
            End If
        Next
    
        If count > 0 Then
            Array.Resize(partition, count)
            doThis(partition)
        End If
    End Sub
    

    这个函数避免了多次循环遍历源,唯一的内存开销是分区的大小(而不是像其他一些选项那样整个源)。这个函数不是我自己写的,而是从this answer改编了一个类似的C#函数。

    这看起来比我的问题中的算法好得多。

    【讨论】:

      【解决方案3】:

      缺少Yield,您可以执行以下操作。我假设您可以转换为 VB

      public IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int size)
      {
          if (source == null)
              throw new ArgumentNullException("list");
      
          if (size < 1)
              throw new ArgumentOutOfRangeException("size");
      
          List<List<T>> partitions = new List<List<T>>();
      
          int index = 1;
          IEnumerable<T> partition = source.Take(size).AsEnumerable();
      
          while (partition.Any())
          {
              partitions.Add(partition);
              partition = source.Skip(index++ * size).Take(size).AsEnumerable();
          }
      
          return partitions;
      }
      

      请注意,这不是准确的翻译。原始代码一次返回一个分区。此代码创建所有分区,然后将它们全部返回到列表中。如果source 不是很大,这不是问题。

      也就是说,它与您的代码看起来相同。也就是说,如果你有:

      foreach (IEnumerable<Foo> part in Partition(FooList, 100))
      {
          // whatever
      }
      

      两个版本的功能基本相同。真正的区别是我上面的翻译会像你写的一样工作:

      foreach (IEnumerable<Foo> part in Partition(FooList, 100).ToList())
      

      正如有人在 cmets 中指出的那样,这不是最有效的处理方式,因为 Skip 可能最终不得不多次迭代项目。同样,如果source 列表不是很大,那么这可能不是问题。 Skip 很可能经过优化,可以对实现 IList 的事物进行直接索引。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-01-03
        • 2011-08-15
        • 1970-01-01
        • 1970-01-01
        • 2015-12-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多