Dividir una matriz JS en N matrices

81

Imagina que tengo una matriz JS como esta:

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

Lo que quiero es dividir esa matriz en N matrices más pequeñas. Por ejemplo:

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

Para Python, tengo esto:

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

Para JS, la mejor solución correcta que se me ocurrió es una función recursiva, pero no me gusta porque es complicada y fea. Esta función interna devuelve una matriz como esta [1, 2, 3, nula, 4, 5, 6, nula, 7, 8], y luego tengo que repetirla y dividirla manualmente. (Mi primer intento fue devolver esto: [1, 2, 3, [4, 5, 6, [7, 8, 9]]], y decidí hacerlo con el separador nulo).

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

Aquí hay un jsfiddle de eso: http://jsfiddle.net/uduhH/

¿Cómo lo harías tú? ¡Gracias!

Tiago
fuente
2
relacionado con - stackoverflow.com/q/40166199/104380
vsync
1
Tu splitfunción no está lejos. Puede eliminar la nullempresa agregando dos envoltorios de matriz: if (cols == 1) return [array]y return [array.slice(0, size)].concat(split(array.slice(size), cols-1)). Encuentro esta versión recursiva mucho más legible que la mayoría de las respuestas aquí.
Scott Sauyet

Respuestas:

133

Puede hacer que los cortes estén "equilibrados" (las longitudes de los subarreglos difieren lo menos posible) o "pares" (todos los subarreglos, pero el último tienen la misma longitud):

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>

georg
fuente
Su solución es ordenada, hace lo mismo que hace mi solución recursiva pero sin todo ese lío. ¡Gracias!
Tiago
3
Funciona como un encanto .. Buena solución
Vardan
Hola @georg, ¿podría explicar esta línea: var size = Math.ceil((len - i) / n--);
dpg5000
@ dpg5000: al cortar el siguiente fragmento, su tamaño es el número de elementos restantes ( len - i) por el número de fragmentos restantes ( n--)
georg
1
Hola @georg, gracias. ¿Cómo modificaría este código para asegurarme de que todos los subarreglos sean de la misma longitud, excepto el último subarreglo (a menos que, por supuesto, el divisor no dé como resultado un resto, por lo que todos los subarreglos serían iguales)? Agradezco cualquier ayuda.
dpg5000
18

Creo que de esta manera usar empalme es el más limpio:

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

Por ejemplo, para parts = 3, tomaría 1/3, luego 1/2 de la parte restante, luego el resto de la matriz. Math.ceilasegura que en caso de un número impar de elementos, irán a los primeros fragmentos.

(Nota: esto destruye la matriz inicial).

Senthe
fuente
16

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

bagbee
fuente
2
Esto no funciona como se esperaba para n = 5 y arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].
Tiago
1
esto no se divide en n subarreglos, simplemente en subarreglos de n longitud.
dpg5000
1
Agregue una explicación de por qué este código ayuda al OP. Esto ayudará a proporcionar una respuesta de la que los futuros espectadores puedan aprender. Consulte Cómo responder para obtener más información.
Heretic Monkey
12

Acabo de realizar una implementación iterativa del algoritmo: http://jsfiddle.net/ht22q/ . Pasa tus casos de prueba.

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;
}
pimvdb
fuente
1
(Esto funciona como se esperaba, pero solo puedo elegir una respuesta como correcta) ¡Hola! Gracias por ayudar. Es bueno pensar en cómo usar el resto.
Tiago
7

Puedes reducirlo a una matriz. El siguiente ejemplo divide la matriz ( arr) en una matriz de matrices de dos posiciones. Si desea otros tamaños, simplemente cambie el valor 2 en la segunda línea:

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

¡Espero eso ayude!

EDITAR: Debido a que algunas personas todavía están comentando, esto no responde a la pregunta ya que estaba arreglando el tamaño de cada fragmento en lugar del número de fragmentos que quiero. Aquí viene el código que explica lo que trato de explicar en la sección de comentarios: Usando el 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>') }

sospedra
fuente
2
¡Vaya manera agradable y concisa de hacer esto! ¡Quiéralo! ¡Gran trabajo! :-)
Philippe Monnet
1
Amo esta técnica, pero no responde la pregunta. Devuelve cualquier cantidad de fragmentos de tamaño x, mientras que las preguntas pedían x cantidad de fragmentos de tamaño uniforme.
Jodi Warren
3
Me gusta esto !!! He refactorizado para devolver trozos de tamaño uniforme 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
definitivamente no responde a la pregunta.
macdelacruz
1
Conciso y realmente inteligente, mi forma preferida de resolver este y otros casos con este patrón simple, ¡gracias!
Edmond Tamas
4

Actualización: 21/7/2020

