将一个JS数组拆分为N个数组

122

假设我有这样一个JS数组:

var a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];

我想要的是将该数组分成N个较小的数组。例如:

split_list_in_n(a, 2)
[[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11]]

For N = 3:
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]]

For N = 4:
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11]]

For N = 5:
[[1, 2, 3], [4, 5], [6, 7], [8, 9], [10, 11]]

对于Python,我有这样一个代码:

def split_list_in_n(l, cols):
    """ Split up a list in n lists evenly size chuncks """
    start = 0
    for i in xrange(cols):
        stop = start + len(l[i::cols])
        yield l[start:stop]
        start = stop

对于JS而言,我能想到的最好的右解决方案是一个递归函数,但我不喜欢它,因为它很复杂且难看。这个内部函数返回一个像这样的数组[1, 2, 3, null, 4, 5, 6, null, 7, 8],然后我必须再次循环并手动拆分它。(我的第一次尝试返回了这个:[1, 2, 3, [4, 5, 6, [7, 8, 9]]],然后我决定用null作为分隔符来实现它)。

function split(array, cols) {
    if (cols==1) return array;
    var size = Math.ceil(array.length / cols);
    return array.slice(0, size).concat([null]).concat(split(array.slice(size), cols-1));
}

这是一个 jsfiddle 的链接:http://jsfiddle.net/uduhH/

你怎么实现这个?谢谢!


2
与 - https://dev59.com/cpvga4cB1Zd3GeqP7tGI 相关 - vsync
1
你的 split 函数已经很接近了。你可以通过添加两个数组包装器来消除 null 的问题:if (cols == 1) return [array]return [array.slice(0, size)].concat(split(array.slice(size), cols-1))。我发现这个递归版本比这里大多数答案更易读。 - Scott Sauyet
25个回答

158

您可以使切片“平衡”(子数组的长度尽可能相似)或“均匀”(除最后一个子数组外,所有子数组的长度相同):

function chunkify(a, n, balanced) {
    
    if (n < 2)
        return [a];

    var len = a.length,
            out = [],
            i = 0,
            size;

    if (len % n === 0) {
        size = Math.floor(len / n);
        while (i < len) {
            out.push(a.slice(i, i += size));
        }
    }

    else if (balanced) {
        while (i < len) {
            size = Math.ceil((len - i) / n--);
            out.push(a.slice(i, i += size));
        }
    }

    else {

        n--;
        size = Math.floor(len / n);
        if (len % size === 0)
            size--;
        while (i < size * n) {
            out.push(a.slice(i, i += size));
        }
        out.push(a.slice(size * n));

    }

    return out;
}


///////////////////////

onload = function () {
    function $(x) {
        return document.getElementById(x);
    }

    function calc() {
        var s = +$('s').value, a = [];
        while (s--)
            a.unshift(s);
        var n = +$('n').value;
        $('b').textContent = JSON.stringify(chunkify(a, n, true))
        $('e').textContent = JSON.stringify(chunkify(a, n, false))
    }

    $('s').addEventListener('input', calc);
    $('n').addEventListener('input', calc);
    calc();
}
<p>slice <input type="number" value="20" id="s"> items into
<input type="number" value="6" id="n"> chunks:</p>
<pre id="b"></pre>
<pre id="e"></pre>


3
运作得很好,是个不错的解决方案。 - Vardan
嗨@georg,你能解释一下这行代码吗:var size = Math.ceil((len - i) / n--); - dpg5000
@dpg5000:在切割下一个块时,其大小为剩余元素数(len - i)除以剩余块数(n--)。 - georg
1
嗨@georg,谢谢。我该如何修改这段代码以确保所有子数组的长度相等,除了最后一个子数组(当然,如果除数没有余数,则所有子数组将相等)。感激任何帮助。 - dpg5000
1
@cbdeveloper:这是你要的 function chunkify<T>(a: T[], n: number, balanced: boolean): T[][] - georg
显示剩余2条评论

62

我认为使用splice的方式是最干净的:

function splitToNChunks(array, n) {
    let result = [];
    for (let i = n; i > 0; i--) {
        result.push(array.splice(0, Math.ceil(array.length / i)));
    }
    return result;
}

// Example:

const example = [0,1,2,3,4,5,6,7,8,9,10,11,12]

console.log(splitToNChunks([...example], 3))
console.log(splitToNChunks([...example], 5))

