将List拆分为N个大小的更小的列表

我试图将一个列表拆分为一系列较小的列表。

我的问题:我的拆分列表的函数没有将它们拆分为正确大小的列表。它应该把它们分成大小为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;
}

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

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

330448 次浏览
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;
}

通用版:

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

如何:

while(locations.Any())
{
list.Add(locations.Take(nSize).ToList());
locations= locations.Skip(nSize).ToList();
}

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

    /// <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;
}

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

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

例如,如果你把18个项目分成5个块,它会给你一个包含4个子列表的列表,其中包含以下项目:5-5-5-3。

注:在即将在__ABC1中对LINQ进行改进块 会像这样从盒子里出来:

const int PAGE_SIZE = 5;


IEnumerable<Movie[]> chunks = movies.Chunk(PAGE_SIZE);

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

我发现公认的答案(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;
}

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++;
}

结果是

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

ids被分成5个块,每个块有2个元素。

补充后很有用的评论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
}
}

这将枚举序列多少次?

假设你将源代码划分为chunkSize块。只枚举前N个块。对于每个枚举块,只枚举前M个元素。

While(source.Any())
{
...
}

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

yield return source.Take(chunkSize);

根据参考源,它将执行如下操作:

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

在开始枚举所获取的Chunk之前,这不会做很多工作。如果您获取了几个Chunk,但决定不对第一个Chunk进行枚举,则不会执行foreach,正如调试器将显示的那样。

如果你决定取第一个块的前M个元素,那么yield返回将执行M次。这意味着:

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

在第一个区块已经返回yield后,我们跳过第一个区块:

source = source.Skip(chunkSize);

再一次:我们将查看参考源来找到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;
}
}
}

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

所以在每个Chunk中,我们看到完成了以下工作:

  • 任何():GetEnumerator;1 MoveNext ();处理枚举器;
  • < p > ():

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

    • Skip():对于每个被枚举的chunk(不是chunk的内容): GetEnumerator(), MoveNext() chunkSize times, no Current!李处理枚举器< / p > < / > 李< / ul > < / >

    如果你看看枚举器发生了什么,你会发现有很多对MoveNext()的调用,而对于你真正决定访问的TSource项,只有对Current的调用。

    如果你取N个大小为chunkSize的chunk,然后调用MoveNext()

    • Any() N次
    • 只要你不枚举块,Take就没有时间了
    • N倍chunkSize for Skip()

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

    MoveNext calls: N + N*M + N*chunkSize
    Current calls: N*M; (only the items you really access)
    

    所以如果你决定枚举所有块的所有元素:

    MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
    Current: every item is accessed exactly once
    

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

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

    在开始将项划分为块之前,调用AsEnumerable()或ToLists()是否明智取决于您的存储库

虽然上面的很多答案都是可行的,但它们在永不结束的序列(或非常长的序列)上都失败了。下面是一个完全在线的实现,它保证了最好的时间和内存复杂度。我们只迭代源枚举对象一次,并使用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;
}
}
}

编辑:刚刚意识到OP要求将List<T>分解为更小的List<T>,所以我关于无限枚举数的评论不适用于OP,但可能会帮助其他在这里结束的人。这些注释是对其他已发布的解决方案的响应,这些解决方案确实使用IEnumerable<T>作为其函数的输入,但却多次枚举源可枚举对象。

针对.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.Chunk(3);

在。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);
}
}
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
return items.Select((item, index) => new { item, index })
.GroupBy(x => x.index / maxItems)
.Select(g => g.Select(x => x.item));
}

这个怎么样?我们的想法是只使用一个循环。谁知道呢,也许你在代码中只使用了IList实现,你不想强制转换为List。

private IEnumerable<IList<T>> SplitList<T>(IList<T> list, int totalChunks)
{
IList<T> auxList = new List<T>();
int totalItems = list.Count();


if (totalChunks <= 0)
{
yield return auxList;
}
else
{
for (int i = 0; i < totalItems; i++)
{
auxList.Add(list[i]);


if ((i + 1) % totalChunks == 0)
{
yield return auxList;
auxList = new List<T>();
}


else if (i == totalItems - 1)
{
yield return auxList;
}
}
}
}

多一个

