LeetCode 189 这道题,我刷第一遍的时候差点直接跳过。轮转数组,不就是把数组往右挪几位吗?有什么好讲的。但后来拿它去带新人、自己也作为面试官用它出题,才发现这道题是把数组下标、取模、逆序反转、空间复杂度串联起来的绝佳训练题。它难度标签只是 Medium,但能把这题讲清楚的人,基本能在数组类问题上绕开大多数坑。
题目本身一句话就能概括:给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。比如 [1,2,3,4,5,6,7] 向右轮转 3 位,结果是 [5,6,7,1,2,3,4]。很多人一看到这个,第一反应就是“直接再申请一个数组,按下标映射放过去”,这确实是最容易想到的方案。但这题的难点和趣味全藏在另一个地方:如果题目要求只能用 O(1) 额外空间,你还能不能做?
1. 把题意翻译成人话:轮转的本质是一个平移问题
1.1 先分清“右移”和“左移”的公式
面试里遇到过不少候选人,上来就说这题简单,然后就把方向写反了。所以第一个要确认的事,就是“向右轮转”这四个字到底在说哪个方向。
从数组下标来看,向右轮转 k 位的意思是:原来在下标 i 的元素,移动后应该落在下标 (i + k) % n。以 [1,2,3,4,5,6,7]、k=3 为例,下标 0 上的数字 1 最终应该到下标 3,下标 4 上的数字 5 最终到下标 (4+3)%7=0,所以新数组开头是 5。
反过来,如果是“向左轮转 k 位”,目标下标就是 (i - k + n) % n。这里为什么要先加一个 n 再取模?因为很多编程语言对负数取模的结果并不统一。比如 Java 和 C++ 里 -1 % 7 的结果是 -1,而不是 6。如果直接写 (i - k) % n,下标一旦变负,程序就访问到错误位置了。有人觉得这是小事,但这类边界问题恰恰是线上 bug 最常见的来源。
1.2 两段视角:元素视角和区间视角
轮转操作可以从两个角度看:
- 元素视角:每个元素独立向右移动 k 步,越界的绕回数组头部。
- 区间视角:把数组从某个位置切成两段,交换两段的前后位置。
元素视角对应的是“额外数组”和“环状替换”的解法,区间视角对应的是“三次反转”解法。初学者通常只能看到元素视角,于是会写出暴力循环或申请新数组的代码。而真正高效的解法,往往需要跳出单个元素的层面,从整体结构上观察:右移 k 位,本质上就是让数组从“前 n-k 个元素 + 后 k 个元素”变成“后 k 个元素 + 前 n-k 个元素”。
换句话说,原数组可以看作 AB,其中 A 的长度是 n-k,B 的长度是 k。向右轮转 k 位的结果就是 BA。这个抽象非常关键,后面讲三次反转时会反复用到。
1.3 k 的范围决定了代码的第一道坎
题目说 k 是非负整数,但没说 k 一定小于数组长度。LeetCode 的测试用例里就有 nums = [1,2]、k = 3 这种情况,实际等价于轮转 3 % 2 = 1 次。所以无论用哪种解法,第一行几乎都逃不掉:
java复制k %= n;
如果不做取模,三次反转时 reverse(nums, 0, k-1) 里的 k-1 很可能直接越界;环状替换里虽然每次都会 % n,但链路会变得冗余,逻辑更难讲清楚。这条规则属于“先做归一化,再处理核心逻辑”的典型工程思维,在数组轮转、循环队列、分页计算里都能用上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先拿能过的方案打底:额外数组与朴素移位的界限
2.1 朴素移位为什么在面试中不建议写
很多人看到这道题,第一反应是:每次把最后一个元素摘出来,整体往后挪一位,再放到开头,重复 k 次。写成代码大概是这样:
java复制public void rotateByBrute(int[] nums, int k) {
int n = nums.length;
k %= n;
for (int i = 0; i < k; i++) {
int last = nums[n - 1];
for (int j = n - 1; j > 0; j--) {
nums[j] = nums[j - 1];
}
nums[0] = last;
}
}
这个解法在思路上没问题,但时间复杂度是 O(n * k)。当 n 和 k 都到 10^5 级别时,运算量会膨胀到 10^10,提交时直接超时。
它能提供的价值是帮助理解“右移一位”这个原子操作。在我的经验里,能写
