有效地将列表划分为固定大小的块

时间:2013-12-12 23:56:31

标签: vb.net algorithm partitioning

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

我希望能够在可读性,优雅和性能之间取得平衡。问题是,我在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的内容以按需生成结果?

3 个答案:

答案 0 :(得分:2)

这可能是一种解决方法。使子例程成为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

答案 1 :(得分: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#函数。

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

答案 2 :(得分:0)

缺少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())

正如有人在评论中指出的那样,这不是最有效的方法,因为Skip 可能最终必须多次迭代项目。同样,如果source列表不是很大,那么这可能不是问题。并且Skip可能会针对实现IList的内容进行直接索引优化。