【力扣100题】58.轮转数组

题目描述

给定一个整数数组 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,或修改翻转顺序

三次翻转法是轮转数组的最优解,代码简洁,面试中容易通过,建议熟练掌握。


相关推荐
Navigator_Z3 小时前
LeetCode //C - 1289. Minimum Falling Path Sum II
c语言·算法·leetcode
CCblandy3 小时前
令牌桶在 0.9 倍负载超发 19%、固定窗口边界放行 20 个:四种限流算法同构实测
后端·python·算法·性能优化·架构
mmmmath_33 小时前
LeetCode.104.二叉树的最大深度
数据结构·leetcode
YangYang9YangYan4 小时前
2027供应链计划校招:数学与应用数学专业如何把预测模型讲成业务决策?
人工智能·sql·算法
jsjzsl24 小时前
递归自改进系统(RSI)的本质是自发对称破缺:智能爆炸有界—— AI研发切记盲目投入
人工智能·python·算法
程序员清风4 小时前
最短路径算法:BFS、Dijkstra 与 Floyd
java·算法·宽度优先
梦帮科技4 小时前
Kali Linux Scapy 畸形报文构造学——深度解剖非法 TCP Flags 与反常序列号
linux·爬虫·神经网络·算法·数学建模·系统架构·模拟退火算法
171320330计算机毕设编程6 小时前
计算机毕业设计 基于YOLO改进算法的车辆特征识别系统设计与实现--lw
java·大数据·python·sql·算法
姜鱼问生6 小时前
Nginx gzip 压缩:一个配置让页面加载快 3 倍
后端·算法·架构