例如,对于 n = 3,您将取 1/3,然后取剩余部分的 1/2,最后取数组的其余部分。 Math.ceil 确保在元素数量不均匀的情况下它们将进入最早的块。
(注意:在数组上调用 .splice 将直接更改其长度。为了避免破坏初始数组,您可以使用其临时浅拷贝:const copiedArray = [ ...originalArray ]

2
这个解决方案对我很有效。只有一个建议。为了不破坏初始数组,请添加此行 const copyArray = array.map(v => v); 来创建一个浅拷贝的数组。然后在函数的其余部分中操作复制的数组。 - Peet
稍作修改。它也会创建一个副本。 const copyArray = [...array] - PCPbiscuit

19

function split(array, n) {
  let [...arr]  = array;
  var res = [];
  while (arr.length) {
    res.push(arr.splice(0, n));
  }
  return res;
}


2
对于 n = 5 和 arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],这个程序并没有按照预期工作。 - Tiago
1
这并不是将数组分成n个子数组,而是将其分成长度为n的子数组。 - dpg5000
1
请添加一些解释,说明为什么这段代码有助于原作者。这将有助于为未来的观众提供一个可以学习的答案。更多信息请参见[answer]。 - Heretic Monkey
1
为了解决OP的问题,你可以使用split(arr, Math.ceil(arr.length/chunkCount)),我猜...但我来这里是为了将数组分割成N个大小相等的块,所以这个方法非常适合 :) - wizzard0

16

我刚刚完成了算法的迭代实现:http://jsfiddle.net/ht22q/。它通过了你的测试用例。

function splitUp(arr, n) {
    var rest = arr.length % n, // how much to divide
        restUsed = rest, // to keep track of the division over the elements
        partLength = Math.floor(arr.length / n),
        result = [];

    for(var i = 0; i < arr.length; i += partLength) {
        var end = partLength + i,
            add = false;

        if(rest !== 0 && restUsed) { // should add one element for the division
            end++;
            restUsed--; // we've used one division element now
            add = true;
        }

        result.push(arr.slice(i, end)); // part of the array

        if(add) {
            i++; // also increment i in the case we added an extra element for division
        }
    }

    return result;
}

1
嗨!谢谢你的帮助。很好地考虑了如何使用 REST。 - Tiago

10

你可以将它转化为一个矩阵。下面的例子将数组(arr)分割成了由两个元素组成的数组矩阵。如果你想要其他大小,只需在第二行更改2的值:

target.reduce((memo, value, index) => {
  if (index % 2 === 0 && index !== 0) memo.push([])
  memo[memo.length - 1].push(value)
  return memo
}, [[]])

希望对您有所帮助!

编辑:因为一些人仍然在评论中提出我并没有回答问题,因为我正在调整每个块的大小而不是我想要的块数。这里是代码,解释了我在评论部分尝试解释的内容:使用target.length

// Chunk function

const chunk = (target, size) => {
  return target.reduce((memo, value, index) => {
    // Here it comes the only difference
    if (index % (target.length / size) == 0 && index !== 0) memo.push([])
    memo[memo.length - 1].push(value)
    return memo
  }, [[]])
}

// Usage

write(chunk([1, 2, 3, 4], 2))
write(chunk([1, 2, 3, 4], 4))

// For rendering pruposes. Ignore
function write (content) { document.write(JSON.stringify(content), '</br>') }


2
哇,这是一个非常简洁的方法!太棒了!干得好! :-) - Philippe Monnet
1
我喜欢这个技巧,但它并没有回答问题。它返回任意数量的x大小的块,而问题是要求x数量的均匀大小的块。 - Jodi Warren
3
太好了!我已经将代码重构,使其返回均匀分割的子数组。function splitArr(arr, n) { return arr.reduce(function (a, i) { if (a[a.length - 1].length >= arr.length / n) { a.push([]); } a[a.length - 1].push(i); return a; }, [[]]); } - davide andreazzini
3
肯定不是问题的答案。 - macdelacruz
1
简洁而聪明,这是我解决这个问题和其他许多情况的首选方法,感谢! - Edmond Tamas
显示剩余3条评论

9

更新:2020年7月21日

几年前我给出的答案只适用于originalArray.length <= numCols的情况。您可以尝试使用下面的函数,但是它将创建一个与问题不完全匹配的布局(水平排序而不是垂直排序)。例如:[1,2,3,4] -> [[1,4],[2],[3]]。我知道这可能仍然有价值,所以我会保留这个答案,但我建议您参考Senthe的答案

function splitArray(flatArray, numCols){
  const newArray = []
  for (let c = 0; c < numCols; c++) {
    newArray.push([])
  }
  for (let i = 0; i < flatArray.length; i++) {
    const mod = i % numCols
    newArray[mod].push(flatArray[i])
  }
  return newArray
}

2017年的原始回答:

虽然不要求使用vanillaJS,但由于许多人试图使用lodash/chunk来解决此问题,而且不会误解_.chunk实际上的功能,因此这里提供了一种简洁而准确的使用lodash的解决方案:

(与已接受的答案不同,即使originalArray.length<numCols,这也保证了n列)

import _chunk from 'lodash/chunk'

/**
 * Split an array into n subarrays (or columns)
 * @param  {Array} flatArray Doesn't necessarily have to be flat, but this func only works 1 level deep
 * @param  {Number} numCols   The desired number of columns
 * @return {Array}
 */
