Matriz de JavaScript rotar()


Me preguntaba cuál era la forma más eficiente de rotar un array JavaScript.

Me encontré con esta solución, donde un positivo n gira la matriz a la derecha, y un negativo n a la izquierda (-length < n < length) :

Array.prototype.rotateRight = function( n ) {
  this.unshift( this.splice( n, this.length ) )
}

Que luego se puede usar de esta manera:

var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
months.rotate( new Date().getMonth() )

Mi versión original anterior tiene un defecto, como señala Christoph en los comentarios a continuación, una versión correcta es (el retorno adicional permite encadenar):

Array.prototype.rotateRight = function( n ) {
  this.unshift.apply( this, this.splice( n, this.length ) )
  return this;
}

Hay un ¿una solución más compacta y/o más rápida, posiblemente en el contexto de un framework JavaScript? (ninguna de las versiones propuestas a continuación es más compacta o más rápida)

¿Hay algún framework JavaScript con un array rotar integrado? (Todavía no contestado por nadie)

Author: Victor, 2009-12-31

17 answers

Type-safe, versión genérica que muta el array:

Array.prototype.rotate = (function() {
    // save references to array functions to make lookup faster
    var push = Array.prototype.push,
        splice = Array.prototype.splice;

    return function(count) {
        var len = this.length >>> 0, // convert to uint
            count = count >> 0; // convert to int

        // convert count to value in range [0, len)
        count = ((count % len) + len) % len;

        // use splice.call() instead of this.splice() to make function generic
        push.apply(this, splice.call(this, 0, count));
        return this;
    };
})();

En los comentarios, Jean planteó la cuestión de que el código no admite la sobrecarga de push() y splice(). No creo que esto sea realmente útil (ver comentarios), pero una solución rápida (algo así como un truco, sin embargo) sería reemplazar la línea

push.apply(this, splice.call(this, 0, count));

Con este:

(this.push || push).apply(this, (this.splice || splice).call(this, 0, count));

Usar unshift() en lugar de push() es casi el doble de rápido en Opera 10, mientras que las diferencias en FF eran insignificantes; código:

Array.prototype.rotate = (function() {
    var unshift = Array.prototype.unshift,
        splice = Array.prototype.splice;

    return function(count) {
        var len = this.length >>> 0,
            count = count >> 0;

        unshift.apply(this, splice.call(this, count % len, len));
        return this;
    };
})();
 43
Author: Christoph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-07 19:59:46

EDIT: Véase también mi nueva respuesta con count argumento: https://stackoverflow.com/a/33451102

Puede utilizar push(), pop(), shift() y unshift() funciones:

function arrayRotateOne(arr, reverse){
  if(reverse)
    arr.unshift(arr.pop())
  else
    arr.push(arr.shift())
  return arr
} 

Uso:

arrayRotate(['h','e','l','l','o']);       // ['e','l','l','o','h'];
arrayRotate(['h','e','l','l','o'], true); // ['o','h','e','l','l'];
 89
Author: Yukulélé,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-10 14:26:18

Probablemente haría algo como esto:

Array.prototype.rotate = function(n) {
    return this.slice(n, this.length).concat(this.slice(0, n));
}

Edit Aquí hay una versión mutante:

Array.prototype.rotate = function(n) {
    while (this.length && n < 0) n += this.length;
    this.push.apply(this, this.splice(0, n));
    return this;
}
 24
Author: Gumbo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-31 19:36:45

Esta función funciona en ambos sentidos y funciona con cualquier número (incluso con un número mayor que la longitud de la matriz):

function arrayRotate(arr, count) {
  count -= arr.length * Math.floor(count / arr.length)
  arr.push.apply(arr, arr.splice(0, count))
  return arr
}

Ejemplo:

for(let i = -6 ; i <= 6 ; i++)
  console.log( arrayRotate( ["H","e","l","l","o"], i).join(''), i )

Resultado:

