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

假设我有一个这样的 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]]] ,我决定使用空分隔符)。

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

下面是一个简单的例子: http://jsfiddle.net/uduhH/

你怎么做到的,谢谢!

105081 次浏览

I just made an iterative implementation of the algorithm: http://jsfiddle.net/ht22q/. It passes your test cases.

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

You can make the slices "balanced" (subarrays' lengths differ as less as possible) or "even" (all subarrays but the last have the same length):

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>

Recursive approach, not tested.

function splitArray(array, parts, out) {
var
len = array.length
, partLen


if (parts < len) {
partLen = Math.ceil(len / parts);
out.push(array.slice(0, partLen));
if (parts > 1) {
splitArray(array.slice(partLen), parts - 1, out);
}
} else {
out.push(array);
}
}

I made it this way, it works...

function splitArray(array, parts) {
if (parts< array.length && array.length > 1 && array != null) {
var newArray = [];
var counter1 = 0;
var counter2 = 0;


while (counter1 < parts) {
newArray.push([]);
counter1 += 1;
}


for (var i = 0; i < array.length; i++) {
newArray[counter2++].push(array[i]);
if (counter2 > parts - 1)
counter2 = 0;
}


return newArray;
} else
return array;
}

Another recursive works quite well, it is less ugly

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

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

You can reduce it into a matrix. The example below split the array (arr) into a matrix of two-positions arrays. If you want other sizes just change the 2 value on the second line:

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

Hope it helps!

EDIT: Because some people is still commenting this doesn't answer the question since I was fixing the size of each chunk instead of the number of chunks I want. Here it comes the code explaining what I'm trying to explain in the comments section: Using the 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>') }

check my version of this array split

// divide array
Array.prototype.divideIt = function(d){
if(this.length <= d) return this;
var arr = this,
hold = [],
ref = -1;
for(var i = 0; i < arr.length; i++){
if(i % d === 0){
ref++;
}
if(typeof hold[ref] === 'undefined'){
hold[ref] = [];
}
hold[ref].push(arr[i]);
}


return hold;
};

if you know wanna set child_arrays.length then i think this solution best:

function sp(size, arr){ //size - child_array.length
var out = [],i = 0, n= Math.ceil((arr.length)/size);
while(i < n) { out.push(arr.splice(0, (i==n-1) && size < arr.length ? arr.length: size));  i++;}
return out;
}

call fn: sp(2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) //2 - child_arrat.length

answer: [1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]

Just use lodash' chunk function to split the array into smaller arrays https://lodash.com/docs#chunk No need to fiddle with the loops anymore!

Probably the cleaner approach would be the following (without using any other library) :

var myArray = [];
for(var i=0; i<100; i++){
myArray.push(i+1);
}
console.log(myArray);


function chunk(arr, size){
var chunkedArr = [];
var noOfChunks = Math.ceil(arr.length/size);
console.log(noOfChunks);
for(var i=0; i<noOfChunks; i++){
chunkedArr.push(arr.slice(i*size, (i+1)*size));
}
return chunkedArr;
}


var chunkedArr = chunk(myArray, 3);
console.log(chunkedArr);

I have created my own array which is to be chunked. You can find the code here

Also we have a method "chunk" in the lodash library which is of great use. Hope that helps

If you are using lodash, you can achieve it fairly easily like below:

import {chunk} from 'lodash';
// divides the array into 2 sections
chunk([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 2); // => [[1,2,3,4,5,6], [7,8,9,10,11]]

Update: 7/21/2020

The answer I've given a few years ago only works if originalArray.length <= numCols. You could alternatively use something like this function below, but that will create a layout that doesn't quite match the question at hand (horizontal sorting rather than vertical sorting). AKA: [1,2,3,4] -> [[1,4],[2],[3]]. I understand this might still provide value so I'll leave this here, but I recommend Senthe's answer.

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
}

Original Answer from 2017:

Old question, but since vanillaJS is not a requirement and so many are trying to solve this with lodash/chunk, and without mistaking what _.chunk actually does, here's a concise + accurate solution using lodash:

(Unlike the accepted answer, this also guarantees n columns even if originalArray.length < numCols)

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
}

The for loop at the end is what guarantees the desired number of "columns".

If you can use lodash and would like a functional programming approach, here is what I come up with:

const _ = require('lodash')


function splitArray(array, numChunks) {
return _.reduce(_.range(numChunks), ({array, result, numChunks}, chunkIndex) => {
const numItems = Math.ceil(array.length / numChunks)
const items = _.take(array, numItems)
result.push(items)
return {
array: _.drop(array, numItems),
result,
numChunks: numChunks - 1
}
}, {
array,
result: [],
numChunks
}).result
}

all above might work fine, but what if you have associative array with strings as keys?

