189. 轮转数组

4 阅读5分钟

一、 题目与代码重现

题目描述

  • 题目名称:189. 轮转数组

  • 难度:中等

  • 题目要求:给定一个整数数组 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 <= nums.length <= 10^5
    • -2^31 <= nums[i] <= 2^31 - 1
    • 0 <= k <= 10^5

原始代码(JavaScript)

javascript

var rotate = function(nums, k) {
    const n = nums.length;
    k = k % n;
    const temp = new Array(n);
    
    // 将元素放到新位置
    for (let i = 0; i < n; i++) {
        temp[(i + k) % n] = nums[i];
    }
    
    // 复制回原数组
    for (let i = 0; i < n; i++) {
        nums[i] = temp[i];
    }
};

二、 标准解题逻辑步骤

核心思想:辅助数组映射法

向右轮转 k 位,本质上是将原数组 nums 的元素重新排列:
原本在位置 i 的元素,轮转后应移动到 (i + k) % n 的位置(因为向右移动)。

详细流程

  1. 归一化 k
    由于轮转 n 次会回到原数组,因此 k 对 n 取模,k = k % n,减少不必要的操作。

  2. 创建辅助数组 temp
    初始化一个与 nums 等长的空数组,用于暂存轮转后的结果。

  3. 映射填充
    遍历原数组的每个索引 i

    • 计算目标位置 target = (i + k) % n
    • 将 nums[i] 赋值给 temp[target]
  4. 写回原数组
    将 temp 中的所有元素依次复制回 nums,完成原地修改(虽然用了额外空间,但从函数外部看,nums 已被修改)。

注意:虽然用了 O(n) 额外空间,但题目要求的"原地"通常指不返回新数组,且数组内容在函数返回后改变。这种做法是大多数解法中最直观、最安全的方式。


三、 API 与语法深度解析

语法 / API作用参数说明在该题中的场景
new Array(n)创建一个长度为 n 的空数组n:数组长度创建临时数组存储轮转结果
k = k % n取模运算,得到实际轮转步数左侧 k 重新赋值为余数当 k >= n 时,减少冗余轮转
(i + k) % n计算轮转后的新索引i:原索引,k:步数确保索引始终在 [0, n-1] 范围内
nums[i] = temp[i]数组元素赋值右侧值赋给左侧数组元素将临时数组内容复制回原数组

注意% 在 JavaScript 中对于负数也返回非负余数(与被除数同号),但这里 i 和 k 均为非负,所以结果正确。


四、 得分重点与理解难点划分

得分重点

  • 正确处理 k > nums.length 的情况(取模)。
  • 能够实现元素位置映射的推导公式 (i + k) % n
  • 保证修改原数组(无论是否使用辅助空间)。

理解难点

  • 轮转方向:向右轮转 k 位 ≠ 向右移动每个元素 k 格(而是整体循环移动)。
  • 索引映射的取模运算:初学者容易写成 (i - k) % n 或忘记取模。
  • 为什么需要 k = k % n:如果 k = n,数组等于没变,但执行轮转会浪费性能。

五、 极易踩错的细节与边界错误

边界条件

  • 数组长度为 1n = 1,任何 k 取模后都是 0,数组不变。但如果代码中没有处理 k % n(例如 k = 100000),可能导致数组越界或无限循环。
  • k = 0k % n = 0(i + 0) % n = i,数组保持原样,复制回 nums 后结果不变。
  • k 大于等于 n:取模后变成较小值,避免无效移动。

常见逻辑陷阱

  • 直接使用 nums 作为源同时修改:如果尝试原地交换会弄乱数据,所以必须借助辅助数组或三次反转法。
  • 忘记 return 或返回新数组:题目要求修改原数组,但函数不要求返回值(void)。如果返回了 temp,用例会判断 nums 未改变而报错。

超时/内存风险

  • nums.length 最大 10^5,本解法 O(n) 时间和空间,完全可接受。
  • 如果用 splice 和 push 反复操作,会因 O(n^2) 而超时。

六、 复杂度对比与优化方案

基础写法对比

  • 暴力法(每次右移 1 位,重复 k 次):

    • 时间复杂度:O(n * k),当 k 接近 n 时退化到 O(n^2)
    • 空间复杂度:O(1)
    • ❌ 在限制条件下(n=10^5k=10^5)会严重超时。
  • 辅助数组法(本题代码)

    • 时间复杂度:O(n)
    • 空间复杂度:O(n)
    • ✅ 直观且安全,面试中容易快速写出。

最优解:三次反转法(原地,O(1) 空间)

核心思路

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

原理
原数组 [1,2,3,4,5,6,7]k=3

  • 整体反转 → [7,6,5,4,3,2,1]
  • 反转前 3 个 → [5,6,7,4,3,2,1]
  • 反转后 4 个 → [5,6,7,1,2,3,4] ✅

优化后代码(JavaScript)

javascript

var rotate = function(nums, k) {
    const n = nums.length;
    k = k % n;
    if (k === 0) return;  // 可提前结束

    // 辅助反转函数
    function reverse(left, right) {
        while (left < right) {
            [nums[left], nums[right]] = [nums[right], nums[left]];
            left++;
            right--;
        }
    }

    reverse(0, n - 1);
    reverse(0, k - 1);
    reverse(k, n - 1);
};

复杂度

  • 时间:O(n)(三次遍历,每次反转部分元素)
  • 空间:O(1)(仅用常数变量)

对比结论:若面试要求原地修改且空间越少越好,三次反转法是最佳选择。若允许 O(n) 空间,辅助数组法更易理解且不易出错,通常也能通过力扣的判定。