JavaScript Array rotate()

最后都变了- 提交于 2019-12-24 13:21:14

问题


I was wondering what was the most efficient way to rotate a JavaScript array.

I came up with this solution, where a positive n rotates the array to the right, and a negative n to the left (-length < n < length) :

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

Which can then be used this way:

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

My original version above has a flaw, as pointed out by Christoph in the comments bellow, a correct version is (the additional return allows chaining):

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

Is there a more compact and/or faster solution, possibly in the context of a JavaScript framework? (none of the proposed versions bellow is either more compact or faster)

Is there any JavaScript framework out there with an array rotate built-in? (Still not answered by anyone)


回答1:


Type-safe, generic version which mutates the 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;
    };
})();

In the comments, Jean raised the issue that the code doesn't support overloading of push() and splice(). I don't think this is really useful (see comments), but a quick solution (somewhat of a hack, though) would be to replace the line

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

with this one:

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

Using unshift() instead of push() is nearly twice as fast in Opera 10, whereas the differences in FF were negligible; the 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;
    };
})();



回答2:


You can use push(), pop(), shift() and unshift() methods:

function arrayRotate(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'];

If you need count argument see my other answer: https://stackoverflow.com/a/33451102




回答3:


I would probably do something like this:

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

Edit    Here’s a mutator version:

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



回答4:


This function works in both way and works with any number (even with number greater than array length):

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

usage:

for(let i = -6 ; i <= 6 ; i++) {
  console.log(arrayRotate(["🧡","💚","💙","💜","🖤"], i), i);
}

result:

[ "🖤", "🧡", "💚", "💙", "💜" ]    -6
[ "🧡", "💚", "💙", "💜", "🖤" ]    -5
[ "💚", "💙", "💜", "🖤", "🧡" ]    -4
[ "💙", "💜", "🖤", "🧡", "💚" ]    -3
[ "💜", "🖤", "🧡", "💚", "💙" ]    -2
[ "🖤", "🧡", "💚", "💙", "💜" ]    -1
[ "🧡", "💚", "💙", "💜", "🖤" ]    0
[ "💚", "💙", "💜", "🖤", "🧡" ]    1
[ "💙", "💜", "🖤", "🧡", "💚" ]    2
[ "💜", "🖤", "🧡", "💚", "💙" ]    3
[ "🖤", "🧡", "💚", "💙", "💜" ]    4
[ "🧡", "💚", "💙", "💜", "🖤" ]    5
[ "💚", "💙", "💜", "🖤", "🧡" ]    6



回答5:


So many of these answers seem over-complicated and difficult to read. I don't think I saw anyone using splice with 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;
}

console.log outputs (*generated in May):

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

As for compactness, I can offer a couple of generic one-liner functions (not counting the console.log | return portion). Just feed it the array and the target value in the arguments.

I combine these functions into one for a four-player card game program where the array is ['N','E','S','W']. I left them separate in case anyone wants to copy/paste for their needs. For my purposes, I use the functions when seeking whose turn is next to play/act during different phases of the game (Pinochle). I haven't bothered testing for speed, so if someone else wants to, feel free to let me know the results.

*notice, the only difference between functions is the "+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;
}

combination function...

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



回答6:


@Christoph, you've done a clean code, but 60% slowest than this one i found. Look at the result on jsPerf : http://jsperf.com/js-rotate-array/2 [Edit] OK now there is more browsers an that not obvious witch methods the best

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



回答7:


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



回答8:


The accepted answer has a flaw of not being able to handle arrays larger than the call stack size which depends on the session but should be around like 100~300K items. For instance, in the current Chrome session that i tried it was 250891. In many cases you may not even know to what size the array might dynamically grow into. So that's a serious problem.

To overcome this limitation, I guess one interesting method is utilizing Array.prototype.map() and mapping the elements by rearranging the indices in a circular fashion. This method takes one integer argument. If this argument is positive it will rotate on increasing indices and if negative on decreasing indices direction. This has only O(n) time complexity and will return a new array without mutating the one it's called upon while handling millions of items without any problem. Let see how it works;

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

Actually after i have been criticized on two matters as follows;

  1. There is no need for a conditional for positive or negative input since it reveals a violation of DRY .you could do this with one map because every negative n has a positive equivalent (Totally right..)
  2. An Array function should either change the current array or make a new array, your function could do either depending on whether a shift is necessary or not (Totally right..)

I have decided to modify the code as follows;

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

Then again; of course the JS functors like Array.prototype.map() are slow compared to their equivalents coded in plain JS. In order to gain more than 100% performance boost the following would probably be my choice of Array.prototype.rotate() if i ever need to rotate an array in production code like the one i used in my attempt on 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;
};



回答9:


Here is a very simple way to shift items in an array:

function rotate(array, stepsToShift) {

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

    return array;
}



回答10:


This function is a little faster than the accepted answer for small arrays but MUCH faster for large arrays. This function also allows for an arbitrary number of rotations greater than the length of the array, which is a limitation of the original function.

Lastly, the accepted answer rotates the opposite direction as described.

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

And the functional equivalent (which seems to also have some performance benefits):

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

