【力扣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,或修改翻转顺序

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


相关推荐
倒头就睡的小比特2 天前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!2 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
猎头南楼2 天前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
旖旎夜光2 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark2 天前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her12 天前
并查集-听课笔记
笔记·算法·并查集
码流子2 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
another heaven2 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
wzdark2 天前
从算法设计模式看编程思维的抽象能力4
算法
2601_962218612 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法