export function splitArray(flatArray, numCols){
  const maxColLength = Math.ceil(flatArray.length/numCols)
  const nestedArray = _chunk(flatArray, maxColLength)
  let newArray = []
  for (var i = 0; i < numCols; i++) {
    newArray[i] = nestedArray[i] || []
  }
  return newArray
}

最后的 for 循环保证了所需的“列数”。

当数组长度为4且numCols为3时,此方法会失败。尝试使用splitArray([1, 2, 3, 4], 3)进行测试,它返回[[1, 2],[3, 4],[]]。 - Pratik Kulshreshth
你说得完全正确,@PratikKulshreshth。我会更新答案。对于任何感兴趣的人,我现在最喜欢Senthe的答案:https://dev59.com/iGsy5IYBdhLWcg3w-i_h#51514813 - Joao

7

通常来说,变异是个坏事™。

这是简洁、干净且幂等的。

function partition(list = [], n = 1) {
  const isPositiveInteger = Number.isSafeInteger(n) && n > 0;
  if (!isPositiveInteger) {
    throw new RangeError('n must be a positive integer');
  }

  const partitions = [];
  const partitionLength = Math.ceil(list.length / n);

  for (let i = 0; i < list.length; i += partitionLength) {
    const partition = list.slice(i, i+partitionLength);
    partitions.push( partition );
  }

  return partitions;
}
<编辑以添加> 这里还有一种变体,调用者指定分区大小而不是要创建的分区数:
function partition(list = [], n = 1) {
  const isPositiveInteger = Number.isSafeInteger(n) && n > 0;
  if (!isPositiveInteger) {
    throw new RangeError('n must be a positive integer');
  }

  const partitions = [];

  for (let i = 0; i < list.length; i += n) {
    const partition = list.slice(i, i+n);
    partitions.push( partition );
  }

  return partitions;
}

如果你希望每个块之间的长度差不超过1,使其“平衡”,那只需要进行一些简单的数学计算。

为了以这种方式将M件物品分配到N个桶中,我们首先需要确定M / N的商Q和余数R。

让Q表示基本的分区长度。R总是小于N,它是需要在所有分区之间分配的多余项的数量。因此,前R个分区将包含Q + 1个项目,而其余的分区将包含Q个项目。

例如,要将100个项目的列表分成8个桶,我们得到:

M = 10 N = 8 Q = 12 R = 4

所以我们会得到:

  • 4个(R)由Q + 1(13)个项目组成的桶,以及
  • 4个(N-R)由Q(12)个项目组成的桶

4 * 13 + 4 * 12 可以化简为 52+48,即100。

这就导致了这个结果:

function partition(list = [], n = 1) {
  const isPositiveInteger = Number.isSafeInteger(n) && n > 0;
  if (!isPositiveInteger) {
    throw new RangeError('n must be a positive integer');
  }

  const q = Math.floor( list.length / n );
  const r = list.length % n;

  let i   ; // denotes the offset of the start of the slice
  let j   ; // denotes the zero-relative partition number
  let len ; // denotes the computed length of the slice

  const partitions = [];
  for ( i=0, j=0, len=0; i < list.length; i+=len, ++j ) {
    len = j < r ? q+1 : q ;
    const partition = list.slice( i, i+len ) ; 
    partitions.push( partition ) ;
  }

  return partitions;
}

你的代码很棒!我们需要传递我们需要将数组分成多少块,但是指定每个块需要多少元素的方式需要自己计算,即 partition(arr, Math.round(arr.length / n)),其中 arr 是实际的数组,n 是每个块允许的最大元素数。 - Azzaz

6
如果您事先知道要使用的数据块大小,可以使用一种非常优雅的ES6方法来实现:

const groupsOfFour = ([a,b,c,d, ...etc]) =>
  etc.length? [[a,b,c,d], ...groupsOfFour(etc)] : [[a,b,c,d]];
  
console.log(groupsOfFour([1,2,3,4,1,2,3,4,1,2,3,4]));

我发现这种符号对于例如从Uint8ClampedArray中解析RGBA非常有用。


1
除了 n < 4 的情况失败:groupsOfFour( [ 1 ] ) 返回 [ 1, undefined, undefined, undefined ], 而不是期望的 (也是想要的) [ [1] ] - Nicholas Carey
你想让groupsOfFour返回一个只有一个成员的组吗?也许你使用了错误的函数。 - user1034533

3
function splitArray(arr, numOfParts = 10){
        const splitedArray = []
        for (let i = 0; i < numOfParts;i++) {
            const numOfItemsToSplice = arr.length / numOfParts;
            splitedArray.push(arr.splice(0, numOfItemsToSplice))
        }
        return splitedArray;
    }

3

另一种递归方法效果不错,而且看起来更简洁。

function nSmaller(num, arr, sliced) {

    var mySliced = sliced || [];
    if(num === 0) {
        return sliced;
    }

    var len = arr.length,
        point = Math.ceil(len/num),
        nextArr = arr.slice(point);

    mySliced.push(arr.slice(0, point));
    nSmaller(num-1, nextArr, mySliced);

    return(mySliced);
}

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