public static IList<IList<T>> SplitList<T>(this IList<T> list, int chunkSize)
{
var chunks = new List<IList<T>>();
List<T> chunk = null;
for (var i = 0; i < list.Count; i++)
{
if (i % chunkSize == 0)
{
chunk = new List<T>(chunkSize);
chunks.Add(chunk);
}
chunk.Add(list[i]);
}
return chunks;
}
public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize)
{
var result = new List<List<T>>();
for (int i = 0; i < source.Count; i += chunkSize)
{
var rows = new List<T>();
for (int j = i; j < i + chunkSize; j++)
{
if (j >= source.Count) break;
rows.Add(source[j]);
}
result.Add(rows);
}
return result;
}
List<int> orginalList =new List<int>(){1,2,3,4,5,6,7,8,9,10,12};
Dictionary<int,List<int>> dic = new Dictionary <int,List<int>> ();
int batchcount = orginalList.Count/2; //To List into two 2 parts if you
want three give three
List<int> lst = new List<int>();
for (int i=0;i<orginalList.Count; i++)
{
lst.Add(orginalList[i]);
if (i % batchCount == 0 && i!=0)
{
Dic.Add(threadId, lst);
lst = new List<int>();**strong text**
threadId++;
}
}
if(lst.Count>0)
Dic.Add(threadId, lst); //in case if any dayleft
foreach(int BatchId in Dic.Keys)
{
Console.Writeline("BatchId:"+BatchId);
Console.Writeline('Batch Count:"+Dic[BatchId].Count);
}

我也遇到过同样的需求,我使用了Linq的跳过()带()方法的组合。我用我取的数乘以到目前为止的迭代次数,这就得到了要跳过的项目数,然后我取下一组。

        var categories = Properties.Settings.Default.MovementStatsCategories;
var items = summariesWithinYear
.Select(s =>  s.sku).Distinct().ToList();


//need to run by chunks of 10,000
var count = items.Count;
var counter = 0;
var numToTake = 10000;


while (count > 0)
{
var itemsChunk = items.Skip(numToTake * counter).Take(numToTake).ToList();
counter += 1;


MovementHistoryUtilities.RecordMovementHistoryStatsBulk(itemsChunk, categories, nLogger);


count -= numToTake;
}
基于德米特里·巴甫洛夫回答,我将删除.ToList()。还要避免使用匿名类。 相反,我喜欢使用不需要分配堆内存的结构体。(ValueTuple也可以做这个工作。)

public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (chunkSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(chunkSize), chunkSize, "The argument must be greater than zero.");
}


return source
.Select((x, i) => new ChunkedValue<TSource>(x, i / chunkSize))
.GroupBy(cv => cv.ChunkIndex)
.Select(g => g.Select(cv => cv.Value));
}


[StructLayout(LayoutKind.Auto)]
[DebuggerDisplay("{" + nameof(ChunkedValue<T>.ChunkIndex) + "}: {" + nameof(ChunkedValue<T>.Value) + "}")]
private struct ChunkedValue<T>
{
public ChunkedValue(T value, int chunkIndex)
{
this.ChunkIndex = chunkIndex;
this.Value = value;
}


public int ChunkIndex { get; }


public T Value { get; }
}

可以像下面这样使用,只在集合上迭代一次并且 也不分配任何重要的内存

int chunkSize = 30;
foreach (var chunk in collection.ChunkBy(chunkSize))
{
foreach (var item in chunk)
{
// your code for item here.
}
}

如果确实需要一个具体的列表,我会这样做:

int chunkSize = 30;
var chunkList = new List<List<T>>();
foreach (var chunk in collection.ChunkBy(chunkSize))
{
// create a list with the correct capacity to be able to contain one chunk
// to avoid the resizing (additional memory allocation and memory copy) within the List<T>.
var list = new List<T>(chunkSize);
list.AddRange(chunk);
chunkList.Add(list);
}

在。net 6中,你可以只使用source.Chunk(chunkSize)

一个基于Serj-Tm的公认答案的更通用的版本。

    public static IEnumerable<IEnumerable<T>> Split<T>(IEnumerable<T> source, int size = 30)
{
var count = source.Count();
for (int i = 0; i < count; i += size)
{
yield return source
.Skip(Math.Min(size, count - i))
.Take(size);
}
}

如果你想用条件而不是固定的数字来分割它:

///<summary>
/// splits a list based on a condition (similar to the split function for strings)
///</summary>
public static IEnumerable<List<T>> Split<T>(this IEnumerable<T> src, Func<T, bool> pred)
{
var list = new List<T>();
foreach(T item in src)
{
if(pred(item))
{
if(list != null && list.Count > 0)
yield return list;
                

list = new List<T>();
}
else
{
list.Add(item);
}
}
}

从。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
}
}

你可以只使用LINQ简单地尝试以下代码:

public static IList<IList<T>> Split<T>(IList<T> source)
{
return  source
.Select((x, i) => new { Index = i, Value = x })
.GroupBy(x => x.Index / 3)
.Select(x => x.Select(v => v.Value).ToList())
.ToList();
}