【LeetCode Hot100 刷题日记 (15/100)】189. 轮转数组 —— 数组、双指针、数学、原地操作🔄

72 阅读5分钟

🔄 题目链接:leetcode.cn/problems/ro…

🔍 难度:中等 | 🏷️ 标签:数组、双指针、数学、原地操作

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)


🧠 题目分析

给定一个整数数组 nums,要求将数组中的元素 向右轮转 k 个位置。注意:

  • 轮转是“循环”的:超出末尾的元素会回到开头;
  • k 可能大于数组长度,因此实际有效轮转次数为 k % n
  • 题目要求 原地修改(in-place),即不能使用额外 O(n) 空间(除非作为进阶对比);
  • 进阶明确指出:至少要有三种解法,且必须掌握 O(1) 空间的原地算法。

这道题看似简单,但考察了对 数组索引映射、模运算、循环节、原地操作技巧 的综合理解,是面试高频题!


🔑 核心算法及代码讲解:数组翻转法(推荐!)

这是最优雅、最易实现、且满足 O(1) 空间要求的解法。其核心思想基于一个观察:

向右轮转 k 位,等价于:

  1. 将整个数组反转;
  2. 将前 k 个元素反转;
  3. 将后 n - k 个元素反转。

✅ 为什么这样是对的?

假设原数组为:
[1, 2, 3, 4, 5, 6, 7]k = 3

  • 轮转后应为:[5, 6, 7, 1, 2, 3, 4]
  • 注意:最后 3 个元素 [5,6,7] 跑到了前面

如果我们先整体翻转:
[7, 6, 5, 4, 3, 2, 1]

此时,原来的“尾巴” [5,6,7] 变成了“头部”,但顺序反了;
原来的“头部” [1,2,3,4] 变成了“尾巴”,也反了。

于是我们分别翻转前 k=3 个和后 4 个:

  • 翻转 [7,6,5][5,6,7]
  • 翻转 [4,3,2,1][1,2,3,4]

最终得到:[5,6,7,1,2,3,4]

📜 代码实现(带详细行注释)

// 翻转子数组 [start, end](闭区间)
void reverse(vector<int>& nums, int start, int end) {
    while (start < end) {
        swap(nums[start], nums[end]); // 交换首尾元素
        start++;                      // 左指针右移
        end--;                        // 右指针左移
    }
}

// 主函数:原地轮转数组
void rotate(vector<int>& nums, int k) {
    int n = nums.size();
    k %= n;                         // 处理 k >= n 的情况,取模得有效轮转数
    reverse(nums, 0, n - 1);        // 第一步:整体翻转
    reverse(nums, 0, k - 1);        // 第二步:翻转前 k 个元素
    reverse(nums, k, n - 1);        // 第三步:翻转后 n-k 个元素
}

💡 关键点k %= n 是必须的!否则当 k > n 时,k - 1 可能越界(如 k = 10, n = 7k = 3)。


🧩 解题思路(分步骤)

  1. 预处理 k:由于轮转 n 次等于没动,所以只需轮转 k % n 次;
  2. 整体翻转:使“要移到前面的尾部元素”出现在数组开头,但顺序颠倒;
  3. 局部翻转
    • 翻转前 k 个 → 恢复正确顺序;
    • 翻转后 n - k 个 → 恢复剩余部分顺序;
  4. 完成原地轮转,无需额外空间。

📊 算法分析

方法时间复杂度空间复杂度是否原地面试推荐度
额外数组O(n)O(n)⭐⭐
环状替换O(n)O(1)⭐⭐⭐
数组翻转O(n)O(1)⭐⭐⭐⭐⭐
  • 时间:三次翻转,每个元素最多被交换两次 → O(2n) = O(n)
  • 空间:仅用几个变量 → O(1)
  • 稳定性:不改变相对顺序(因为只是块移动)
  • 鲁棒性:自动处理 k = 0k = nk > n 等边界情况

🎯 面试加分项:能清晰解释“为什么三次翻转能得到结果”,并画出示意图。


💡 其他解法简述(拓展视野)

方法一:额外数组(简单直观)

  • 新建数组 newArrnewArr[(i + k) % n] = nums[i]
  • 缺点:空间 O(n),不符合进阶要求
  • 适合快速 AC 或作为验证手段

方法二:环状替换(数学思维强)

  • 利用 最大公约数 gcd(n, k) 确定需要多少个“环”
  • 每个环内通过 temp 临时变量完成轮换
  • 难点:理解为何环的数量是 gcd(n, k)
  • 优点:真正“逐元素移动”,无冗余操作
  • 缺点:代码易错,边界复杂

📌 面试建议:优先讲 翻转法,因其简洁、安全、易写;若被追问“还有别的方法吗?”,再提环状替换展示深度。


💻 完整可运行代码(含测试)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 翻转子数组 [start, end]
void reverse(vector<int>& nums, int start, int end) {
    while (start < end) {
        swap(nums[start], nums[end]);
        start++;
        end--;
    }
}

// 原地轮转数组
void rotate(vector<int>& nums, int k) {
    int n = nums.size();
    k %= n;
    reverse(nums, 0, n - 1);
    reverse(nums, 0, k - 1);
    reverse(nums, k, n - 1);
}

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 测试用例 1
    vector<int> nums1 = {1,2,3,4,5,6,7};
    rotate(nums1, 3);
    for (int x : nums1) cout << x << " "; // 输出: 5 6 7 1 2 3 4
    cout << "\n";

    // 测试用例 2
    vector<int> nums2 = {-1,-100,3,99};
    rotate(nums2, 2);
    for (int x : nums2) cout << x << " "; // 输出: 3 99 -1 -100
    cout << "\n";

    // 边界测试:k = 0
    vector<int> nums3 = {1,2};
    rotate(nums3, 0);
    for (int x : nums3) cout << x << " "; // 输出: 1 2
    cout << "\n";

    // 边界测试:k > n
    vector<int> nums4 = {1,2,3};
    rotate(nums4, 5); // 5 % 3 = 2
    for (int x : nums4) cout << x << " "; // 输出: 2 3 1
    cout << "\n";

    return 0;
}

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第16题 ——238. 除自身以外数组的乘积 —— 前缀积与后缀积(空间优化)

📌 题目链接:leetcode.cn/problems/pr…

🔍 难度:中等 | 🏷️ 标签:数组、前缀积、后缀积、双指针、原地操作

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1) (不计入输出数组)


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!