"oHell", -6
"Hello", -5
"elloH", -4
"lloHe", -3
"loHel", -2
"oHell", -1
"Hello",  0
"elloH",  1
"lloHe",  2
"loHel",  3
"oHell",  4
"Hello",  5
"elloH",  6
 15
Author: Yukulélé,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-13 12:33:35

Muchas de estas respuestas parecen demasiado complicadas y difíciles de leer. No creo haber visto a nadie usando splice con Concat...

function rotateCalendar(){
    var cal=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
    cal=cal.concat(cal.splice(0,new Date().getMonth()));
    console.log(cal);  // return cal;
}

Consola.salidas de registro (*generadas en mayo):

["May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr"]

En cuanto a la compacidad, puedo ofrecer un par de funciones genéricas de un solo revestimiento (sin contar la consola.porción de registro / retorno). Simplemente alimente la matriz y el valor de destino en los argumentos.

Combino estas funciones en una para un programa de juego de cartas de cuatro jugadores donde el array es ['N','E','S','W']. Los dejé separados en caso de que alguien quiera copiar / pegar para sus necesidades. Para mis propósitos, utilizo las funciones cuando busco cuyo turno está al lado de jugar / actuar durante las diferentes fases del juego (Pinochle). No me he molestado en probar la velocidad, así que si alguien más quiere, no dude en hacerme saber los resultados.

*aviso, la única diferencia entre las funciones es el "+1".

function rotateToFirst(arr,val){  // val is Trump Declarer's seat, first to play
    arr=arr.concat(arr.splice(0,arr.indexOf(val)));
    console.log(arr); // return arr;
}
function rotateToLast(arr,val){  // val is Dealer's seat, last to bid
    arr=arr.concat(arr.splice(0,arr.indexOf(val)+1));
    console.log(arr); // return arr;
}

Combinación función...

function rotateArray(arr,val,pos){
    // set pos to 0 if moving val to first position, or 1 for last position
    arr=arr.concat(arr.splice(0,arr.indexOf(val)+pos));
    return arr;
}
var adjustedArray=rotateArray(['N','E','S','W'],'S',1);

AdjustedArray =

W,N,E,S
 5
Author: mickmackusa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-30 04:22:00

@Christoph, has hecho un código limpio, pero 60% más lento que este que encontré. Mira el resultado en jsPerf : http://jsperf.com/js-rotate-array/2 [Editar] OK ahora hay más navegadores y que no métodos de bruja obvias la mejor

var rotateArray = function(a, inc) {
    for (var l = a.length, inc = (Math.abs(inc) >= l && (inc %= l), inc < 0 && (inc += l), inc), i, x; inc; inc = (Math.ceil(l / inc) - 1) * inc - l + (l = inc))
    for (i = l; i > inc; x = a[--i], a[i] = a[i - inc], a[i - inc] = x);
    return a;
};

var array = ['a','b','c','d','e','f','g','h','i'];

console.log(array);
console.log(rotateArray(array.slice(), -1)); // Clone array with slice() to keep original
 3
Author: molokoloco,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-22 22:47:58

Véase http://jsperf.com/js-rotate-array/8

function reverse(a, from, to) {
  --from;
  while (++from < --to) {
    var tmp = a[from];
    a[from] = a[to];
    a[to] = tmp;
  }
}

function rotate(a, from, to, k) {
  var n = to - from;
  k = (k % n + n) % n;
  if (k > 0) {
    reverse(a, from, from + k);
    reverse(a, from + k, to);
    reverse(a, from, to);
  }
}
 3
Author: Vic99999,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-06-16 16:34:26

La respuesta aceptada tiene el defecto de no ser capaz de manejar matrices más grandes que el tamaño de la pila de llamadas que depende de la sesión, pero debería ser de alrededor de 100~300K elementos. Por ejemplo, en la sesión actual de Chrome que probé fue 250891. En muchos casos es posible que ni siquiera sepa en qué tamaño podría crecer dinámicamente la matriz. Ese es un problema serio.

