web-dev-qa-db-fra.com

Rotation de tableau JavaScript ()

Je me demandais quel était le moyen le plus efficace de faire pivoter un JavaScript array.

Je suis arrivé avec cette solution, où une n positive fait pivoter le tableau vers la droite, et une n négative vers la gauche (-length < n < length):

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

Ce qui peut alors être utilisé de cette façon:

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

Ma version originale ci-dessus a un défaut, comme l'a souligné Christoph dans les commentaires ci-dessous, une version correcte est (le retour supplémentaire permet l'enchaînement):

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

Existe-t-il une solution plus compacte et/ou plus rapide, éventuellement dans le contexte d’un framework JavaScript? (aucune des versions proposées ci-dessous n'est plus compacte ni plus rapide)

Existe-t-il un framework JavaScript avec une rotation de tableau intégrée? (Toujours pas répondu par personne)

56
Jean Vincent

Type, version générique qui mute le tableau:

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

Dans les commentaires, Jean a soulevé le problème suivant: le code ne prend pas en charge la surcharge de Push() et splice(). Je ne pense pas que cela soit vraiment utile (voir commentaires), mais une solution rapide (un peu un bidouillage, cependant) serait de remplacer la ligne

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

avec celui-ci:

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

Utiliser unshift() au lieu de Push() est presque deux fois plus rapide dans Opera 10, alors que les différences de FF étaient négligeables; le code:

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;
    };
})();
46
Christoph

Vous pouvez utiliser les fonctions Push(), pop(), shift() et unshift():

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

usage:

arrayRotate(['h','e','l','l','o']);       // ['e','l','l','o','h'];
arrayRotate(['h','e','l','l','o'], true); // ['o','h','e','l','l'];

Si vous avez besoin de l'argument count, consultez mon autre réponse: https://stackoverflow.com/a/33451102

101
Yukulélé

Je ferais probablement quelque chose comme ça:

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

Edit Voici une version mutatrice:

Array.prototype.rotate = function(n) {
    while (this.length && n < 0) n += this.length;
    this.Push.apply(this, this.splice(0, n));
    return this;
}
29
Gumbo

Cette fonction fonctionne dans les deux sens et avec n'importe quel nombre (même si le nombre est supérieur à la longueur du tableau):

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

exemple:

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

résultat:

"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
16
Yukulélé

Tant de réponses semblent trop compliquées et difficiles à lire ... Je ne crois pas avoir vu quelqu'un utiliser l'épissure avec du 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;
}

sorties console.log (* générées en mai):

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

En ce qui concerne la compacité, je peux proposer quelques fonctions génériques à une ligne (sans compter la partie console.log | return). Il suffit de nourrir le tableau et la valeur cible dans les arguments.

Je combine ces fonctions en une seule pour un programme de jeu de cartes à quatre joueurs où le tableau est ['N', 'E', 'S', 'W']. Je les ai laissés séparés au cas où quelqu'un voudrait copier/coller pour leurs besoins. Pour mes besoins, j'utilise les fonctions lorsque je cherche le tour suivant de jouer/jouer durant les différentes phases du jeu (Pinochle). Je n'ai pas pris la peine de tester la vitesse, alors si quelqu'un d'autre le souhaite, n'hésitez pas à me le faire savoir.

* remarque, la seule différence entre les fonctions est le "+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;
}

fonction de combinaison ...

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

tableau ajusté =

W,N,E,S
5
mickmackusa

voir 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
Vic99999

@Christoph, vous avez fait un code propre, mais 60% plus lent que celui-ci que j'ai trouvé. Regardez le résultat sur jsPerf: http://jsperf.com/js-rotate-array/2 [Edit] OK, il y a maintenant plus de navigateurs que de méthodes non évidentes. 

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
molokoloco

