将列表拆分为N个较小的N列表

saz*_*azr 179 c# split list

我试图将列表拆分成一系列较小的列表.

我的问题:我拆分列表的功能不会将它们拆分成正确大小的列表.它应该将它们分成大小为30的列表,而是将它们分成大小为114的列表?

如何使我的功能将列表拆分为X个大小为30或更小的列表?

public static List<List<float[]>> splitList(List <float[]> locations, int nSize=30) 
{       
    List<List<float[]>> list = new List<List<float[]>>();

    for (int i=(int)(Math.Ceiling((decimal)(locations.Count/nSize))); i>=0; i--) {
        List <float[]> subLocat = new List <float[]>(locations); 

        if (subLocat.Count >= ((i*nSize)+nSize))
            subLocat.RemoveRange(i*nSize, nSize);
        else subLocat.RemoveRange(i*nSize, subLocat.Count-(i*nSize));

        Debug.Log ("Index: "+i.ToString()+", Size: "+subLocat.Count.ToString());
        list.Add (subLocat);
    }

    return list;
}
Run Code Online (Sandbox Code Playgroud)

如果我在144的列表上使用该函数,那么输出是:

指数:4,大小:120
指数:3,大小:114
索引:2,大小:114
索引:1,大小:114
索引:0,大小:114

Dmi*_*lov 351

我建议使用此扩展方法将源列表按指定的块大小分块到子列表:

/// <summary>
/// Helper methods for the lists.
/// </summary>
public static class ListExtensions
{
    public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize) 
    {
        return source
            .Select((x, i) => new { Index = i, Value = x })
            .GroupBy(x => x.Index / chunkSize)
            .Select(x => x.Select(v => v.Value).ToList())
            .ToList();
    }
}
Run Code Online (Sandbox Code Playgroud)

例如,如果您按照每个块的5个项目清除18个项目的列表,它会为您提供4个子列表的列表,其中包含以下项目:5-5-5-3.

  • 在生产中使用它之前,请确保了解内存和性能的运行时影响.仅仅因为LINQ可以简洁,并不意味着它是一个好主意. (19认同)
  • 我不认为内存和性能在这里应该是一个大问题。我碰巧需要将一个包含超过 200,000 条记录的列表拆分为每个大约 3000 条记录的较小列表,这让我来到了这个线程,我测试了两种方法,发现运行时间几乎相同。之后,我测试了将该列表拆分为每个包含 3 条记录的列表,但性能仍然正常。我确实认为 Serj-Tm 的解决方案更简单,但具有更好的可维护性。 (11认同)
  • 当然,@尼克我会建议在做任何事之前先考虑一下.使用LINQ进行分块不应该是经常重复数千次的操作.通常,您需要批处理列表以批处理和/或并行处理项目. (3认同)
  • @IarekKovtunenko 好吧,有了无数的记录,您绝对应该根据您的特定需求调整算法。我会用缓冲区实现类似流处理逻辑的东西,它分两步对记录进行分块:1)获取第一部分 - 任何合理数量的记录(例如 10K)和 2)每个部分内的块。不要用显微镜敲钉子——使用正确的工具来完成这项任务;) (3认同)
  • 请注意,最好不要使用 `ToList()`s,让惰性求值来发挥它的魔力。 (2认同)
  • @DmitryPavlov在这段时间里,我一直不知道能够在select语句中投影这样的索引!我一直以为这是一个新功能,直到我注意到您在2014年发布了此功能,这真让我感到惊讶!感谢分享。另外,让此扩展方法可用于IEnumerable并返回IEnumerable会更好吗? (2认同)

Ser*_*-Tm 226

public static List<List<float[]>> splitList(List<float[]> locations, int nSize=30)  
{        
    var list = new List<List<float[]>>(); 

    for (int i = 0; i < locations.Count; i += nSize) 
    { 
        list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i))); 
    } 

    return list; 
} 
Run Code Online (Sandbox Code Playgroud)

