文章目錄
- 1. 題目描述
- 2. 理解題目
- 3. 解法一:使用額外數組
- 3.1 思路
- 3.2 Java代碼實現
- 3.3 代碼詳解
- 3.4 復雜度分析
- 3.5 適用場景
- 4. 解法二:環狀替換法(原地算法)
- 4.1 思路
- 4.2 Java代碼實現
- 4.3 代碼詳解
- 4.4 復雜度分析
- 4.5 陷阱與注意事項
- 5. 解法三:數組翻轉法(最優原地算法)
- 5.1 思路
- 5.2 Java代碼實現
- 5.3 代碼詳解
- 5.4 數學證明
- 5.5 復雜度分析
- 5.6 適用場景
- 6. 詳細步驟分析與示例跟蹤
- 6.1 示例跟蹤:使用額外數組法
- 6.2 示例跟蹤:環狀替換法
- 6.3 示例跟蹤:數組翻轉法
- 7. 常見錯誤與優化
- 7.1 常見錯誤
- 7.2 優化技巧
- 8. 三種解法的對比與選擇
- 9. 擴展題目與應用
- 9.1. 左輪轉數組
- 9.2. 字符串輪轉
- 9.3. 循環移位操作
- 10. 實際應用場景
- 11. 完整的 Java 解決方案
- 12. 總結與技巧
- 12.1 解題要點
- 12.2 學習收獲
- 12.3 面試技巧
- 13. 參考資料
1. 題目描述
給你一個數組,將數組中的元素向右輪轉 k
個位置,其中 k
是非負數。
示例 1:
輸入: nums = [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:
輸入:nums = [-1,-100,3,99], k = 2
輸出:[3,99,-1,-100]
解釋:
向右輪轉 1 步: [99,-1,-100,3]
向右輪轉 2 步: [3,99,-1,-100]
提示:
- 1 <= nums.length <= 10^5
- -2^31 <= nums[i] <= 2^31 - 1
- 0 <= k <= 10^5
進階:
- 盡可能想出更多的解決方案,至少有三種不同的方法可以解決這個問題。
- 你可以使用空間復雜度為 O(1) 的 原地 算法解決這個問題嗎?
2. 理解題目
這道題要求我們將數組中的所有元素向右移動k個位置。具體來說:
- 數組中的每個元素都向右移動k個索引位置
- 數組是"循環"的,即超出數組末尾的元素會被放置到數組的開頭
- 我們需要原地修改數組,而不是創建一個新數組(盡管也可以使用額外空間的解法)
關鍵點:
- k可能大于數組長度,因此實際的移動次數是k % n(其中n是數組長度)
- 向右輪轉k次等同于將數組最后k個元素移動到數組開頭
- 這道題可以有多種解法,包括使用額外空間和原地(O(1)空間復雜度)算法
3. 解法一:使用額外數組
3.1 思路
最簡單直觀的方法是創建一個新數組,然后將原數組的每個元素放到新數組中的正確位置:
- 創建一個與原數組相同大小的新數組
- 對于原數組中索引為i的元素,其在新數組中的位置為(i + k) % n
- 將新數組的內容復制回原數組
這種方法使用了O(n)的額外空間,但思路非常清晰,適合初學者理解。
3.2 Java代碼實現
class Solution {public void rotate(int[] nums, int k) {int n = nums.length;// 處理 k 大于數組長度的情況k = k % n;// 如果k為0,不需要輪轉if (k == 0) {return;}// 創建一個新數組來存儲輪轉后的結果int[] result = new int[n];// 將原數組中的元素放入新數組的正確位置for (int i = 0; i < n; i++) {result[(i + k) % n] = nums[i];}// 將新數組的內容復制回原數組for (int i = 0; i < n; i++) {nums[i] = result[i];}}
}
3.3 代碼詳解
詳細解釋每一步的意義和實現:
int n = nums.length;// 處理 k 大于數組長度的情況
k = k % n;// 如果k為0,不需要輪轉
if (k == 0) {return;
}
- 首先獲取數組長度n
- 由于輪轉n次會回到原始狀態,所以我們只需要考慮k % n次輪轉
- 如果k為0或者k是n的倍數,數組不需要變化,直接返回
// 創建一個新數組來存儲輪轉后的結果
int[] result = new int[n];// 將原數組中的元素放入新數組的正確位置
for (int i = 0; i < n; i++) {result[(i + k) % n] = nums[i];
}
- 創建一個與原數組相同大小的新數組
- 將原數組中索引為i的元素放到新數組中索引為(i + k) % n的位置
- 這里使用模運算是為了處理索引超出數組長度的情況
// 將新數組的內容復制回原數組
for (int i = 0; i < n; i++) {nums[i] = result[i];
}
- 最后,將臨時數組中的結果復制回原數組,完成輪轉操作
3.4 復雜度分析
- 時間復雜度: O(n),其中n是數組的長度。我們需要遍歷數組兩次,每次遍歷的時間復雜度是O(n)。
- 空間復雜度: O(n),我們創建了一個與原數組等長的新數組。
3.5 適用場景
這種解法適用于:
- 初學者理解問題
- 代碼簡潔性優先于空間效率的場景
- 數組不太大的情況
4. 解法二:環狀替換法(原地算法)
4.1 思路
我們可以直接在原數組上進行操作,不使用額外空間。基本思想是:
- 從位置0開始,將當前位置的元素放到它應該去的位置(即(i + k) % n),同時記錄被替換的元素
- 然后將被替換的元素放到它應該去的位置,繼續這個過程
- 當我們回到起始位置時,我們已經完成了一個循環,需要從下一個位置開始新的循環
- 重復這個過程,直到所有元素都被訪問
這種方法不使用額外數組,但需要仔細處理訪問元素的順序。
4.2 Java代碼實現
class Solution {public void rotate(int[] nums, int k) {int n = nums.length;k = k % n;// 如果k為0,不需要輪轉if (k == 0) {return;}int count = 0; // 記錄已經移動的元素數量// 從0開始,最多需要處理n個元素for (int start = 0; count < n; start++) {int current = start; // 當前處理的位置int prev = nums[start]; // 當前位置的值do {// 計算下一個位置int next = (current + k) % n;// 保存下一個位置的值int temp = nums[next];// 將當前值放到下一個位置nums[next] = prev;// 更新current和prev,繼續下一次替換current = next;prev = temp;count++;} while (start != current); // 當回到起始位置時,一個循環結束}}
}
4.3 代碼詳解
環狀替換的關鍵在于追蹤元素的移動路徑,確保每個元素都被移動到正確位置:
int count = 0; // 記錄已經移動的元素數量// 從0開始,最多需要處理n個元素
for (int start = 0; count < n; start++) {int current = start; // 當前處理的位置int prev = nums[start]; // 當前位置的值
count
變量記錄我們已經處理過的元素數量start
表示當前循環的起始位置current
跟蹤我們當前正在處理的位置prev
存儲當前位置原來的值
do {// 計算下一個位置int next = (current + k) % n;// 保存下一個位置的值int temp = nums[next];// 將當前值放到下一個位置nums[next] = prev;// 更新current和prev,繼續下一次替換current = next;prev = temp;count++;
} while (start != current); // 當回到起始位置時,一個循環結束
- 計算元素應該被放置的下一個位置:
(current + k) % n
- 在替換前,保存目標位置的原始值
- 將當前值放到目標位置
- 更新
current
為新位置,prev
為新位置原來的值 - 增加已處理元素計數
- 重復此過程,直到回到循環的起始位置
當一個循環結束(回到起始位置)時,可能還有未被訪問的元素。因此,我們增加start
并開始一個新的循環,直到所有元素都被處理。
4.4 復雜度分析
- 時間復雜度: O(n),每個元素只會被移動一次,總共需要移動n次。
- 空間復雜度: O(1),只使用了有限的幾個變量,不需要額外數組。
4.5 陷阱與注意事項
環狀替換法需要特別注意:
- 處理循環長度:如果k和n有公約數,一個循環結束后可能還有元素未被訪問,需要開始新的循環
- 邊界條件:確保我們處理了所有的元素(通過計數)
- 避免原地自我覆蓋:在移動元素前保存下一個位置的值
5. 解法三:數組翻轉法(最優原地算法)
5.1 思路
這是最優雅且高效的解法。基本思想是:
- 首先翻轉整個數組
- 然后翻轉前k個元素
- 最后翻轉剩余的n-k個元素
這種方法不需要額外空間,操作簡單明了,且易于實現。
5.2 Java代碼實現
class Solution {public void rotate(int[] nums, int k) {int n = nums.length;k = k % n;// 如果k為0,不需要輪轉if (k == 0) {return;}// 1. 翻轉整個數組reverse(nums, 0, n - 1);// 2. 翻轉前k個元素reverse(nums, 0, k - 1);// 3. 翻轉剩余的n-k個元素reverse(nums, k, n - 1);}// 輔助函數:翻轉數組的指定部分private void reverse(int[] nums, int start, int end) {while (start < end) {int temp = nums[start];nums[start] = nums[end];nums[end] = temp;start++;end--;}}
}
5.3 代碼詳解
數組翻轉法的優雅之處在于它非常直觀:
// 1. 翻轉整個數組
reverse(nums, 0, n - 1);
- 首先將整個數組翻轉,這樣原來的順序就變成了逆序
- 例如:[1,2,3,4,5,6,7] 變成 [7,6,5,4,3,2,1]
// 2. 翻轉前k個元素
reverse(nums, 0, k - 1);
- 然后翻轉前k個元素,將這部分恢復正確的相對順序
- 對于k=3,[7,6,5,4,3,2,1] 的前k個元素 [7,6,5] 翻轉后變成 [5,6,7,4,3,2,1]
// 3. 翻轉剩余的n-k個元素
reverse(nums, k, n - 1);
- 最后翻轉剩余的n-k個元素,將這部分也恢復正確的相對順序
- [5,6,7,4,3,2,1] 的后n-k個元素 [4,3,2,1] 翻轉后變成 [5,6,7,1,2,3,4]
- 這就是最終的輪轉結果
// 輔助函數:翻轉數組的指定部分
private void reverse(int[] nums, int start, int end) {while (start < end) {int temp = nums[start];nums[start] = nums[end];nums[end] = temp;start++;end--;}
}
- 輔助函數實現了數組特定范圍內的翻轉
- 使用雙指針法,從兩端向中間逐步交換元素
- 這是一個標準的數組翻轉操作
5.4 數學證明
為什么數組翻轉法能實現輪轉效果?我們可以從數學角度證明:
設原數組為A,長度為n,需要右移k個位置。
- 定義A’ = A[0…n-k-1],表示原數組的前n-k個元素
- 定義A’’ = A[n-k…n-1],表示原數組的后k個元素
- 原數組可表示為:A = [A’, A’']
- 向右輪轉k個位置后的數組為:B = [A’‘, A’]
數組翻轉法的步驟:
- 翻轉整個數組:[A’, A’‘] → [(A’‘)^r, (A’)r],其中r表示翻轉
- 翻轉前k個元素:[(A’‘)^r, (A’)^r] → [A’‘, (A’)^r]
- 翻轉后n-k個元素:[A’‘, (A’)^r] → [A’‘, A’]
最終結果:[A’‘, A’],這正是輪轉后的結果。
5.5 復雜度分析
- 時間復雜度: O(n),翻轉數組需要O(n)的時間。
- 空間復雜度: O(1),只使用了有限的臨時變量。
5.6 適用場景
數組翻轉法因其簡潔性和效率,幾乎適用于所有場景:
- 需要原地操作的場景
- 性能要求高的場景
- 代碼簡潔度要求高的場景
6. 詳細步驟分析與示例跟蹤
讓我們通過一個具體例子來跟蹤每種算法的執行過程,加深理解。
6.1 示例跟蹤:使用額外數組法
輸入:nums = [1,2,3,4,5,6,7], k = 3
- 計算實際輪轉次數:
k = k % n = 3 % 7 = 3
- 創建新數組:
result = new int[7]
- 將原數組元素放入新數組:
- nums[0]=1 → result[(0+3)%7]=result[3]=1
- nums[1]=2 → result[(1+3)%7]=result[4]=2
- nums[2]=3 → result[(2+3)%7]=result[5]=3
- nums[3]=4 → result[(3+3)%7]=result[6]=4
- nums[4]=5 → result[(4+3)%7]=result[0]=5
- nums[5]=6 → result[(5+3)%7]=result[1]=6
- nums[6]=7 → result[(6+3)%7]=result[2]=7
- 現在result=[5,6,7,1,2,3,4]
- 將result復制回nums:nums=[5,6,7,1,2,3,4]
6.2 示例跟蹤:環狀替換法
輸入:nums = [1,2,3,4,5,6,7], k = 3
-
計算實際輪轉次數:
k = k % n = 3 % 7 = 3
-
開始從start=0處的環狀替換:
- 當前位置current=0,當前值prev=nums[0]=1
- 下一個位置next=(0+3)%7=3,保存nums[3]=4
- 將1放入位置3:nums=[1,2,3,1,5,6,7],current=3,prev=4
- 下一個位置next=(3+3)%7=6,保存nums[6]=7
- 將4放入位置6:nums=[1,2,3,1,5,6,4],current=6,prev=7
- 下一個位置next=(6+3)%7=2,保存nums[2]=3
- 將7放入位置2:nums=[1,2,7,1,5,6,4],current=2,prev=3
- 下一個位置next=(2+3)%7=5,保存nums[5]=6
- 將3放入位置5:nums=[1,2,7,1,5,3,4],current=5,prev=6
- 下一個位置next=(5+3)%7=1,保存nums[1]=2
- 將6放入位置1:nums=[1,6,7,1,5,3,4],current=1,prev=2
- 下一個位置next=(1+3)%7=4,保存nums[4]=5
- 將2放入位置4:nums=[1,6,7,1,2,3,4],current=4,prev=5
- 下一個位置next=(4+3)%7=0,保存nums[0]=1
- 將5放入位置0:nums=[5,6,7,1,2,3,4],current=0,prev=1
- 現在current=0=start,一個循環結束,且count=7,所有元素都已處理
-
最終結果:nums=[5,6,7,1,2,3,4]
6.3 示例跟蹤:數組翻轉法
輸入:nums = [1,2,3,4,5,6,7], k = 3
- 計算實際輪轉次數:
k = k % n = 3 % 7 = 3
- 翻轉整個數組:
- nums=[1,2,3,4,5,6,7] → nums=[7,6,5,4,3,2,1]
- 翻轉前k個元素(前3個):
- nums=[7,6,5,4,3,2,1] → nums=[5,6,7,4,3,2,1]
- 翻轉剩余n-k個元素(后4個):
- nums=[5,6,7,4,3,2,1] → nums=[5,6,7,1,2,3,4]
- 最終結果:nums=[5,6,7,1,2,3,4]
7. 常見錯誤與優化
7.1 常見錯誤
-
忘記處理k大于數組長度的情況:
// 錯誤:不處理k大于數組長度的情況 public void rotate(int[] nums, int k) {// k可能大于nums.length,未處理會導致索引越界... }// 正確:進行取模操作 public void rotate(int[] nums, int k) {int n = nums.length;k = k % n; // 確保k在有效范圍內... }
-
環狀替換中的循環處理錯誤:
// 錯誤:處理不完整,可能漏掉某些元素 public void rotate(int[] nums, int k) {// ...int start = 0;int current = start;int prev = nums[start];// 只進行一個循環,可能無法處理所有元素do {// ...} while (start != current); }// 正確:確保處理所有元素 public void rotate(int[] nums, int k) {// ...int count = 0;for (int start = 0; count < n; start++) {// 確保所有元素都被處理// ...count++;} }
-
數組翻轉邊界錯誤:
// 錯誤:翻轉邊界不正確 reverse(nums, 0, k); // 錯誤,應該是k-1 reverse(nums, k, n); // 錯誤,應該是k到n-1// 正確:正確的翻轉邊界 reverse(nums, 0, k - 1); reverse(nums, k, n - 1);
7.2 優化技巧
-
提前檢查特殊情況:
// 優化:提前處理不需要輪轉的情況 if (k == 0 || k % n == 0) {return; // 不需要輪轉 }
-
使用Java內置的數組復制方法:
// 優化:使用System.arraycopy替代手動循環復制 System.arraycopy(result, 0, nums, 0, n);
-
減少不必要的取模操作:
// 優化前:每次都進行取模 for (int i = 0; i < n; i++) {result[(i + k) % n] = nums[i]; }// 優化后:預計算起始位置 int start = n - k; for (int i = 0; i < k; i++) {result[i] = nums[start + i]; } for (int i = 0; i < n - k; i++) {result[k + i] = nums[i]; }
-
環狀替換中優化循環判斷:
// 優化:使用數學方法計算循環次數 int gcd = gcd(n, k); // 計算n和k的最大公約數// 只需要進行gcd次循環,每次處理n/gcd個元素 for (int start = 0; start < gcd; start++) {// ... }// 輔助方法:計算最大公約數 private int gcd(int a, int b) {return b == 0 ? a : gcd(b, a % b); }
8. 三種解法的對比與選擇
解法 | 時間復雜度 | 空間復雜度 | 優點 | 缺點 | 適用場景 |
---|---|---|---|---|---|
額外數組法 | O(n) | O(n) | 簡單直觀,容易實現 | 需要額外空間 | 初學者,空間不敏感的場景 |
環狀替換法 | O(n) | O(1) | 不需要額外空間 | 實現較復雜,需要處理循環 | 空間敏感場景,追求原地操作 |
數組翻轉法 | O(n) | O(1) | 優雅簡潔,不需要額外空間 | 需要理解翻轉原理 | 幾乎所有場景的最優選擇 |
總結:
- 如果你是初學者或追求代碼簡潔性,使用額外數組法
- 如果你需要節省空間且追求性能,使用數組翻轉法
- 環狀替換法雖然有趣,但實現復雜,通常不是首選
9. 擴展題目與應用
9.1. 左輪轉數組
與本題類似,但方向相反,將數組元素向左移動k個位置。
解決方案:
- 向左輪轉k個位置等同于向右輪轉n-k個位置
- 或者使用相同的數組翻轉法,只需調整翻轉順序:
- 翻轉整個數組
- 翻轉前n-k個元素
- 翻轉后k個元素
9.2. 字符串輪轉
LeetCode 796. 旋轉字符串:檢查一個字符串是否可以通過多次輪轉變成另一個字符串。
解決思路:將原字符串與自身拼接,如果目標字符串是拼接結果的子串,則可以通過輪轉得到。
9.3. 循環移位操作
在計算機系統中,輪轉操作常用于循環移位(Circular Shift):
- 循環左移:將二進制數的最高位移到最低位
- 循環右移:將二進制數的最低位移到最高位
10. 實際應用場景
輪轉數組在實際編程中有多種應用:
-
緩沖區管理:
- 實現循環緩沖區時,可以使用輪轉數組避免數據移動
- 在流媒體處理中保持最近的數據片段
-
圖像處理:
- 圖像旋轉和變換
- 圖像濾波器的實現
-
信號處理:
- 信號采樣和處理時的數據輪轉
- FFT算法中的數據重排
-
游戲開發:
- 游戲地圖的循環移動(如無限地圖)
- 輪流游戲中的玩家順序管理
-
調度算法:
- 輪轉調度算法(Round Robin)中任務的輪換
- 分時系統中的時間片分配
11. 完整的 Java 解決方案
以下是結合了各種最佳實踐的最優解法(數組翻轉法):
class Solution {public void rotate(int[] nums, int k) {if (nums == null || nums.length <= 1) {return; // 處理邊界情況}int n = nums.length;k = k % n; // 處理k大于數組長度的情況if (k == 0) {return; // 不需要輪轉}// 三步翻轉法reverse(nums, 0, n - 1); // 翻轉整個數組reverse(nums, 0, k - 1); // 翻轉前k個元素reverse(nums, k, n - 1); // 翻轉剩余的n-k個元素}// 翻轉數組的指定范圍private void reverse(int[] nums, int start, int end) {while (start < end) {int temp = nums[start];nums[start++] = nums[end];nums[end--] = temp;}}
}
這個解法簡潔高效,適用于大多數場景。在LeetCode上,這個解法通常能擊敗95%以上的提交。
12. 總結與技巧
12.1 解題要點
- 正確理解輪轉:理解輪轉的本質是循環移動,超出數組末尾的元素會回到開頭。
- 取模處理:使用k % n來處理k可能大于數組長度的情況。
- 選擇合適的算法:根據空間要求選擇適當的算法(額外空間法或原地算法)。
- 翻轉技巧:數組翻轉是解決輪轉問題的強大工具。
12.2 學習收獲
通過學習輪轉數組問題,你可以掌握:
- 原地算法的思想和實現
- 雙指針技術在數組操作中的應用
- 數學思維在算法設計中的重要性
- 空間和時間復雜度的權衡
12.3 面試技巧
如果在面試中遇到此類問題:
- 先提出最簡單的解法(使用額外數組)
- 然后提出原地算法(如數組翻轉法)
- 討論每種方法的時間和空間復雜度
- 分析各種解法的適用場景和優缺點
- 實現你認為最優的解法
記住,展示你的思考過程和對不同解法的理解,比僅僅給出一個正確答案更重要。
13. 參考資料
- LeetCode 官方題解:輪轉數組
- LeetCode 題目鏈接:輪轉數組