如何将一个数组分割成特定大小的块?

50

下午好,

我需要将一个数组分成较小的“块”。

我正在传递大约1200个项目,并需要将它们分成每个100个项目更容易处理的数组,然后我需要对其进行处理。

请问有人可以提出一些建议吗?


3
https://dev59.com/xnRB5IYBdhLWcg3wEDul#733261 - JAB
我需要向亚马逊MWS传递1200多个项目,但他们的API一次只允许100个。因此,我需要将数组拆分。我正在使用“string[] amzProductAsins = GetProductAsin();”将数组传入,该数组从我的数据库中获取ASIN,并创建一个数组 :) - thatuxguy
@ChrisGessler 我正在寻找最好或最有效的方法。正如我所说,目前数据库中有1200多个产品,我需要尝试使用Amazon MWS API获得最低价格,而这个API目前仅能批量处理100个产品(最大值)。我的代码失败了,因为我一次传递了1241个产品,哈哈 :) - thatuxguy
1
@thatuxguy - 如果性能是最重要的考虑因素,我建议使用Array.Copy或ArraySegment<>。请参阅我的答案,了解两者的详细信息和性能结果。 - Chris Gessler
1
.NET 6 新的 Linq Chunk() 方法:https://dev59.com/fXRC5IYBdhLWcg3wD87r#69625204 - xhafan
9个回答

84

Array.Copy自1.1版本以来就存在了,可以很好地分块数组。

List.GetRange()也是另一个答案中提到的不错选择。

string[] buffer;

for(int i = 0; i < source.Length; i+=100)
{
    buffer = new string[100];
    Array.Copy(source, i, buffer, 0, 100);
    // process array
}

并为其创建一个扩展:

public static class Extensions
{
    public static T[] Slice<T>(this T[] source, int index, int length)
    {       
        T[] slice = new T[length];
        Array.Copy(source, index, slice, 0, length);
        return slice;
    }
}

而且使用该扩展:

string[] source = new string[] { 1200 items here };

// get the first 100
string[] slice = source.Slice(0, 100);

更新:我认为你可能想要使用ArraySegment<>。无需进行性能检查,因为它只是使用原始数组作为其源并维护偏移量和计数属性来确定“段”。不幸的是,没有一种方法可以仅检索段作为数组,因此一些人编写了包装器,例如这里:ArraySegment - Returning the actual segment C#
ArraySegment<string> segment;
           
for (int i = 0; i < source.Length; i += 100)
{
    segment = new ArraySegment<string>(source, i, 100);

    // and to loop through the segment
    for (int s = segment.Offset; s < segment.Array.Length; s++)
    {
        Console.WriteLine(segment.Array[s]);
    }
}

Array.Copy、Skip/Take和LINQ的性能比较

测试方法(在Release模式下):

static void Main(string[] args)
{
    string[] source = new string[1000000];
    for (int i = 0; i < source.Length; i++)
    {
        source[i] = "string " + i.ToString();
    }

    string[] buffer;

    Console.WriteLine("Starting stop watch");

    Stopwatch sw = new Stopwatch();

    for (int n = 0; n < 5; n++)
    {
        sw.Reset();
        sw.Start();
        for (int i = 0; i < source.Length; i += 100)
        {
            buffer = new string[100];
            Array.Copy(source, i, buffer, 0, 100);
        }

        sw.Stop();
        Console.WriteLine("Array.Copy: " + sw.ElapsedMilliseconds.ToString());

        sw.Reset();
        sw.Start();
        for (int i = 0; i < source.Length; i += 100)
        {
            buffer = new string[100];
            buffer = source.Skip(i).Take(100).ToArray();
        }
        sw.Stop();
        Console.WriteLine("Skip/Take: " + sw.ElapsedMilliseconds.ToString());

        sw.Reset();
        sw.Start();
        String[][] chunks = source                            
            .Select((s, i) => new { Value = s, Index = i })                            
            .GroupBy(x => x.Index / 100)                            
            .Select(grp => grp.Select(x => x.Value).ToArray())                            
            .ToArray();
        sw.Stop();
        Console.WriteLine("LINQ: " + sw.ElapsedMilliseconds.ToString());
    }
    Console.ReadLine();
}

结果(毫秒):

Array.Copy:    15
Skip/Take:  42464
LINQ:         881

Array.Copy:    21
Skip/Take:  42284
LINQ:         585

Array.Copy:    11
Skip/Take:  43223
LINQ:         760

Array.Copy:     9
Skip/Take:  42842
LINQ:         525

