在数组题中,"轮转数组"是一道很典型的题。它的代码并不长,但同时考查了数组下标、空间复杂度、边界处理,以及能否从结果反推出操作过程。
这篇文章用 Java 解决 LeetCode 189「轮转数组」,重点讲清楚一个看起来有些巧妙的方法:为什么只需要三次反转,就能把数组向右轮转 k****个位置。
一、题目描述
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,要求直接修改原数组。
例如:
输入:nums = [1,2,3,4,5,6,7], k = 3
输出:[5,6,7,1,2,3,4]
向右轮转 3 个位置,可以理解为:把数组末尾的 3 个元素 [5,6,7] 移到数组开头,其余元素 [1,2,3,4] 整体向后移动。
二、最容易想到的做法:使用额外数组
原数组下标为 i 的元素,轮转后会出现在:
(i + k) % n
因此,可以创建一个长度相同的新数组,把每个元素放入新位置,再复制回原数组:
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n;
int[] temp = new int[n];
for (int i = 0; i < n; i++) {
temp[(i + k) % n] = nums[i];
}
System.arraycopy(temp, 0, nums, 0, n);
}
这个方法很直观,时间复杂度为 O(n),但需要 O(n) 的额外空间。如果面试官进一步要求空间复杂度为 O(1),就需要考虑原地操作。
三、最优解:三次反转
仍然以数组为例:
[1, 2, 3, 4, 5, 6, 7]
向右轮转 3 位后,希望得到:
[5, 6, 7, 1, 2, 3, 4]
可以把原数组分成两部分:
A = [1, 2, 3, 4]
B = [5, 6, 7]
轮转的目标,本质上就是把 A + B 变成 B + A。
具体分三步。
第一步:反转整个数组
[1, 2, 3, 4, 5, 6, 7]
↓
[7, 6, 5, 4, 3, 2, 1]
此时两部分的位置已经交换,但每个部分内部的顺序也被反转了。可以表示为:
reverse(B) + reverse(A)
第二步:反转前 k 个元素
前 3 个元素是 [7,6,5],将其反转:
[7, 6, 5, 4, 3, 2, 1]
↓
[5, 6, 7, 4, 3, 2, 1]
这样,原数组末尾的 B 已经恢复成正确顺序。
第三步:反转剩余元素
再把下标 k 到 n - 1 的部分 [4,3,2,1] 反转:
[5, 6, 7, 4, 3, 2, 1]
↓
[5, 6, 7, 1, 2, 3, 4]
最终得到目标结果。
整个过程可以概括为:
整体反转 → 反转前 k 个元素 → 反转后 n-k 个元素
四、为什么三次反转一定成立?
假设原数组由两部分组成:
A + B
其中 B 是需要移到前面的最后 k 个元素。
第一次整体反转后得到:
reverse(B) + reverse(A)
然后分别反转前后两部分:
B + A
这正是向右轮转后的结果。因此,三次反转不是只对某个示例有效,而是对任意数组都成立。
五、完整 Java 代码
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n;
// 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 left, int right) {
while (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
}
reverse 方法使用双指针:left 从左向右移动,right 从右向左移动,每次交换两个位置的元素,直到两个指针相遇。
六、为什么必须对 k 取模?
如果数组长度为 7,而 k = 10,向右轮转 10 次和向右轮转 3 次的结果相同:
10 % 7 = 3
因为每轮转 n 次,数组都会恢复原状,所以真正有效的轮转次数是:
k %= nums.length;
这一步还能避免 k 大于数组长度时,后续反转区间越界。
当 k = 0 时,第二次反转调用的区间是 [0, -1]。由于 reverse 中的条件是 left < right,循环不会执行,代码仍然能够正确运行。
七、复杂度分析
虽然进行了三次反转,但每个元素只会被交换有限次,因此:
-
时间复杂度:
O(n) -
空间复杂度:
O(1)
它没有创建与数组长度相关的辅助空间,满足原地修改的要求。
八、常见错误
1. 忘记对 k 取模
当 k > nums.length 时,反转区间可能越界。进入反转逻辑前,应先执行 k %= n。
2. 反转顺序写错
向右轮转的常见写法是:
整体 → 前 k 个 → 后 n-k 个
不要把 k 误写成 n - k。可以始终记住:向右轮转后,原数组最后的 k****个元素要出现在最前面。
3. 逐个移动导致超时
如果每轮转一次,就把整个数组移动一遍,那么时间复杂度是 O(n × k)。当数组和 k 都很大时,这种方法容易超时。
4. 只修改了局部变量
题目要求原地修改 nums,不能只让一个新数组变量指向结果而不复制回去。三次反转直接操作原数组,不存在这个问题。