LeetCode 第189题:旋转数组
题目描述
给你一个数组,将数组中的元素向右轮转 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) 的原地算法解决这个问题
解题思路
方法一:使用额外数组
最直接的思路是创建一个新数组,然后将原数组中的元素放到新数组中对应的位置,最后将新数组中的元素复制回原数组。
关键点:
- 对于数组中的元素
nums[i]
,将其放到新数组中的(i + k) % n
位置上,其中n
是数组的长度 - 最后将新数组复制回原数组
时间复杂度:O(n),其中 n 是数组的长度,每个元素被移动一次
空间复杂度:O(n),需要使用长度为 n 的额外数组
方法二:环状替换
为了达到 O(1) 的空间复杂度,我们可以直接在原数组上操作。考虑把原数组当作一个环,从某个位置开始,不断地将当前元素放到其应该在的位置,直到回到起点。
关键点:
- 从位置 i 开始,将元素 nums[i] 放到位置 (i + k) % n
- 然后从位置 (i + k) % n 的元素开始继续环状替换
- 总共需要移动 n 个元素,但在某些情况下会出现环路(即回到起点)
- 如果未能移动所有元素,则选择下一个未处理的位置开始新的环路
时间复杂度:O(n),每个元素只会被移动一次
空间复杂度:O(1),只需要常数额外空间
方法三:数组翻转
另一种常用技巧是通过数组翻转来实现旋转。
关键点:
- 将整个数组翻转
- 将前 k % n 个元素翻转
- 将剩余的 n - (k % n) 个元素翻转
时间复杂度:O(n),进行了三次翻转,每次翻转的时间复杂度为 O(n)
空间复杂度:O(1),只需要常数额外空间
代码实现
C# 实现
方法一:使用额外数组
public class Solution {
public void Rotate(int[] nums, int k) {
int n = nums.Length;
k = k % n; // 处理 k > n 的情况
int[] temp = new int[n];
for (int i = 0; i < n; i++) {
temp[(i + k) % n] = nums[i];
}
Array.Copy(temp, nums, n);
}
}
方法二:环状替换
public class Solution {
public void Rotate(int[] nums, int k) {
int n = nums.Length;
k = k % n; // 处理 k > n 的情况
if (k == 0) return;
int count = 0; // 记录已经处理的元素个数
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;
prev = temp;
current = next;
count++;
} while (start != current);
}
}
}
方法三:数组翻转
public class Solution {
public void Rotate(int[] nums, int k) {
int n = nums.Length;
k = k % n; // 处理 k > n 的情况
if (k == 0) return;
// 1. 翻转整个数组
Reverse(nums, 0, n - 1);
// 2. 翻转前 k 个元素
Reverse(nums, 0, k - 1);
// 3. 翻转剩余元素
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--;
}
}
}
Python 实现
方法一:使用额外数组
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
k = k % n # 处理 k > n 的情况
temp = [0] * n
for i in range(n):
temp[(i + k) % n] = nums[i]
nums[:] = temp
方法二:环状替换
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
k = k % n # 处理 k > n 的情况
if k == 0:
return
count = 0 # 记录已经处理的元素个数
for start in range(n):
if count >= n:
break
current = start
prev = nums[start]
while True:
next_idx = (current + k) % n
temp = nums[next_idx]
nums[next_idx] = prev
prev = temp
current = next_idx
count += 1
if start == current:
break
方法三:数组翻转
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
k = k % n # 处理 k > n 的情况
if k == 0:
return
# 1. 翻转整个数组
nums.reverse()
# 2. 翻转前 k 个元素
nums[:k] = reversed(nums[:k])
# 3. 翻转剩余元素
nums[k:] = reversed(nums[k:])
C++ 实现
方法一:使用额外数组
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k = k % n; // 处理 k > n 的情况
vector<int> temp(n);
for (int i = 0; i < n; i++) {
temp[(i + k) % n] = nums[i];
}
nums = temp;
}
};
方法二:环状替换
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k = k % n; // 处理 k > n 的情况
if (k == 0) return;
int count = 0; // 记录已经处理的元素个数
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;
prev = temp;
current = next;
count++;
} while (start != current);
}
}
};
方法三:数组翻转
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k = k % n; // 处理 k > n 的情况
if (k == 0) return;
// 1. 翻转整个数组
reverse(nums.begin(), nums.end());
// 2. 翻转前 k 个元素
reverse(nums.begin(), nums.begin() + k);
// 3. 翻转剩余元素
reverse(nums.begin() + k, nums.end());
}
};
性能分析
各语言实现的性能对比:
实现语言 | 方法 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|---|
C# | 方法一 | 256 ms | 48.2 MB | 额外空间,实现简单 |
C# | 方法二 | 232 ms | 47.9 MB | 原地操作,实现复杂 |
C# | 方法三 | 208 ms | 48.0 MB | 原地操作,实现简单,性能最优 |
Python | 方法一 | 248 ms | 25.5 MB | 使用切片,简洁易读 |
Python | 方法二 | 224 ms | 25.3 MB | 实现较复杂 |
Python | 方法三 | 188 ms | 25.4 MB | 使用内置函数,简洁高效 |
C++ | 方法一 | 36 ms | 25.0 MB | 额外空间 |
C++ | 方法二 | 28 ms | 24.8 MB | 原地操作 |
C++ | 方法三 | 20 ms | 24.9 MB | 使用STL库,简洁高效 |
补充说明
代码亮点
- 方法一简单直观,容易理解和实现
- 方法二通过环状替换实现原地操作,节省空间
- 方法三使用数组翻转技巧,既简单又高效
- 所有方法都考虑了 k > n 的情况,通过取模处理
数组翻转法的原理
数组翻转法的原理可以从数学角度理解。对于原始数组 [1,2,3,4,5,6,7] 和 k = 3:
- 全部翻转后得到 [7,6,5,4,3,2,1]
- 翻转前 k 个元素,得到 [5,6,7,4,3,2,1]
- 翻转后 n-k 个元素,得到 [5,6,7,1,2,3,4]
这正是将数组向右轮转 k 个位置后的结果。这种方法之所以有效,是因为它将数组分成两部分,分别翻转后再整体翻转,相当于实现了两部分的位置交换。
常见错误
- 没有处理 k > n 的情况,导致索引越界
- 在环状替换方法中,没有正确处理循环,可能导致死循环或某些元素未被处理
- 忘记处理 k = 0 的特殊情况
- 在翻转法中,翻转范围计算错误