壹 ❀ 引
今天來做一道同樣簡單,但是挺有趣的題,題目來自leetcode189. 旋轉數組,題目描述如下:
給定一個數組,將數組中的元素向右移動 k 個位置,其中 k 是非負數。
示例 1:
輸入: [1,2,3,4,5,6,7] 和 k = 3 輸出: [5,6,7,1,2,3,4]
解釋:
向右旋轉 1 步: [7,1,2,3,4,5,6]
向右旋轉 2 步: [6,7,1,2,3,4,5]
向右旋轉 3 步: [5,6,7,1,2,3,4]
示例 2:輸入: [-1,-100,3,99] 和 k = 2 輸出: [3,99,-1,-100]
解釋:
向右旋轉 1 步: [99,-1,-100,3]
向右旋轉 2 步: [3,99,-1,-100]
說明:盡可能想出更多的解決方案,至少有三種不同的方法可以解決這個問題。
要求使用空間復雜度為 O(1) 的 原地 算法。
老規矩,在分析完題目后,我先來說說我的實現思路,再來解析優質的解答。
貳 ❀ 解題思路
其實也不用被題目嚇到,說是旋轉數組,其實就是給定一個k表示要將數組最后一位數轉移到數組頭部次數的操作,比如第二個例子,k為2,表示一共要進行兩次操作:

第一次將數組最后一位也就是99移動到數組頭部。此時數組變成了[99,-1,-100,3]
,接着第二次操作,這時候最后一位是3:

此時數組變成了[3,99,-1,-100]
。
由於k為2,只需要執行2次,所以旋轉數組完成,思路已經很清晰了,我們來實現它:
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function(nums, k) {
// 如果數組只有一位或為空不用做任何操作
if(nums.length<=1){
return;
};
while(k>0){
//每次取最后一位,並加入到頭部,由於splice返回的是數組,利用拓展運算符...還原成單個元素
nums.unshift(...nums.splice(-1,1));
k--;
};
};
哎,有同學可能就想到了,我何必一個個的轉義,k為2,說到底就是把數組倒數兩個元素整個搬到數組頭部即可,然后我就開始寫了如下代碼:
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function (nums, k) {
nums.length <= 1 ? nums : nums.push(...nums.splice(0, nums.length - k));
};
2020.8.12更新
經博客園用戶Gary咖喱指出,對於splice
第一參數為負數情況描述有誤,特做修正。
注意,這里的splice我是正向剪切,原因是我發現splice的第一個參數為負數時,比如-1表示倒序最后一個開始,第二個參數不管是幾,都只能剪一個:
[1,2,3].splice(-1,1); //[3]
[1,2,3].splice(-1,2); //[3]
當第一參數為負數時,表示倒序剪切,最后一個為-1,倒數第二個為-2,倒數第三個為-3,但裁剪順序還是從左往右裁剪。

如上圖,由於-1是最后一個元素,再往右沒有其它元素,所以第二參數不管是1還是2,都只能裁剪一個,如果我們想裁剪多個,比如:
[1,2,3].splice(-3,3); //[1,2,3]

因為-3是第一個,從左到右剪切3個,這樣就達到目的了。
最后需要補充的就是當第一參數為負數,且絕對值大於數組長度的情況,比如:
[1,2,3].splice(-4,3); //[1,2,3]
其實可以理解為裁剪起點在元素1(也就是索引-3)的左邊,往右裁剪自然涵蓋住了整個數組,所以本質還是對整個數組進行了裁剪。
前面的思路是把尾部剪切了拼到頭部,我們何不反過來,比如[3,99,-1,-100]
我們剪3,99
push到-100
后面呢,所以這才有了nums.length - k
表示前面我們應該剪切的個數。
很遺憾,這段代碼看似可以,但提交掛掉了,原因是這段代碼只滿足數組length大於k的情況,比如數組為[1,2]
,k為3,正確答案是交換3次變成[2,1]
;
而此時nums.length - k
為-1,splice一大特點就是第二參數為0或者負數,表示一個不剪切,所以不符合。
此時,博客園用戶love編程的小可愛想到了%
求余,我立馬靈光閃現改進了代碼:
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function (nums, k) {
nums.length <= 1 ? nums : nums.push(...nums.splice(0, nums.length - (k % nums.length)));
};
唯一區別只是在於nums.length - (k % nums.length)
,什么意思呢?
比如有數組[1,2,3,4]
,k為4,旋轉4次后你會發現結果和最初的樣子一模一樣。而且只要k為數組長度的整數倍都會造成這種情況。
所以比如k為5,其實可以看成4+1
次,我們只用旋轉一次即可了,而%
求余正好能達到這個效果:
8%4 //0
9%4 //1
3%4 //3
2%4 //2
所以用k%length算出我們真正要旋轉數組的次數即可,一行代碼搞定。
題目要求最少三種方法解答問題,但我沒能想出其它做法,這里再補充其它不錯的做法。
引用leetcode用戶秦時明月的一個不錯的做法,不用splice,直接利用pop即可:
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function(nums,k) {
for(var i = 0;i<k;i++){
nums.unshift(nums.pop());
};
};
這個就比我第一種實現要優雅的多了。
然后我在看leetcode用戶🎃的做法時,splice發揮到了極致:
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function(nums, k) {
nums.splice(0,0,...nums.splice(nums.length-k))
};
這代代碼執行是這樣,比如數組[1,2,3,4]
,假設k為3,先執行...nums.splice(nums.length-k)
,得到了2,3,4
。此時數組變成了[1]
。
接着執行前面的nums.splice(0,0,2,3,4)
,表示從0位前面插入元素2,3,4
於是變成了[2,3,4,1]
。
那么關於本題就說到這了。