Appearance
实战:旋转数组
这道题有一个非常巧妙的解法:三次翻转。
这个技巧在很多数组和字符串题目中都会用到,值得记住。
题目描述
LeetCode 189. 旋转数组
给定一个整数数组
nums,将数组中的元素向右轮转k个位置,其中k是非负数。
示例:
输入: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]进阶:你可以使用空间复杂度为 O(1) 的原地算法解决这个问题吗?
题目分析
关键点:
- 向右轮转:每个元素向右移动 k 位,超出末尾的回到开头
- 原地修改:不能创建新数组
- k 可能大于数组长度:需要取模处理
先想一下:轮转 k 位之后,数组变成什么样?
原数组: [1, 2, 3, 4, 5, 6, 7]
|← 前 n-k 个 →| |← 后 k 个 →|
轮转后: [5, 6, 7, 1, 2, 3, 4]
|← 后 k 个 →| |← 前 n-k 个 →|就是把后 k 个元素移到前面。
解法一:使用额外数组
最直观的方法:创建一个新数组,每个元素放到它应该在的位置。
javascript
function rotate(nums, k) {
const n = nums.length;
k = k % n; // 处理 k > n 的情况
const temp = new Array(n);
for (let i = 0; i < n; i++) {
// 元素 i 移动到 (i + k) % n 的位置
temp[(i + k) % n] = nums[i];
}
// 复制回 nums
for (let i = 0; i < n; i++) {
nums[i] = temp[i];
}
}复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(n)
简单直观,但用了额外空间。能不能 O(1) 空间?
解法二:三次翻转(最优解)
这是一个非常巧妙的方法。
观察一下目标数组:
原数组: [1, 2, 3, 4 | 5, 6, 7]
前 4 个 后 3 个
目标: [5, 6, 7 | 1, 2, 3, 4]
后 3 个 前 4 个如果我们:
- 先把整个数组翻转
- 再把前 k 个翻转
- 再把后 n-k 个翻转
看看会发生什么:
原数组: [1, 2, 3, 4, 5, 6, 7]
Step 1 - 整体翻转:
[7, 6, 5, 4, 3, 2, 1]
Step 2 - 翻转前 k=3 个:
[5, 6, 7, 4, 3, 2, 1]
Step 3 - 翻转后 n-k=4 个:
[5, 6, 7, 1, 2, 3, 4] ✓完美!
为什么这样做是对的?
整体翻转后,原来的后 k 个跑到了前面,但顺序是反的。原来的前 n-k 个跑到了后面,顺序也是反的。
分别把它们翻转回来,就得到了正确的结果。
javascript
function rotate(nums, k) {
const n = nums.length;
k = k % n;
// 辅助函数:翻转数组的 [start, end] 区间
const reverse = (start, end) => {
while (start < end) {
[nums[start], nums[end]] = [nums[end], nums[start]];
start++;
end--;
}
};
reverse(0, n - 1); // 翻转整个数组
reverse(0, k - 1); // 翻转前 k 个
reverse(k, n - 1); // 翻转后 n-k 个
}复杂度分析:
- 时间复杂度:O(n)——每个元素翻转两次,总共 2n 次操作
- 空间复杂度:O(1)——只用了几个变量
这是最优解。代码简洁,空间最优。
解法三:环状替换
还有一种方法:直接把每个元素放到它应该在的位置。
问题是,放过去之后,原来在那个位置的元素怎么办?
答案是:把它也移到它应该在的位置。这样一直移下去,最终会回到起点,形成一个"环"。
javascript
function rotate(nums, k) {
const n = nums.length;
k = k % n;
let count = 0; // 已移动的元素数
for (let start = 0; count < n; start++) {
let current = start;
let prev = nums[start];
do {
const next = (current + k) % n;
[nums[next], prev] = [prev, nums[next]];
current = next;
count++;
} while (current !== start);
}
}这个方法比较难理解,在某些情况下(k 和 n 有公约数时)需要多个环才能覆盖所有元素。
面试中推荐使用三次翻转法,更容易解释。
三种方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 额外数组 | O(n) | O(n) | 最简单直观 |
| 三次翻转 | O(n) | O(1) | 空间最优,推荐 |
| 环状替换 | O(n) | O(1) | 巧妙但较难理解 |
常见错误
- 没有处理 k > n
javascript
// 错误:k=10, n=7 时会出问题
k = k % n; // 必须加这一行- 翻转边界错误
javascript
// 正确的边界
reverse(0, k - 1); // 前 k 个是 [0, k-1]
reverse(k, n - 1); // 后 n-k 个是 [k, n-1]- k = 0 的情况
当 k = 0 或 k = n 时,数组不需要变化。k % n = 0 会自动处理这种情况。
相关题目
- 61. 旋转链表:链表版本
- 186. 反转字符串中的单词 II:也用到翻转技巧
本章小结
这一章我们学习了三次翻转技巧:
- 目标:把后 k 个元素移到前面
- 方法:整体翻转 → 翻转前 k 个 → 翻转后 n-k 个
- 为什么有效:翻转可以"交换"两部分的位置
记住这个技巧。当你需要"把数组的一部分移到另一个位置"时,翻转往往是最优雅的解法。
下一章,我们来看「加一」这道题。