You can check out the performance breakdown here.




回答11:


EDIT:: Hey so turns out there's too much iteration happening. No loops, no branching.

Still works with negative n for right rotation and positive n for left rotation for any size 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))
}

Here's the code golf version for giggles

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

EDIT1::* Branchless, mutationless implementation.

So hey, turns out I had a branch where I didn't need it. Here is a working solution. negative num = right rotate by |num| positive num = left rotate by num

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

The equation ((n%l) + l) % l maps exactly positive and negative numbers of any arbitrarily large values of n

ORIGINAL

Rotate left and right. Rotate left with positive n, rotate right with negative n.

Works for obscenely large inputs of n.

No mutation mode. Too much mutation in these answers.

Also, fewer operations than most answers. No pop, no push, no splice, no shift.

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

or

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

Explanation:

map each index of A to the value at index offset. In this case

offset = num

if the offset < 0 then offset + index + positive length of A will point to the inverse offset.

if offset > 0 and offset < length of A then simply map the current index to the offset index of A.

Otherwise, modulo the offset and the length to map the offset in the bounds of the array.

Take for instance offset = 4 and offset = -4.

When offset = -4, and A = [1,2,3,4,5], for each index, offset + index will make the magnitude (or Math.abs(offset)) smaller.

Let's explain the calculation for the index of negative n first. A[(((n % A.length) + A.length) % A.length)+0] and been intimidated. Don't be. It took me 3 minutes in a Repl to work it out.

  1. We know n is negative because the case is n < 0. If the number is larger than the range of the Array, n % A.length will map it into the range.
  2. n + A.length add that number to A.length to offset n the correct amount.
  3. We know n is negative because the case is n < 0. n + A.length add that number to A.length to offset n the correct amount.
  4. Next Map it to the range of the length of A using modulo. The second modulous is necessary to map the result of the calculation into an indexable range

  5. First index: -4 + 0 = -4. A.length = 5. A.length - 4 = 1. A2 is 2. Map index 0 to 2. [2,... ]

  6. Next index, -4 + 1 = -3. 5 + -3 = 2. A2 is 3. Map index 1 to 3. [2,3... ]
  7. Etc.

The same process applies to offset = 4. When offset = -4, and A = [1,2,3,4,5], for each index, offset + index will make the magnitude bigger.

  1. 4 + 0 = 0. Map A[0] to the value at A[4]. [5...]
  2. 4 + 1 = 5, 5 is out of bounds when indexing, so map A2 to the value at the remainder of 5 / 5, which is 0. A2 = value at A[0]. [5,1...]
  3. repeat.



回答12:


When I couldn't find a ready-made snippet to start a list of days with 'today', I did it like this (not quite generic, probably far less refined than the above examples, but did the job):

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

Thanks to a little refactor by a better programmer than me it's a line or two shorter than my initial attempt, but any further comments on efficiency are welcome.




回答13:


Using ES6's spread for an immutable example ...

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

and

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

It's probably not the most efficient though but it's concise.




回答14:


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




回答15:


Easy solution with slice and destructuring:

const rotate = (arr, count) => {
	if (count === 0) return arr;
  return [...arr.slice(count, arr.length), ...arr.slice(0, count)];
};

const arr = [1,2,3,4,5];

console.log(rotate(arr, 1));  // [2, 3, 4, 5, 1]
console.log(rotate(arr, 2));  // [3, 4, 5, 1, 2]
console.log(rotate(arr, -2)); // [4, 5, 1, 2, 3]
console.log(rotate(arr, -1)); // [5, 1, 2, 3, 4]



回答16:


How about incrementing a counter and then getting the remainder of a division by the array length to get where you are supposed to be.

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

Language syntax aside this should work fine.




回答17:


If your array is going to be large and/or you are going to rotate a lot, you might want to consider using a linked list instead of an array.




回答18:


@molokoloco I needed a function that I could configure to rotate in a direction - true for forward and false for backward. I created a snippet that takes a direction, a counter and an array and outputs an object with the counter incremented in the appropriate direction as well as prior, current, and next values. It does NOT modify the original array.

I also clocked it against your snippet and although it is not faster, it is faster than the ones you compare yours with - 21% slower 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)



回答19:


I am coming late but I have a brick to add to these good answers. I was asked to code such a function and I first did:

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

But it appeared to be less efficient than following when n is big:

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



回答20:


I am not sure if this is the most efficient way but I like the way it reads, it's fast enough for most large tasks as I have tested it on 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()



回答21:


Non mutating solution

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

with mutation

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



回答22:


I am sharing my solution which I am using for rotating on carousel. It might break when array size is smaller than displayCount, but you could add extra condition to stop rotating when it's small, or concatenating the main array *displayCount times too.

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]



回答23:


Native, fast, small, semantic, works on old engines and "curryable".

function rotateArray(offset, array) {
    offset = -(offset % array.length) | 0 // ensure int
    return array.slice(offset).concat(
        array.slice(0, offset)
    )
}


来源:https://stackoverflow.com/questions/29775123/javascript-rotating-an-array-using-a-function-with-splice

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!