在 Java 里处理数组循环移位,本质是把下标按照某个偏移量重新映射。假设有一个长度为 n 的整型数组,希望每个元素向右循环移动 k 位,那么原来在位置 i 的元素应当去到位置 (i + k) % n。最直接的想法是新建一个同样长度的数组,把元素按新位置放好再拷贝回去,但这样空间复杂度是 O(n),当数组很大时会明显吃内存。真正合理的做法是在原数组上完成重排,只使用 O(1) 的额外空间。

三次反转法实现右移
三次反转是一种经典且容易证明正确性的办法。以右移 k 位为例,我们可以先把整个数组反转,再把前 k 个元素反转,最后把剩下的 n-k 个元素反转,就能得到正确结果。这种方法不需要任何额外数组,只用到一个交换两个元素的小函数,空间复杂度稳定为 O(1)。
从原理上看,反转操作相当于把序列头尾对调。第一次全反转让原本在尾部的 k 个元素跑到了最前面,但顺序是反的;后面两次局部反转分别把这两段恢复成正确顺序。它的时间复杂度是 O(n),每个元素被移动约两次,在绝大多数场景下都足够高效。
public class RotateArray {
// 将数组从 start 到 end(含)的部分反转
private static void reverse(int[] arr, int start, int end) {
while (start < end) {
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
start++;
end--;
}
}
// 向右循环移位 k 位,空间复杂度 O(1)
public static void rotateRight(int[] arr, int k) {
int n = arr.length;
if (n == 0) {
return;
}
// 防止 k 大于数组长度,取模简化
k = k % n;
if (k == 0) {
return;
}
reverse(arr, 0, n - 1);
reverse(arr, 0, k - 1);
reverse(arr, k, n - 1);
}
public static void main(String[] args) {
int[] data = {1, 2, 3, 4, 5};
rotateRight(data, 2);
for (int v : data) {
System.out.print(v + " ");
}
// 输出:4 5 1 2 3
}
}
上面的代码里,reverse 方法只用到 temp 这一个临时变量,因此无论数组多大,额外空间都不会随规模增长。需要注意的是,传入的 k 应当先做取模处理,否则当 k 大于 n 时会出现无效反转区间。对于左移,只需把 k 换成 n - (k % n) 再调用同一套逻辑即可。
滚动暂存法控制空间
如果不想做三次反转,也可以用滚动暂存的方式:每次把最后一个元素取出来,整体向右挪一格,再把暂存值放到开头,重复 k 次。这种做法思路直观,同样只用了常数个变量,但时间复杂度在极端情况下会达到 O(n * k),当 k 接近 n 时比三次反转慢不少。
为了缓解重复移动的问题,可以改为按环移动:计算最大公约数 gcd(n, k),把数组分成若干条独立循环链,每条链上直接跳步赋值。这样每个元素只被写一次,时间复杂度回到 O(n),空间依然是 O(1)。下面给出基于最大公约数的写法。
public class RotateByGcd {
private static int gcd(int a, int b) {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a;
}
// 向右循环移位 k 位,按环跳跃,空间 O(1)
public static void rotate(int[] arr, int k) {
int n = arr.length;
if (n == 0) {
return;
}
k = k % n;
if (k == 0) {
return;
}
int cycles = gcd(n, k);
for (int start = 0; start < cycles; start++) {
int current = start;
int prev = arr[start];
do {
int next = (current + k) % n;
int temp = arr[next];
arr[next] = prev;
prev = temp;
current = next;
} while (current != start);
}
}
public static void main(String[] args) {
int[] data = {1, 2, 3, 4, 5, 6, 7};
rotate(data, 3);
for (int v : data) {
System.out.print(v + " ");
}
// 输出:5 6 7 1 2 3 4
}
}
这段代码中,cycles 表示独立循环链的数量。每条链从 start 出发,沿着加 k 取模的路径走,直到回到原点。prev 保存上一次被挤掉的值,整个过程中只借助了 prev、temp 等固定数量的变量。相比简单滚动,它避免了同一个元素被反复搬动,在 k 较大时优势明显。
方法对比与选用建议
从工程角度看,三次反转法代码最短、最不容易写错,适合面试快速实现以及对性能要求不极端的业务。按环跳跃法在理论时间上更优,但下标推算稍复杂,维护时可读性略差。二者空间复杂度都是 O(1),都不会因为数组变大而多申请内存。
如果数组元素类型是重量级对象,反转法和跳跃法都只交换引用而不发生拷贝,依然安全。需要留意的是,Java 里数组是定长结构,上述算法都直接修改原数组;若调用方不允许改动入参,就只能在方法内新建一个长度相同的数组做中转,那样空间复杂度会变为 O(n),这一点要在接口设计时就讲清楚。
| 方法 | 时间复杂度 | 空间复杂度 | 实现难度 |
|---|---|---|---|
| 三次反转 | O(n) | O(1) | 低 |
| 简单滚动 | O(n*k) | O(1) | 低 |
| 按环跳跃 | O(n) | O(1) | 中 |
总结来说,在 Java 中控制数组循环移位的空间占用,核心就是拒绝再分配一个同等规模的数组。掌握反转与跳跃两种原地算法,就能在内存敏感或数据量大的场景下写出更稳的代码。