Array.Copy:    24
Skip/Take:  43134
LINQ:         638

@psubsee2003 - 谢谢。我猜它的性能也会比Skip/Take解决方案更好,特别是考虑到必须调用.ToArray()的情况。 - Chris Gessler
在您的Skip(),Take()示例中,有一个不必要的额外数组分配。 - James Michael Hare
@JamesMichaelHare - 重新运行测试 - 结果相同。请记住,这个测试将一百万个元素的数组分解,这不太可能在真实世界中频繁进行。当我将数字降低到10,000个元素时,百分比差异下降得更接近于LINQ,即10-20,000%更慢。 - Chris Gessler
1
@ChrisGessler - 实际上,这似乎是一个非常常见的用例,我认为它可以被赋予自己的专用LINQ扩展方法(命名为Slice()之类的东西)。 - James Michael Hare
谢谢,我选择了Array.Copy()解决方案。我进行了性能基准测试,并且对于我需要的目的来说非常棒:)谢谢 - badjuice
显示剩余3条评论

54

您可以使用 LINQ 将所有项按块大小进行分组,然后创建新的数组。

// build sample data with 1200 Strings
string[] items = Enumerable.Range(1, 1200).Select(i => "Item" + i).ToArray();
// split on groups with each 100 items
String[][] chunks = items
                    .Select((s, i) => new { Value = s, Index = i })
                    .GroupBy(x => x.Index / 100)
                    .Select(grp => grp.Select(x => x.Value).ToArray())
                    .ToArray();

for (int i = 0; i < chunks.Length; i++)
{
    foreach (var item in chunks[i])
        Console.WriteLine("chunk:{0} {1}", i, item);
}

请注意,创建新数组(会耗费cpu周期和内存)不是必要的。当省略两个ToArrays时,您也可以使用IEnumerable<IEnumerable<String>>

下面是运行代码:http://ideone.com/K7Hn2


3
LINQ 在不需要实际使用关系型数据库的情况下,可以很方便地使用关系型数据库技术,看起来非常实用。 - JAB
这看起来很不错,那么一旦我将它们分成100组,我就可以将它们传递给亚马逊的MWS进行处理了? - thatuxguy

20

这里我发现了另一种 LINQ 解决方案:

int[] source = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int chunkSize = 3;
int[][] result = source.GroupBy(s => i++ / chunkSize).Select(g => g.ToArray()).ToArray();

//result = [1,2,3][4,5,6][7,8,9]

11
你可以使用 Skip()Take()
string[] items = new string[]{ "a", "b", "c"};
string[] chunk = items.Skip(1).Take(1).ToArray();

3
由于Skip和Take是在IEnumerable上操作的,因此您可能需要在将结果存储在string[]之前对其进行ToArray()处理。 - Heinzi
@Asif,已经创建了一个数组 - string[] amzProductAsins = GetProductAsin(); - thatuxguy
2
这不会创建相同块大小的组(12个大小为100的“子数组”)。 - Tim Schmelter
1
这个解决方案比Array.Copy更好在哪里? - Chris Gessler

8
    string[]  amzProductAsins = GetProductAsin();;
    List<string[]> chunks = new List<string[]>();
    for (int i = 0; i < amzProductAsins.Count; i += 100)
    {
        chunks.Add(amzProductAsins.Skip(i).Take(100).ToArray());
    }

@Habib.OSU 我已经有一个包含1200多个项目的字符串了...我该如何在这个 - string[] amzProductAsins = GetProductAsin(); 上使用它? - thatuxguy
还有不到1200,这个正确吗?也许需要先数一下,我猜 :D - thatuxguy
Skip().Take().ToArray() 相比于 Array.Copy 非常慢。 - Chris Gessler
@ChrisGessler,你说得没错,但Skip().Take()是其中一种可能的方法,而且这是我在看到问题后首先想到的。 - Habib
我同意这是一种可能的方法,但问题是“最佳拆分数组的方法”,这需要更多的思考和研究。 - Chris Gessler

5
您可以使用List.GetRange进行操作,具体用法请参见官方文档
for(var i = 0; i < source.Count; i += chunkSize)
{
    List<string> items = source.GetRange(i, Math.Min(chunkSize, source.Count - i));
}

虽然不如Array.Copy快,但我认为这样看起来更加简洁:

var list = Enumerable.Range(0, 723748).ToList();

var stopwatch = new Stopwatch();