La respuesta que di hace unos años solo funciona si originalArray.length<= numCols. Alternativamente, podría usar algo como esta función a continuación, pero eso creará un diseño que no coincide con la pregunta en cuestión (clasificación horizontal en lugar de clasificación vertical). AKA: [1,2,3,4]-> [[1,4],[2],[3]]. Entiendo que esto aún podría proporcionar valor, así que lo dejaré aquí, pero recomiendo la respuesta de 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
}

Respuesta original de 2017:

Pregunta antigua, pero dado que vanillaJS no es un requisito y muchos están tratando de resolver esto con lodash / chunk, y sin confundir lo que _.chunkrealmente hace, aquí hay una solución concisa y precisa usando lodash:

(A diferencia de la respuesta aceptada, esto también garantiza n columnas incluso si 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
}

El forbucle al final es lo que garantiza el número deseado de "columnas".

Joao
fuente
Esto falla cuando la longitud de la matriz es 4 y numCols es 3. Se intentó con splitArray ([1, 2, 3, 4], 3) y devuelve [[1, 2], [3, 4], []].
Pratik Kulshreshth
Tienes toda la razón @PratikKulshreshth. Actualizaré la respuesta. Para cualquier interesado, me gusta la respuesta de Senthe la mejor ahora: stackoverflow.com/a/51514813/1322810
Joao
2

Enfoque recursivo, no probado.

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);
    }
}
12000
fuente
2

Otro recursivo funciona bastante bien, es menos feo

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);
}
Mcs
fuente
2
function parseToPages(elements, pageSize = 8) {
    var result = [];
    while (elements.length) {
        result.push(elements.splice(0, pageSize));
    }
    return result;
}
Vitalii Kulyk
fuente
3
No se limite a descargar el código como respuesta. Agregue una explicación de cómo esto aborda el problema de la pregunta.
Mark Rotteveel
2

Si conoce el tamaño de los trozos que desea de antemano, hay una forma ES6 bastante elegante de hacer esto:

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

Encuentro esta notación bastante útil para, por ejemplo, analizar RGBA de un Uint8ClampedArray.

usuario1034533
fuente
Excepto que esto falla para n <4: groupsOfFour( [ 1 ] )devuelve [ 1 , undefined, undefined, undefined ], en lugar de lo esperado (y deseado) [ [1] ].
Nicholas Carey
1

Probablemente, el enfoque más limpio sería el siguiente (sin usar ninguna otra biblioteca):

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

He creado mi propia matriz que se va a fragmentar. Puedes encontrar el código aquí

También tenemos un método "chunk" en la biblioteca lodash que es de gran utilidad. Espero que ayude

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

Lo hice de esta manera, funciona ...

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;
}
juanmorschrott
fuente
0

verifique mi versión de esta matriz dividida

// 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;
};
hitesh upadhyay
fuente
0

si sabes que quieres establecer child_arrays.length, creo que esta solución es la mejor:

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

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

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

Anja Ishmukhametova
fuente
0

Si puede usar lodashy le gustaría un enfoque de programación funcional, esto es lo que se me ocurre:

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
} 
Wormie
fuente
0

todo lo anterior podría funcionar bien, pero ¿qué pasa si tiene una associativematriz con cadenas como claves?

objectKeys = Object.keys;

arraySplit(arr, n) {
    let counter = 0;
    for (const a of this.objectKeys(arr)) {
        this.arr[(counter%n)][a] = arr[a];
        counter++;
    }
}
lokers
fuente
0

Tengo uno que no altera la matriz original.

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
}

Ihsan Müjdeci
fuente
0

Puedes usar una función recursiva simple

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

Matías Sánchez Rivas
fuente
0
splitToChunks(arrayvar, parts) {
    let result = [];
    for (let i = parts; i > 0; i--) {
        result.push(arrayvar.splice(0, Math.ceil(arrayvar.length / i)));
    }
    return result;
}
Abhi Savadiya
fuente
0

La mutación es, en general, algo malo ™.

Esto es agradable, limpio e idempotente.

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;
}
Nicolás Carey
fuente
-1

Simplemente use la función chunk lodash 'para dividir la matriz en matrices más pequeñas https://lodash.com/docs#chunk ¡ Ya no es necesario jugar con los bucles!

George Herolyants
fuente
1
La pregunta pregunta cómo resolver esto usando vannila js, no usando bibliotecas js
TJ
3
Gracias por mencionar este tema. No sabía que Lodash tenía esto.
Tiago
9
Esto tampoco responde a la pregunta. Quiere N matrices, no matrices de N elementos.
mAAdhaTTah
-3

Si está usando lodash, puede lograrlo con bastante facilidad, como se muestra a continuación:

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]]
abhisekpaul
fuente
3
Esto está mal. _.chunkcrea matrices de N elementos, no de N matrices. Su ejemplo tendría una salida de 6 matrices con 2 elementos en cada una[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]]
excepto
esa es la pregunta original. por favor lea el comportamiento esperado en la pregunta.
abhisekpaul