Para superar esta limitación, supongo que un método interesante es utilizar Array.prototype.map() y mapear los elementos reorganizando el índices de forma circular. Este método toma un argumento entero. Si este argumento es positivo, rotará en los índices crecientes y si es negativo en la dirección de los índices decrecientes. Esto solo tiene complejidad de tiempo O (n) y devolverá una nueva matriz sin mutar la que se llama mientras maneja millones de elementos sin ningún problema. Veamos cómo funciona;

Array.prototype.rotate = function(n) {
var len = this.length;
return !(n % len) ? this
                  : n > 0 ? this.map((e,i,a) => a[(i + n) % len])
                          : this.map((e,i,a) => a[(len - (len - i - n) % len) % len]);
};
var a = [1,2,3,4,5,6,7,8,9],
    b = a.rotate(2);
console.log(JSON.stringify(b));
    b = a.rotate(-1);
console.log(JSON.stringify(b));

En realidad después de haber sido criticado en dos asuntos de la siguiente manera;

  1. Hay no hay necesidad de un condicional para la entrada positiva o negativa, ya que revela una violación de SECO .usted podría hacer esto con un mapa porque cada n negativo tiene un equivalente positivo (Totalmente correcto..)
  2. Una función de matriz debe cambiar la matriz actual o hacer una nueva matriz, su función podría hacerlo dependiendo de si un cambio es necesario o no (Totalmente correcto..)

He decidido modificar el código de la siguiente manera;

Array.prototype.rotate = function(n) {
var len = this.length;
return !(n % len) ? this.slice()
                  : this.map((e,i,a) => a[(i + (len + n % len)) % len]);
};
var a = [1,2,3,4,5,6,7,8,9],
    b = a.rotate(10);
console.log(JSON.stringify(b));
    b = a.rotate(-10);
console.log(JSON.stringify(b));

Entonces otra vez; de por supuesto, los funtores JS como Array.prototype.map() son lentos en comparación con sus equivalentes codificados en JS simple. Para obtener más del 100% de aumento de rendimiento, lo siguiente probablemente sería mi elección de Array.prototype.rotate() si alguna vez necesito rotar una matriz en código de producción como la que usé en mi intento de String.prototype.diff()

Array.prototype.rotate = function(n){
  var len = this.length,
      res = new Array(this.length);
  if (n % len === 0) return this.slice();
  else for (var i = 0; i < len; i++) res[i] = this[(i + (len + n % len)) % len];
  return res;
};
 2
Author: Redu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-04 09:01:30

Aquí hay una forma muy sencilla de cambiar elementos en un array:

function rotate(array, stepsToShift) {

    for (var i = 0; i < stepsToShift; i++) {
        array.unshift(array.pop());
    }

    return array;
}
 2
Author: Aryeh Harris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-06 18:32:25

Esta función es un poco más rápida que la respuesta aceptada para matrices pequeñas, pero mucho más rápida para matrices grandes. Esta función también permite un número arbitrario de rotaciones mayores que la longitud de la matriz, que es una limitación de la función original.

Por último, la respuesta aceptada gira en la dirección opuesta como se describe.

const rotateForEach = (a, n) => {
    const l = a.length;
    a.slice(0, -n % l).forEach(item => a.push( item ));
    return a.splice(n % l > 0 ? (-n % l) : l + (-n % l));
}

Y el equivalente funcional (que parece tener también algunos beneficios de rendimiento):

const rotateReduce = (arr, n) => {
    const l = arr.length;
    return arr.slice(0, -n % l).reduce((a,b) => {
        a.push( b );
        return a;
    }, arr).splice(n % l> 0 ? l + (-n % l) : -n % l);
};

Usted puede comprobar hacia fuera el desglose de rendimiento aquí.

 2
Author: Tanner Stults,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-18 04:42:26

Cuando no pude encontrar un fragmento listo para comenzar una lista de días con 'hoy', lo hice así (no del todo genérico, probablemente mucho menos refinado que los ejemplos anteriores, pero hice el trabajo):