for (int n = 0; n < 5; n++)
{
    stopwatch.Reset();
    stopwatch.Start();
    for(int i = 0; i < list.Count; i += 100)
    {
        List<int> c = list.GetRange(i, Math.Min(100, list.Count - i));
    }
    stopwatch.Stop();
    Console.WriteLine("List<T>.GetRange: " + stopwatch.ElapsedMilliseconds.ToString());

    stopwatch.Reset();
    stopwatch.Start();
    for (int i = 0; i < list.Count; i += 100)
    {
        List<int> c = list.Skip(i).Take(100).ToList();
    }
    stopwatch.Stop();
    Console.WriteLine("Skip/Take: " + stopwatch.ElapsedMilliseconds.ToString());

    stopwatch.Reset();
    stopwatch.Start();
    var test = list.ToArray();
    for (int i = 0; i < list.Count; i += 100)
    {
        int length = Math.Min(100, list.Count - i);
        int[] c = new int[length];
        Array.Copy(test, i, c, 0, length);
    }
    stopwatch.Stop();
    Console.WriteLine("Array.Copy: " + stopwatch.ElapsedMilliseconds.ToString());

    stopwatch.Reset();
    stopwatch.Start();
    List<List<int>> chunks = list
        .Select((s, i) => new { Value = s, Index = i })
        .GroupBy(x => x.Index / 100)
        .Select(grp => grp.Select(x => x.Value).ToList())
        .ToList();
    stopwatch.Stop();
    Console.WriteLine("LINQ: " + stopwatch.ElapsedMilliseconds.ToString());
}

毫秒级结果:

List<T>.GetRange: 1
Skip/Take: 9820
Array.Copy: 1
LINQ: 161

List<T>.GetRange: 9
Skip/Take: 9237
Array.Copy: 1
LINQ: 148

List<T>.GetRange: 5
Skip/Take: 9470
Array.Copy: 1
LINQ: 186

List<T>.GetRange: 0
Skip/Take: 9498
Array.Copy: 1
LINQ: 110

List<T>.GetRange: 8
Skip/Take: 9717
Array.Copy: 1
LINQ: 148

1
通用递归扩展方法:

    public static IEnumerable<IEnumerable<T>> SplitList<T>(this IEnumerable<T> source, int maxPerList)
    {
        var enumerable = source as IList<T> ?? source.ToList();
        if (!enumerable.Any())
        {
            return new List<IEnumerable<T>>();
        }
        return (new List<IEnumerable<T>>() { enumerable.Take(maxPerList) }).Concat(enumerable.Skip(maxPerList).SplitList<T>(maxPerList));
    }

1
使用LINQ,您可以使用Take()和Skip()函数。

我认为Take和Skip是IEnumerable扩展方法,而不是LINQ。 - Chris Gessler
3
它们是System.Linq库的一部分。LINQ不仅是查询语言,还包括库。 - James Michael Hare

-2
如果您有一个需要分割但是分割后有余数的数组,使用这个简单的解决方案,您可以将缺失的元素平均分配到各个“块”中。
int[] arrInput = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
var result = SplitArrey(arrInput, 5);
foreach (var item in result) {
  Console.WriteLine("   {0}", String.Join(" ", item));
}

这个函数是:

public static List<int[]> SplitArrey(int[] arrInput, int nColumn) {

        List<int[]> result = new List<int[]>(nColumn);
    
        int itemsForColum = arrInput.Length / nColumn;  
        int countSpareElement = arrInput.Length - (itemsForColum * nColumn);    

        // Add and extra space for the spare element
        int[] newColumLenght = new int[nColumn];
        for (int i = 0; i < nColumn; i++)
        {
            int addOne = (i < countSpareElement) ? 1 : 0;
            newColumLenght[i] = itemsForColum + addOne;
            result.Add(new int[itemsForColum + addOne]);
        }

        // Copy the values
        int offset = 0;
        for (int i = 0; i < nColumn; i++)
        {
            int count_items_to_copy = newColumLenght[i];
            Array.Copy(arrInput, offset, result[i], 0, count_items_to_copy);
            offset += newColumLenght[i];
        }
        return result;
    }

结果是:

1 2 3
4 5 6
7 8
9 10
11 12

public static T[] Slice<T>(this T[] source, int index, int length) { int delta = source.Length - (index + length); int actualLength = delta >= 0 ? length : length + delta; // 当 index + length > source.Length 时,减少长度 T[] slice = new T[actualLength]; Array.Copy(source, index, slice, 0, actualLength); return slice; } - user1920925
1
对于一个简单的事情来说,它太复杂了。 - Muhammad Faizan Khatri

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接