通用版本:

public static IEnumerable<List<T>> splitList<T>(List<T> locations, int nSize=30)  
{        
    for (int i = 0; i < locations.Count; i += nSize) 
    { 
        yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); 
    }  
} 
Run Code Online (Sandbox Code Playgroud)

  • @MatthewPigram测试了它的工作.Math.Min取最小值,因此如果最后一个块小于nSize(2 <3),它将创建一个包含剩余项目的列表. (2认同)
  • @Jorn.Beyers 可能属于微优化类别。如果有问题那就只是问题。微软表示 .Count 是一个 O(1) 操作,所以我怀疑您将其存储在变量中是否会看到任何改进:https://docs.microsoft.com/en-us/dotnet/api/system.collections .generic.list-1.count?view=netcore-3.1 (2认同)

Raf*_*fal 33

怎么样:

while(locations.Any())
{    
    list.Add(locations.Take(nSize).ToList());
    locations= locations.Skip(nSize).ToList();
}
Run Code Online (Sandbox Code Playgroud)

  • 是的,每个循环都会创建新列表.是的它消耗了记忆.但是如果您遇到内存问题,那么这不是优化的地方,因为该列表的实例已准备好在下一个循环中收集.您可以通过跳过"ToList"来交换内存性能,但我不打算尝试优化它 - 它是如此微不足道,不太可能成为瓶颈.这种实现的主要好处是它的易懂性很容易理解.如果你想要,你可以使用接受的答案,它不会创建这些列表,但有点复杂. (2认同)
  • 每次调用.Skip(n)都会在n个元素上进行迭代,尽管这可能没事,但是考虑性能关键的代码很重要。http://stackoverflow.com/questions/20002975/performance-of-skip-and-similar-functions-like-take (2认同)

equ*_*tas 10

Serj-Tm解决方案很好,这也是通用版本作为列表的扩展方法(把它放到静态类中):

public static List<List<T>> Split<T>(this List<T> items, int sliceSize = 30)
{
    List<List<T>> list = new List<List<T>>();
    for (int i = 0; i < items.Count; i += sliceSize)
        list.Add(items.GetRange(i, Math.Min(sliceSize, items.Count - i)));
    return list;
} 
Run Code Online (Sandbox Code Playgroud)


Lin*_*nas 8

我发现接受的答案(Serj-Tm)最强大,但我想建议一个通用版本.

   public static List<List<T>> splitList<T>(List<T> locations, int nSize = 30)
    {
        var list = new List<List<T>>();

        for (int i = 0; i < locations.Count; i += nSize)
        {
            list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i)));
        }

        return list;
    }
Run Code Online (Sandbox Code Playgroud)


Har*_*lse 8

在最后 mhand 的非常有用的评论之后添加

原答案

尽管大多数解决方案可能有效,但我认为它们不是很有效。假设您只想要前几个块的前几个项目。那么您就不想遍历序列中的所有(无数)项。

以下将最多枚举两次:一次用于 Take,一次用于 Skip。它不会枚举比您将使用的元素更多的元素:

public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
    (this IEnumerable<TSource> source, int chunkSize)
{
    while (source.Any())                     // while there are elements left
    {   // still something to chunk:
        yield return source.Take(chunkSize); // return a chunk of chunkSize
        source = source.Skip(chunkSize);     // skip the returned chunk
    }
}
Run Code Online (Sandbox Code Playgroud)

这将枚举序列多少次?

假设您将源分为chunkSize. 您仅枚举前 N 个块。从每个枚举块中,您将只枚举前 M 个元素。

While(source.Any())
{
     ...
}
Run Code Online (Sandbox Code Playgroud)

Any 将获取枚举器,执行 1 MoveNext() 并在处理枚举器后返回返回值。这将做N次

yield return source.Take(chunkSize);
Run Code Online (Sandbox Code Playgroud)