//returns 7 day names with today first
function startday() {
    const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
    let today = new Date();
    let start = today.getDay(); //gets day number
    if (start == 0) { //if Sunday, days are in order
        return days
    }
    else { //if not Sunday, start days with today
        return days.slice(start).concat(days.slice(0,start))
    }
}

Gracias a un pequeño refactor de un programador mejor que yo, es una línea o dos más corta que mi intento inicial, pero cualquier comentario adicional sobre la eficiencia es bienvenido.

 2
Author: Dave Everitt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-16 12:43:02

Qué tal incrementar un contador y luego obtener el resto de una división por la longitud de la matriz para llegar a donde se supone que debe estar.

var i = 0;
while (true);
{
    var position = i % months.length;
    alert(months[position]);
    ++i;
}

Sintaxis de lenguaje aparte esto debería funcionar bien.

 0
Author: tgandrews,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-31 13:40:57

Si su matriz va a ser grande y/o va a rotar mucho, es posible que desee considerar el uso de una lista vinculada en lugar de una matriz.

 0
Author: Thomas Eding,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-01-01 10:10:12

@molokoloco Necesitaba una función que pudiera configurar para girar en una dirección - true para adelante y false para atrás. He creado un fragmento que toma una dirección, un contador y una matriz y produce un objeto con el contador incrementado en la dirección adecuada, así como los valores anteriores, actuales y siguientes. NO modifica la matriz original.

También lo sincronicé con tu fragmento y aunque no es más rápido, es más rápido que los que comparas con los tuyos - 21% más lento http://jsperf.com/js-rotate-array/7 .

function directionalRotate(direction, counter, arr) {
  counter = direction ? (counter < arr.length - 1 ? counter + 1 : 0) : (counter > 0 ? counter - 1 : arr.length - 1)
  var currentItem = arr[counter]
  var priorItem = arr[counter - 1] ? arr[counter - 1] : arr[arr.length - 1]
  var nextItem = arr[counter + 1] ? arr[counter + 1] : arr[0]
  return {
    "counter": counter,
    "current": currentItem,
    "prior": priorItem,
    "next": nextItem
  }
}
var direction = true // forward
var counter = 0
var arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'];

directionalRotate(direction, counter, arr)
 0
Author: saranicole,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-04 18:46:12

Llego tarde pero tengo un ladrillo que añadir a estas buenas respuestas. Me pidieron que codificara tal función y primero lo hice:

Array.prototype.rotate = function(n)
{
    for (var i = 0; i < n; i++)
    {
        this.push(this.shift());
    }
    return this;
}

Pero parecía ser menos eficiente que seguir cuando n es grande:

Array.prototype.rotate = function(n)
{
    var l = this.length;// Caching array length before map loop.

    return this.map(function(num, index) {
        return this[(index + n) % l]
    });
}
 0
Author: antoni,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-15 02:12:58

EDIT:: Oye, resulta que hay demasiada iteración sucediendo. Sin bucles, sin ramificaciones.

Todavía funciona con n negativo para la rotación derecha y n positivo para la rotación izquierda para cualquier tamaño n, Libre de mutación

function rotate(A,n,l=A.length) {
  const offset = (((n % l) + l) %l)
  return A.slice(offset).concat(A.slice(0,offset))
}

Aquí está la versión de code golf para giggles

const r = (A,n,l=A.length,i=((n%l)+l)%l)=>A.slice(i).concat(A.slice(0,i))

EDIT1:: * Implementación sin ramas y sin mutaciones.

Bueno, resulta que tenía una rama donde no la necesitaba. Aquí hay una solución de trabajo. num negativo = rotación derecha by / num| num positivo = rotación izquierda por num

function r(A,n,l=A.length) {
  return A.map((x,i,a) => A[(((n+i)%l) + l) % l])
}

La ecuación ((n%l) + l) % l mapea exactamente números positivos y negativos de cualquier valor arbitrariamente grande de n

ORIGINAL

Gire a la izquierda y a la derecha. Rotar a la izquierda con positivo n, rotar a la derecha con negativo n.