Quand je ne pouvais pas trouver d'extrait prêt à l'emploi pour commencer une liste de jours avec 'aujourd'hui', je l'ai fait comme ceci (pas tout à fait générique, probablement beaucoup moins raffiné que les exemples ci-dessus, mais j'ai fait le travail):

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

Merci à un petit refactor par un meilleur programmeur que moi, c'est une ligne ou deux plus courte que ma tentative initiale, mais tout commentaire supplémentaire sur l'efficacité est le bienvenu.

2
Dave Everitt

La réponse acceptée a le défaut de ne pas pouvoir gérer des tableaux plus grands que la taille de la pile d’appel, qui dépend de la session mais doit se situer autour de 100 ~ 300K éléments. Par exemple, dans la session Chrome actuelle que j’ai essayée, il s’agissait de 250891. Dans de nombreux cas, vous pouvez même ne pas savoir à quelle taille le tableau peut se développer de manière dynamique. C'est donc un problème grave.

Pour surmonter cette limitation, je suppose qu'une méthode intéressante consiste à utiliser Array.prototype.map() et à cartographier les éléments en réorganisant les indices de manière circulaire. Cette méthode prend un argument entier. Si cet argument est positif, il alternera avec les indices croissants et, s'il est négatif, avec une direction décroissante. Cela n'a que O(n) temps complexe et renverra un nouveau tableau sans muter celui auquel il est appelé tout en manipulant des millions d'éléments sans aucun problème. Voyons comment cela fonctionne.

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 fait, après avoir été critiqué pour deux raisons,

  1. Aucune condition pour les entrées positives ou négatives n'est nécessaire car elle révèle une violation de DRY. Vous pouvez le faire avec une carte, car chaque négatif n a un équivalent positif (tout à fait ..).
  2. Une fonction de tableau doit soit changer le tableau actuel, soit en créer un nouveau. Votre fonction peut le faire en fonction de la nécessité ou non d'un décalage (Totalement à droite ..)

J'ai décidé de modifier le code comme suit;

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

Puis encore; Bien entendu, les foncteurs JS comme Array.prototype.map() sont lents par rapport à leurs équivalents codés en JS simple. Pour gagner plus de 100% de performances, voici mon choix de Array.prototype.rotate() si je dois faire pivoter un tableau dans le code de production comme celui que j'ai utilisé lors de ma tentative sur 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
Redu

Cette fonction est un peu plus rapide que la réponse acceptée pour les petits tableaux, mais BEAUCOUP plus rapide pour les grands tableaux. Cette fonction permet également un nombre arbitraire de rotations supérieur à la longueur du tableau, ce qui constitue une limitation de la fonction d'origine.

Enfin, la réponse acceptée tourne dans le sens opposé, comme décrit.

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

Et l’équivalent fonctionnel (qui semble également présenter des avantages en termes de performances):

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

Vous pouvez consulter la ventilation performance ici.

2
Tanner Stults

Voici un moyen très simple de déplacer des éléments dans un tableau:

function rotate(array, stepsToShift) {

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

    return array;
}
2
Aryeh Harris

Utiliser ES6's spread pour un exemple immuable ... 

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

et 

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

Ce n'est probablement pas le plus efficace, mais c'est concis.

1
Dudley Craig
Follow a simpler approach of running a loop to n numbers and shifting places upto that element.

function arrayRotateOne(arr, n) {
  for (let i = 0; i < n; i++) {
    arr.unshift(arr.pop());
  }
  return arr;
}
console.log( arrayRotateOne([1,2,3,4,5,6],2));



function arrayRotateOne(arr,n) {
  for(let i=0; i<n;i++){
      arr.Push(arr.shift());
      console.log('execute',arr)
    }
     return arr;
 }

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

1
Bhaskar Mishra

Solution non mutante

var arr = ['a','b','c','d']
arr.slice(1,arr.length).concat(arr.slice(0,1)

avec mutation

var arr = ['a','b','c','d']
arr = arr.concat(arr.splice(0,1))
0
Haseeb Eqx

@molokoloco J'avais besoin d'une fonction que je pouvais configurer pour effectuer une rotation dans une direction: true pour forward et false pour backward. J'ai créé un extrait qui prend une direction, un compteur et un tableau et génère un objet avec le compteur incrémenté dans la direction appropriée, ainsi que les valeurs antérieures, actuelles et suivantes. Il ne modifie pas le tableau d'origine.

Je l'ai également synchronisé avec votre extrait et bien qu'il ne soit pas plus rapide, il est plus rapide que ceux avec lesquels vous comparez le vôtre - 21% plus lent 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
saranicole

EDIT :: Hey, il s'avère qu'il y a trop d'itérations en cours. Pas de boucles, pas de branchement.

Fonctionne toujours avec n négatif pour la rotation à droite et n positif pour la rotation à gauche pour toute taille n, Mutation free

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

Voici la version de code de golf pour rire

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

EDIT1 :: * Implémentation sans branche, sans mutation.

Alors hé, il se trouve que j'avais une branche où je n'en avais pas besoin. Voici une solution de travail. négatif num = rotation à droite de | num | positif num = rotation à gauche de num

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

L'équation ((n%l) + l) % l mappe exactement les nombres positifs et négatifs de toute valeur arbitrairement grande de n

ORIGINAL

Tourner à gauche et à droite. Faites pivoter à gauche avec n positif, tournez à droite avec n négatif. 

Fonctionne pour des entrées obscènes de n

Aucun mode de mutation. Trop de mutation dans ces réponses.

En outre, moins d'opérations que la plupart des réponses. Pas de pop, pas de push, pas d'épissure, pas de décalage.

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

ou 

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

Explication: 

mapper chaque index de A à la valeur au décalage d'index. Dans ce cas

offset = num

si le offset < 0 puis offset + index + positive length of A pointera vers le décalage inverse. 

si offset > 0 and offset < length of A, mappez simplement l'index actuel sur l'index de décalage de A.

Sinon, modulo le décalage et la longueur pour mapper le décalage dans les limites du tableau. 

Prenons par exemple offset = 4 et offset = -4

Lorsque offset = -4 et A = [1,2,3,4,5], pour chaque index, offset + index réduira la magnitude (ou Math.abs(offset)). 

Expliquons le calcul de l'indice de n négatif en premier. A[(((n % A.length) + A.length) % A.length)+0] et été intimidé. Ne sois pas. Il m'a fallu 3 minutes en réplique pour résoudre le problème. 

  1. Nous savons que n est négatif car le cas est n < 0. Si le nombre est supérieur à la plage du tableau, n % A.length le mappera dans la plage. 
  2. n + A.length ajouter ce nombre à A.length pour compenser n le montant correct
  3. Nous savons que n est négatif car le cas est n < 0. n + A.length ajouter ce nombre à A.length pour compenser n le montant correct. 
  4. Next Mappez-le sur la plage de longueur de A en utilisant modulo. Le deuxième modulable est nécessaire pour mapper le résultat du calcul dans une plage indexable 

     enter image description here

  5. Premier indice: -4 + 0 = -4. A.length = 5. A.length - 4 = 1. A 2 est 2. Index de la carte 0 à 2. [2,... ]

  6. Index suivant, -4 + 1 = -3. 5 + -3 = 2. A 2 est 3. Index de la carte 1 à 3. [2,3... ]
  7. Etc.

Le même processus s'applique à offset = 4. Lorsque offset = -4 et A = [1,2,3,4,5], pour chaque index, offset + index augmentera la magnitude. 

  1. 4 + 0 = 0. Mappez A [0] à la valeur de A [4]. [5...]
  2. 4 + 1 = 5, 5 est hors limites lors de l'indexation, donc mappez A 2 sur la valeur du reste de 5 / 5, qui est 0. A 2 = valeur à A [0] . [5,1...]
  3. répéter.
0
nathan rogers

Je partage ma solution que j'utilise pour la rotation sur carrousel. Cela peut casser lorsque la taille du tableau est inférieure à displayCount, mais vous pouvez ajouter une condition supplémentaire pour arrêter la rotation lorsqu'elle est petite ou concaténer le tableau principal * fois le nombre de fois displayCount.

function rotate(arr, moveCount, displayCount) {
  const size = arr.length;

  // making sure startIndex is between `-size` and `size`
  let startIndex = moveCount % size;
  if (startIndex < 0) startIndex += size; 

  return [...arr, ...arr].slice(startIndex, startIndex + displayCount);
}

// move 3 to the right and display 4 items
// rotate([1,2,3,4,5], 3, 4) -> [4,5,1,2]

// move 3 to the left and display 4 items
// rotate([1,2,3,4,5], -3, 4) -> [3,4,5,1]

// move 11 to the right and display 4
// rotate([1,2,3,4,5], 3, 4) -> [2,3,4,5]
0
emil

Je ne suis pas sûr que ce soit le moyen le plus efficace, mais j'aime bien la façon dont il se lit, il est assez rapide pour la plupart des tâches volumineuses, car je l'ai testé en production ...

function shiftRight(array) {
  return array.map((_element, index) => {
    if (index === 0) {
      return array[array.length - 1]
    } else return array[index - 1]
  })
}

function test() {
  var input = [{
    name: ''
  }, 10, 'left-side'];
  var expected = ['left-side', {
    name: ''
  }, 10]
  var actual = shiftRight(input)

  console.log(expected)
  console.log(actual)

}

test()
0
Andy Gonzalez

J'arrive en retard mais j'ai une brique à ajouter à ces bonnes réponses… .. On m'a demandé de coder une telle fonction et je l'ai d'abord fait:

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

Mais cela semblait moins efficace que de suivre lorsque n est gros:

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
antoni

Pourquoi ne pas incrémenter un compteur, puis obtenir le reste d’une division par la longueur du tableau pour arriver où vous êtes supposé être.

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

La syntaxe de langage mise à part, cela devrait fonctionner correctement.

0
tgandrews

Si votre tableau doit être grand et/ou que vous allez effectuer beaucoup de rotations, vous pouvez envisager d'utiliser une liste chaînée au lieu d'un tableau.

0
Thomas Eding