objectKeys = Object.keys;


arraySplit(arr, n) {
let counter = 0;
for (const a of this.objectKeys(arr)) {
this.arr[(counter%n)][a] = arr[a];
counter++;
}
}
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;
}

I think this way using splice is the cleanest:

function splitToChunks(array, parts) {
let result = [];
for (let i = parts; 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(splitToChunks([...example], 3))
console.log(splitToChunks([...example], 5))

For example, for parts = 3, you would take 1/3, then 1/2 of the remaining part, then the rest of the array. Math.ceil ensures that in case of uneven number of elements they will go to the earliest chunks.

(Note: calling .splice on an array will directly change its length. To avoid destroying your initial array, you can use its temporary shallow copy instead: const copiedArray = [ ...originalArray ])

I have one that doesn't alter original array

function splitArray(array = [], nPieces = 1){
const splitArray = [];
let atArrPos = 0;
for(let i = 0; i < nPieces; i++){
const splitArrayLength  = Math.ceil((array.length - atArrPos)/ (nPieces - i));
splitArray.push([]);
splitArray[i] = array.slice(atArrPos, splitArrayLength + atArrPos);
atArrPos += splitArrayLength;
}
return  splitArray
}

If you happen to know the size of the chunks you want beforehand, there's a pretty elegant ES6 way of doing this:

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

I find this notation pretty useful for, for example parsing RGBA out of a Uint8ClampedArray.

You can use a simple recursive function

const chunkify = (limit, completeArray, finalArray = [])=>{
if(!completeArray.length) return finalArray
const a = completeArray.splice(0,limit);
return chunkify(limit, completeArray, [...finalArray,a])
}

splitToChunks(arrayvar, parts) {
let result = [];
for (let i = parts; i > 0; i--) {
result.push(arrayvar.splice(0, Math.ceil(arrayvar.length / i)));
}
return result;
}

Mutation is, generally speaking, a Bad Thing™.

This is nice, clean, and idempotent.

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

[Edited to add]

Here's another flavor where the caller specifies the partition size rather than the number of partitions to be created:

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

And if you want that to be "balanced" such that the individual chunks will differ in length by no more than 1, that only requires a little math.

To distribute, say M things into N buckets in that manner, we need to first determine the quotient Q and remainder R of M / N.

Let Q denote the basic partition length. R will always be less than N, and is the number of excess items that need to be distributed across all the partitions. Ergo, the first R partitions will contain Q+1 items and the remaining partitions will contain Q items.

For example, to partition a list of 100 items into 8 buckets, we get:

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

So we will get:

  • 4 (R) buckets of Q+1 (13) items, and
  • 4 (N-R) buckets of Q (12) items

And 4 * 13 + 4 * 12 reduces to 52+48, or 100.

That leads us to this:

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

const partition = (x,n) => {
const p=x.length%n, q=Math.ceil(x.length/n), r=Math.floor(x.length/n);
return [...Array(n)].reduce((a,_,i)=>(a[0].push(x.slice(a[1],(a[1]+=i<p?q:r))),a),[[],0])[0];
};

DEMO

// to make it consistent to filter pass index and array as arguments
const partition = (x,n) => {
const p = x.length % n,q = Math.ceil(x.length / n),r = Math.floor(x.length / n);
return [...Array(n)].reduce((a,_,i)=>(a[0].push(x.slice(a[1],(a[1]+=i<p?q:r))),a),[[],0])[0];
};


console.log(partition([], 3))
console.log(partition([1, 2], 3))
console.log(partition([1, 2, 3, 4, 5, 6, 7, 8, 9], 3))
console.log(partition([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3))

For Typescript

const partition = <T>(x: T[], n: number) => {
const p = x.length % n, q = Math.ceil(x.length / n), r = Math.floor(x.length / n);
return [...Array(n) as never[]].reduce((a, _, i) =>
(a[0].push(x.slice(a[1], a[1] += i < p ? q : r)), a)
, [[], 0] as [T[][], number])[0]
}

ONE-LINER Partition (but different order)

const part=(x,n)=>x.reduce((a,v,i)=>(a[i%n].push(v),a),[...Array(n)].map(()=>[]));

DEMO

// to make it consistent to filter pass index and array as arguments
const part=(x,n)=>x.reduce((a,v,i)=>(a[i%n].push(v),a),[...Array(n)].map(()=>[]));


console.log(part([1, 2, 3, 4, 5], 3));
console.log(part([1, 2, 3, 4, 5, 6], 3));
console.log(part([1, 2], 3));

For Typescript

const part = <T>(array: T[], parts: number) =>
array.reduce(
(acc, value, i) => (acc[i % parts].push(value), acc),
[...Array(parts)].map(() => []) as T[][]
);