题目描述
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例
示例 1:
输入: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]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
提示:
- 1 <= nums.length <= 10^5
- -2^31 <= numsi <= 2^31 - 1
- 0 <= k <= 10^5
解题思路总览
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|---|
| 三次翻转 | 先整体翻转,再分别翻转两段 | O(n) | O(1) | 推荐解法,最优 |
| 环状替换 | 每个元素放到最终位置 | O(n) | O(1) | 需要处理 gcd |
| 暴力法 | 每次轮转一个元素 | O(n*k) | O(1) | 会超时 |
| 额外数组 | 借助临时数组 | O(n) | O(n) | 最简单但空间不优 |
一、三次翻转法(最常用)
核心思想
通过三次反转操作,将数组向右轮转 k 位:
原数组: [1,2,3,4,5,6,7], k = 3
第一步:整体翻转
reverse(nums, 0, n-1)
[7,6,5,4,3,2,1]
第二步:翻转前 k 个元素
reverse(nums, 0, k-1)
[5,6,7,4,3,2,1]
第三步:翻转后 n-k 个元素
reverse(nums, k, n-1)
[5,6,7,1,2,3,4]
结果:[5,6,7,1,2,3,4]
图解
nums = [1,2,3,4,5,6,7], k = 3
初始状态:
[1, 2, 3, 4, 5, 6, 7]
0 1 2 3 4 5 6
第一步:reverse(0, 6)
[7, 6, 5, 4, 3, 2, 1]
第二步:reverse(0, 2)
[5, 6, 7, 4, 3, 2, 1]
第三步:reverse(3, 6)
[5, 6, 7, 1, 2, 3, 4]
输出: [5,6,7,1,2,3,4]
代码实现
cpp
class Solution {
public:
void rotate(vector<int>& nums, int k) {
k %= nums.size(); // 取模,防止 k 大于数组长度
ranges::reverse(nums); // 整体翻转 [1,2,3,4,5,6,7] -> [7,6,5,4,3,2,1]
reverse(nums.begin(), nums.begin() + k); // 翻转前 k 个 [7,6,5] -> [5,6,7]
reverse(nums.begin() + k, nums.end()); // 翻转后 n-k 个 [4,3,2,1] -> [1,2,3,4]
}
};
二、算法流程图
详细过程(以 nums = 1,2,3,4,5,6,7, k = 3 为例)
输入: nums = [1,2,3,4,5,6,7], k = 3
Step 1: k %= n
n = 7, k = 3 % 7 = 3
Step 2: ranges::reverse(nums)
翻转整个数组
[1,2,3,4,5,6,7] -> [7,6,5,4,3,2,1]
Step 3: reverse(nums.begin(), nums.begin()+k)
翻转前 3 个元素 [7,6,5]
[7,6,5,4,3,2,1] -> [5,6,7,4,3,2,1]
Step 4: reverse(nums.begin()+k, nums.end())
翻转后 4 个元素 [4,3,2,1]
[5,6,7,4,3,2,1] -> [5,6,7,1,2,3,4]
输出: [5,6,7,1,2,3,4]
边界情况处理
情况 1: k = 0
k %= n = 0
整体翻转 + 翻转前 0 个 + 翻转后 n 个 = 整体翻转再整体翻转 = 不变
结果正确
情况 2: k = n(数组长度)
k %= n = 0
相当于没有轮转
结果正确
情况 3: k > n
例如 n=7, k=10
k %= n = 3
等价于轮转 3 位
结果正确
三、逐行解析
cpp
k %= nums.size();
- 取模,防止 k 大于数组长度
- 例如 n=7, k=10,k %= 7 = 3,等价于轮转 3 位
- 当 k = 0 或 k = n 时,结果和原数组一样
cpp
ranges::reverse(nums);
- C++20 的
ranges::reverse,整体翻转数组 [1,2,3,4,5,6,7]->[7,6,5,4,3,2,1]- 也可以用
reverse(nums.begin(), nums.end())
cpp
reverse(nums.begin(), nums.begin() + k);
- 翻转前 k 个元素
[7,6,5,4,3,2,1]->[5,6,7,4,3,2,1]- 此时前 k 位已经在正确位置
cpp
reverse(nums.begin() + k, nums.end());
- 翻转后 n-k 个元素
[5,6,7,4,3,2,1]->[5,6,7,1,2,3,4]- 完成所有调整
四、为什么三次翻转能实现轮转?
数学证明
设数组长度为 n,轮转 k 位。
整体翻转后:原数组 [0...n-1] 变为 [n-1...0]
前 k 位翻转:[n-1...n-k] 变为 [n-k...n-1]
后 n-k 位翻转:[n-k-1...0] 变为 [n-k...n-1...0]
最终结果:[n-k...n-1] + [0...n-k-1]
= 轮转后的正确顺序
证毕。
直观理解
原始数组: [1,2,3,4,5,6,7]
目标: [5,6,7,1,2,3,4]
= 后k位 + 前n-k位
观察:
- 后 k 位 [5,6,7] 在原数组中是前 n-k 位
- 前 n-k 位 [1,2,3,4] 在原数组中是后 n-k 位
三次翻转的作用:
1. 整体翻转 -> 把顺序反过来
2. 翻转前 k -> 把后 k 位翻到前面(但顺序反了)
3. 翻转后 n-k -> 把前 n-k 位翻到后面(顺序也反了)
两个"反了"抵消,最终得到正确顺序。
五、环状替换法
核心思想
每个元素直接放到最终位置,通过换手处理。
nums = [1,2,3,4,5,6,7], k = 3
数组下标对应关系:
原位置 0 -> 新位置 (0+k) % n = 3
原位置 1 -> 新位置 (1+k) % n = 4
原位置 2 -> 新位置 (5) % n = 5
原位置 3 -> 新位置 (6) % n = 6
原位置 4 -> 新位置 (0) % n = 0 (回到起点)
原位置 5 -> 新位置 (1) % n = 1
原位置 6 -> 新位置 (2) % n = 2
从位置 0 开始,顺时针走:
0 -> 3 -> 6 -> 2 -> 5 -> 1 -> 4 -> 0
形成一个环,共 7 个元素
cpp
class Solution {
public:
void rotate(vector<int>& nums, int k) {
k %= nums.size();
int n = nums.size();
int count = 0; // 已放置的元素个数
for (int start = 0; count < n; start++) {
int curr = start;
int prev = nums[start];
do {
int next = (curr + k) % n;
int temp = nums[next];
nums[next] = prev;
prev = temp;
curr = next;
count++;
} while (curr != start);
count++; // 补偿最后一次多减的
}
}
};
与三次翻转法对比
| 维度 | 三次翻转 | 环状替换 |
|---|---|---|
| 代码复杂度 | 简单,3行搞定 | 复杂,需要处理环 |
| 空间复杂度 | O(1) | O(1) |
| 时间复杂度 | O(n) | O(n) |
| 边界处理 | 自动处理 | 需要处理 gcd 防止死循环 |
六、复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|
| 三次翻转 | O(n) | O(1) | 推荐,最简单 |
| 环状替换 | O(n) | O(1) | 需要小心处理 |
| 暴力法 | O(n*k) | O(1) | 会超时 |
| 额外数组 | O(n) | O(n) | 最简单但空间不优 |
详细分析:
三次翻转:
翻转 1: O(n)
翻转 2: O(k)
翻转 3: O(n-k)
总计: O(n) + O(k) + O(n-k) = O(n)
空间:
原地翻转,使用常数额外空间 O(1)
七、面试追问 FAQ
| 问题 | 回答要点 |
|---|---|
| Q: 为什么三次翻转能实现轮转? | 整体翻转改变顺序,前后分别翻转抵消了整体翻转带来的逆序,最终得到正确顺序 |
| Q: k %= n 的作用是什么? | 防止 k 大于数组长度,k=10, n=7 等价于 k=3,避免不必要的翻转 |
| Q: 能否只翻转一次? | 不能,一次翻转只能反转顺序,无法实现轮转 |
| Q: 三次翻转的时间复杂度是多少? | O(n),三次翻转的总时间之和为 O(n) |
| Q: 环状替换和三次翻转哪个更好? | 三次翻转代码更简单,面试中推荐;环状替换需要处理 gcd,较复杂 |
| Q: 如何向左轮转? | 将 k 改为 n-k,或者修改翻转顺序:先翻前后两段,再整体翻转 |
| Q: 能用队列模拟吗? | 可以,每次 pop_front 再 push_back,但时间复杂度 O(n*k),会超时 |
八、相关题目
| 题目编号 | 题目名称 | 难度 | 核心差异 |
|---|---|---|---|
| 189 | 轮转数组 | 中等 | 基础题,向右轮转 |
| 61 | 旋转链表 | 中等 | 旋转链表,非数组 |
| 151 | 反转字符串中的单词 | 中等 | 先整体翻转再分段翻转 |
| 796 | 旋转字符串 | 简单 | 判断是否为旋转串 |
| 剑指 Offer 58 | 左旋转字符串 | 简单 | 向左轮转 |
九、总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 三次翻转:整体 -> 前k -> 后n-k |
| 关键操作 | k %= n 防止越界 |
| 时间复杂度 | O(n)(三次翻转) |
| 空间复杂度 | O(1)(原地操作) |
| 代码行数 | 仅 4 行,简洁高效 |
| 注意事项 | k 可能大于 n,需要取模 |
| 变形 | 向左轮转:将 k 改为 n-k,或修改翻转顺序 |
三次翻转法是轮转数组的最优解,代码简洁,面试中容易通过,建议熟练掌握。