Funciona para entradas obscenamente grandes de n.

No hay modo de mutación. Demasiada mutación en estas respuestas.

También, menos operaciones que la mayoría de las respuestas. No pop, no push, sin empalme, sin turno.

const rotate = (A, num ) => {
   return A.map((x,i,a) => {
      const n = num + i
      return n < 0 
        ? A[(((n % A.length) + A.length) % A.length)]
        : n < A.length 
        ? A[n] 
        : A[n % A.length]
   })
}

O

 const rotate = (A, num) => A.map((x,i,a, n = num + i) => 
  n < 0
    ? A[(((n % A.length) + A.length) % A.length)]
    : n < A.length 
    ? A[n] 
    : A[n % A.length])

//test
rotate([...Array(5000).keys()],4101)   //left rotation
rotate([...Array(5000).keys()],-4101000)  //right rotation, num is negative

// will print the first index of the array having been rotated by -i
// demonstrating that the rotation works as intended
[...Array(5000).keys()].forEach((x,i,a) => {
   console.log(rotate(a,-i)[0])
}) 
// prints even numbers twice by rotating the array by i * 2 and getting the first value
//demonstrates the propper mapping of positive number rotation when out of range
[...Array(5000).keys()].forEach((x,i,a) => {
   console.log(rotate(a,i*2)[0])
})

Explicación:

Asigna cada índice de A al valor en el desplazamiento del índice. En este caso

offset = num

Si el offset < 0 entonces offset + index + positive length of A apuntará al desplazamiento inverso.

Si offset > 0 and offset < length of A entonces simplemente mapee el índice actual al índice de desplazamiento de A.

De lo contrario, modulo el desplazamiento y la longitud para mapear el desplazamiento en los límites de la matriz.

Tomemos por ejemplo offset = 4 y offset = -4.

Cuando offset = -4, y A = [1,2,3,4,5], para cada índice, offset + index hará que la magnitud (o Math.abs(offset)) más pequeños.

Vamos a explicar el cálculo para el índice de n negativo primero. A[(((n % A.length) + A.length) % A.length)+0] y han sido intimidados. No lo estés. Me tomó 3 minutos en un Repl para resolverlo.

  1. Sabemos que n es negativo porque el caso es n < 0. Si el número es mayor que el rango del Array, n % A.length lo mapeará dentro del rango.
  2. n + A.length añadir ese número a A.length para compensar n el correcto cantidad.
  3. Sabemos que n es negativo porque el caso es n < 0. n + A.length añadir ese número a A.length para compensar n la cantidad correcta.
  4. El Siguiente Mapa para el rango de la longitud de Una utilizando un módulo. El segundo módulo es necesario para mapear el resultado del cálculo en un rango indexable

    introduzca la descripción de la imagen aquí

  5. Primer índice: -4 + 0 = -4. A. longitud = 5. A. longitud - 4 = 1. A2 is 2. Índice del mapa 0 a 2. [2,... ]

  6. Siguiente índice, -4 + 1 = -3. 5 + -3 = 2. A2 es 3. Índice del mapa 1 al 3. [2,3... ]
  7. Etc.

El mismo proceso se aplica a offset = 4. Cuando offset = -4, y A = [1,2,3,4,5], para cada índice, offset + index hará la magnitud más grande.

  1. 4 + 0 = 0. Asigne A [0] al valor A[4]. [5...]
  2. 4 + 1 = 5, 5 está fuera de los límites al indexar, por lo que mapear A2 a la valor en el resto de 5 / 5, que es 0. A2 = valor a A [0]. [5,1...]
  3. repita.
 0
Author: nathan rogers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-28 06:12:41

Usando el spread de ES6 para un ejemplo inmutable ...

[...array.slice(1, array.length), array[0]]

Y

[array[array.items.length -1], ...array.slice(0, array.length -1)]

Probablemente no sea el más eficiente, pero es conciso.

 0
Author: Dudley Craig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-10-04 11:42:52