根据参考来源,这将执行以下操作:

public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
    return TakeIterator<TSource>(source, count);
}

static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
    foreach (TSource element in source)
    {
        yield return element;
        if (--count == 0) break;
    }
}
Run Code Online (Sandbox Code Playgroud)

在您开始枚举获取的 Chunk 之前,这不会做很多事情。如果您获取多个块,但决定不枚举第一个块,则不会执行 foreach,因为您的调试器会显示给您。

如果您决定采用第一个块的前 M 个元素,则 yield return 将执行 M 次。这意味着:

  • 获取枚举器
  • 调用 MoveNext() 和 Current M 次。
  • 处理枚举器

在返回第一个块后,我们跳过第一个块:

source = source.Skip(chunkSize);
Run Code Online (Sandbox Code Playgroud)

再次:我们将查看参考源以找到skipiterator

static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
    using (IEnumerator<TSource> e = source.GetEnumerator()) 
    {
        while (count > 0 && e.MoveNext()) count--;
        if (count <= 0) 
        {
            while (e.MoveNext()) yield return e.Current;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,对 Chunk 中的每个元素SkipIterator调用MoveNext()一次。它不会调用Current.

因此,每个 Chunk 我们看到完成了以下操作:

  • Any(): GetEnumerator; 1 移动下一步();处置枚举器;
  • 拿():

    • 如果块的内容没有被枚举,则什么都没有。
    • 如果枚举内容:GetEnumerator(),每个枚举项一个 MoveNext 和一个 Current,Dispose enumerator;

    • Skip():对于枚举的每个块(不是块的内容):GetEnumerator(), MoveNext() chunkSize 次,没有 Current!配置枚举器

如果您查看枚举器发生的情况,您会发现有很多对 MoveNext() 的调用,并且只调用了Current您实际决定访问的 TSource 项。

如果取 N 个大小为 chunkSize 的块,则调用 MoveNext()

  • Any() 的 N 次
  • 还没到 Take 的时候,只要你不列举 Chunks
  • Skip() 的 N 倍 chunkSize

如果您决定仅枚举每个获取的块的前 M 个元素,那么您需要为每个枚举的块调用 MoveNext M 次。

总数

MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Run Code Online (Sandbox Code Playgroud)

因此,如果您决定枚举所有块的所有元素:

MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
Run Code Online (Sandbox Code Playgroud)

MoveNext 是否需要大量工作,取决于源序列的类型。对于列表和数组,它是一个简单的索引增量,可能带有超出范围的检查。

但是如果你的 IEnumerable 是数据库查询的结果,请确保数据确实在你的计算机上具体化,否则数据将被多次获取。DbContext 和 Dapper 会在数据被访问之前正确地将数据传输到本地进程。如果多次枚举相同的序列,则不会多次提取。Dapper 返回一个 List 对象,DbContext 记住数据已经被获取。

在开始划分 Chunks 中的项目之前调用 AsEnumerable() 或 ToLists() 是否明智取决于您的存储库


Sco*_*nen 7

.NET 6 之前

public static IEnumerable<IEnumerable<T>> SplitIntoSets<T>
    (this IEnumerable<T> source, int itemsPerSet) 
{
    var sourceList = source as List<T> ?? source.ToList();
    for (var index = 0; index < sourceList.Count; index += itemsPerSet)
    {
        yield return sourceList.Skip(index).Take(itemsPerSet);
    }
}
Run Code Online (Sandbox Code Playgroud)

.NET 6

var originalList = new List<int>{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}

// split into arrays of no more than three
IEnumerable<int[]> chunks = originalList.originalList.Chunk(3);
Run Code Online (Sandbox Code Playgroud)


Tia*_*Lin 6

我有一个通用的方法,可以采取任何类型包括浮点数,它已经过单元测试,希望它有所帮助:

    /// <summary>
    /// Breaks the list into groups with each group containing no more than the specified group size
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="values">The values.</param>
    /// <param name="groupSize">Size of the group.</param>
    /// <returns></returns>
    public static List<List<T>> SplitList<T>(IEnumerable<T> values, int groupSize, int? maxCount = null)
    {
        List<List<T>> result = new List<List<T>>();
        // Quick and special scenario
        if (values.Count() <= groupSize)
        {
            result.Add(values.ToList());
        }
        else
        {
            List<T> valueList = values.ToList();
            int startIndex = 0;
            int count = valueList.Count;
            int elementCount = 0;

            while (startIndex < count && (!maxCount.HasValue || (maxCount.HasValue && startIndex < maxCount)))
            {
                elementCount = (startIndex + groupSize > count) ? count - startIndex : groupSize;
                result.Add(valueList.GetRange(startIndex, elementCount));
                startIndex += elementCount;
            }
        }


        return result;
    }
Run Code Online (Sandbox Code Playgroud)

  • 对可枚举的多个枚举要小心。`values.Count()` 将导致一个完整的枚举,然后 `values.ToList()` 另一个。更安全的做法是 `values = values.ToList()` 所以它已经实现了。 (2认同)

Sid*_*ron 6

图书馆MoreLinq的方法称为 Batch

List<int> ids = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; // 10 elements
int counter = 1;
foreach(var batch in ids.Batch(2))
{
    foreach(var eachId in batch)
    {
        Console.WriteLine("Batch: {0}, Id: {1}", counter, eachId);
    }
    counter++;
}
Run Code Online (Sandbox Code Playgroud)

结果是

Batch: 1, Id: 1
Batch: 1, Id: 2
Batch: 2, Id: 3
Batch: 2, Id: 4
Batch: 3, Id: 5
Batch: 3, Id: 6
Batch: 4, Id: 7
Batch: 4, Id: 8
Batch: 5, Id: 9
Batch: 5, Id: 0
Run Code Online (Sandbox Code Playgroud)

ids 分为2个元素的5个大块。

  • 这需要成为公认的答案。或者至少在这个页面上更高。 (2认同)

mha*_*and 6

尽管上面的许多答案都能胜任,但它们都以永无止境的顺序(或很长的顺序)严重失败。以下是一个完全在线的实现,可以保证最佳的时间和内存复杂性。我们仅将源可枚举迭代一次,然后使用yield return进行惰性评估。消费者可以在每次迭代时丢弃该列表,从而使内存占用量batchSize与元素数量相等的内存占用量相等。

public static IEnumerable<List<T>> BatchBy<T>(this IEnumerable<T> enumerable, int batchSize)
{
    using (var enumerator = enumerable.GetEnumerator())
    {
        List<T> list = null;
        while (enumerator.MoveNext())
        {
            if (list == null)
            {
                list = new List<T> {enumerator.Current};
            }
            else if (list.Count < batchSize)
            {
                list.Add(enumerator.Current);
            }
            else
            {
                yield return list;
                list = new List<T> {enumerator.Current};
            }
        }

        if (list?.Count > 0)
        {
            yield return list;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:刚刚意识到OP要求将a List<T>分成更小的List<T>,所以我对无限可枚举的评论不适用于OP,但可能会帮助到这里的其他人。这些评论是对其他发布的解决方案的回应,这些解决方案确实IEnumerable<T>用作其功能的输入,但多次枚举了可枚举的来源。


ola*_*ker 5

从 .NET 6.0 开始,您可以使用 LINQ 扩展Chunk<T>()将枚举拆分为块。文档

var chars = new List<char>() { 'h', 'e', 'l', 'l', 'o', 'w','o','r' ,'l','d' };
foreach (var batch in chars.Chunk(2))
{
    foreach (var ch in batch)
    {
        // iterates 2 letters at a time
    }
}
Run Code Online (Sandbox Code Playgroud)