给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例:[1,2,3,4,5,6,7], k=3 → [5,6,7,1,2,3,4]。
进阶:尽可能想出更多的解决方案,至少有三种;空间复杂度 O(1) 的算法。
题目进阶点名要 O(1) 空间 —— 这句话把"开新数组按位搬运"(O(n) 空间、最直觉)直接排除,剩下的主角就是
三次翻转。它是这类题的面试首选:不需要任何数论,六次交换讲完。
一、先取模,再看那个 Python 语言坑
k 必须先对 n 取模:轮转 n 位等于没转,
k 完全可能比
n 大(
k=100 配单元素数组)。取模后
k ∈ [0, n-1],若
k = 0 直接返回。
顺带一个真实的语言坑:Python 圈流行的一行解法是
nums[:] = nums[-k:] + nums[:-k]
它在 k = 0 时是错的:
-0 就是
0,
nums[-0:] 是
整个数组,结果变成"数组接上自己"。修法是先取模并特判(或写
nums[n-k:] —— 但
k % n == 0 时
n-k = n,
nums[n:] 是空的、
nums[:n] 是全数组,正好对):
def rotate(nums, k):
n = len(nums)
k %= n
if k == 0:
return
nums[:] = nums[n - k:] + nums[:n - k]
nums[:] = ... 这个写法本身就是
原地修改的 Python 惯用法(切片赋值改的是原对象的内存内容)—— 和三次翻转一样满足 O(1) 额外空间吗?不:右侧拼接了新列表,仍是 O(n) 辅助空间。
它只是"看起来原地"。要真 O(1),往下看。
二、三次翻转:把"轮转"分解成两次反转
2.1 核心观察
轮转 k 位 =
把数组在 n-k 处切成两段,交换两段的左右位置:
[1,2,3,4 | 5,6,7] → [5,6,7 | 1,2,3,4]
直接交换两段要 O(n) 暂存。但换个视角:
先整体翻转 —— 两段的内部顺序全部倒过来,同时两段恰好左右就位:
[1,2,3,4,5,6,7] 整体翻转 → [7,6,5 | 4,3,2,1]
↑两段就位,但内部全反
再各自翻回内部顺序:
[7,6,5] → [5,6,7] [4,3,2,1] → [1,2,3,4]
得 [5,6,7,1,2,3,4] ✓
为什么成立:
反转是自己的逆运算(翻两次 = 恒等)。整体翻转完成了"位置交换",代价是把两段内部的顺序搞反了;每段各自再翻一次,内部顺序复原 —— 两次反转恰好一个管位置、一个管顺序,互不干扰。
2.2 逐帧推演
官方例
[1,2,3,4,5,6,7],
k=3(与状态机
rotateSteps.js 的 8 帧实际输出逐行核对过,每帧是一对交换):
第 4 帧有个细节:
"前 k 个"区间 [0,2] 长度 3,只需 1 次交换(中间的 6 不用动)。翻转次数是
floor(区间长度 / 2) —— 三轮合计
floor(7/2) + floor(3/2) + floor(4/2) = 3 + 1 + 2 = 6 次交换,每个元素恰好动 0 或 1 次。
三、三次翻转为什么是 O(1)
每一步都是
双指针向中间收拢的原地交换:
i 从区间左端、
j 从右端,交换后各进一格,相遇即止。全程只用了
i、
j、一个临时变量 ——
O(1) 辅助空间。
时间上每轮扫描半个区间,三轮合计正好 O(n):每个元素被读写常数次。
对照另外两种 O(1) 空间解法:
环状替换的正确性论证依赖
gcd(n, k) 把下标分成若干个置换环 —— 数学上漂亮,工程上三次翻转完胜。
面试先写三次翻转,环状替换作为"我知道还有一种"的加分项。
四、Java 版
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n; // k 可能大于 n
reverse(nums, 0, n - 1); // ① 整体
reverse(nums, 0, k - 1); // ② 前 k
reverse(nums, k, n - 1); // ③ 后 n-k
}
private void reverse(int[] a, int i, int j) {
while (i < j) {
int t = a[i]; a[i] = a[j]; a[j] = t;
i++; j--;
}
}
}
注意
k %= n 之后如果
k == 0,三个 reverse 各自空转(区间长 0 或 1),天然安全 —— 不需要特判。
这是三次翻转比切片法稳的地方:切片法的 k=0 是真坑,翻转法的 k=0 只是空转。
五、边界情况与常见坑
坑一:忘了取模。 k = 100 配
n = 1:翻转区间
[0, 99] 直接越界。取模是第一步,不是可选优化。
坑二:翻转的区间端点。 三轮区间是
[0, n-1] /
[0, k-1] /
[k, n-1] —— 第二轮的右端是
k-1 不是
k(前 k 个的下标是 0..k-1),第三轮的左端是
k。写错一格就是差一位的死循环或漏翻转。
坑三:nums = [1] 这类退化输入。 取模后 k = 0,三个区间
[0,0]/
[0,-1]/
[0,0] —— 第二轮区间为空,
while i < j 天然不进。翻转法对退化输入免疫。
坑四:向左轮转。 题目只考向右,但向左 k 位等价于向右
n - (k % n) 位 —— 一行换算,别重写算法。
六、变体:同一个"反转思想"
家族共性:当目标是"元素的位置重排",先问一句 ——
这个变换能不能拆成若干次"反转"的复合? 反转是原地 O(1) 的最基础操作,能拆就赢。
七、面试怎么答
- 先给思路清单:切片搬运(O(n) 空间)、逐位搬运(O(n·k))、环状替换、三次翻转 —— 展示你知道全部选项;
- 写三次翻转:先
k %= n,三行 reverse;
- 主动讲原理:反转是自己的逆运算,整体翻转管"位置交换"、分段翻转管"顺序恢复";
- 主动踩坑自证:
k %= n 防越界;Python 切片法的 nums[-0:] 陷阱;k=0 时翻转法天然安全;
- 自测:
rotate([1,2,3,4,5,6,7], 3) == [5,6,7,1,2,3,4]、rotate([1,2], 5) == [2,1]、rotate([1,2,3], 3) == [1,2,3](k = n 不变);
- 一句话总结:轮转 = 两段交换位置 = 整体反转 + 两段各自反转 —— 反转是自己的逆运算,所以位置和顺序可以分开还原。
系列上一篇:【LC 33 / 153】旋转数组二分 —— 同样面对"旋转",上一篇是在旋转后的数组上搜索(二分),这一篇是制造旋转本身。本文动画 8 帧(6 次交换逐帧看),与状态机单测(20 例,与切片 / 逐位搬运两个独立参考解三向交叉验证)逐行核对过。