🧠 LeetCode 189 | Rotate Array:从新数组到三次反转,一文讲透原地旋转的精髓

3 阅读4分钟

本文将带你彻底搞懂 LeetCode 第 189 题《Rotate Array》——如何将一个数组向右旋转 k 个位置。我们将从最直观的“新开数组”开始,一步步推导出最优解“三次反转法”,并在过程中帮你理解:为什么只需三次翻转,就能完成复杂的元素重排?


🔍 问题描述

给定一个整数数组 nums 和一个非负整数 k,请将数组向右旋转 k 个位置。

示例:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]

💡 注意:LeetCode 要求的是 原地修改(in-place),即不能创建新数组,只能直接修改原数组。


✅ 解法一:开辟新数组(直观易懂)

这是大多数初学者的第一反应:先算出每个元素的新位置,再放到新数组里

🧠 思路

对于原数组中的第 i 个元素,它在旋转后应该出现在位置 (i + k) % n

所以我们可以:

  1. 创建一个新数组 newArr
  2. 遍历原数组,把每个元素放到它“该去的位置”
  3. 最后再把 newArr 的内容拷贝回原数组

💻 代码实现

var rotate = function(nums, k) {
    const n = nums.length;
    k = k % n; // 处理 k > n 的情况

    const newArr = new Array(n);
    for (let i = 0; i < n; i++) {
        newArr[(i + k) % n] = nums[i];
    }

    // 拷贝回原数组
    for (let i = 0; i < n; i++) {
        nums[i] = newArr[i];
    }
};

⚖️ 复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

✅ 优点:逻辑清晰,容易理解和实现
❌ 缺点:使用了额外空间,不符合“原地修改”的要求


❓ 那我们如何实现原地修改呢?

这是很多人的下一个疑问:能不能不新建数组,只用原数组本身完成旋转?

如果我们不能开新空间,就不能“先存再放”。那怎么办?

我们需要一种不依赖临时存储的方法,来重新排列元素。

这时候,就轮到一个经典算法登场了——三次反转法(Three Reversal Algorithm)


🚀 解法二:原地修改 —— 三次反转法(高效优雅)

这可能是我见过最巧妙的数组操作之一。

🌟 核心思想

右旋转 k 位,本质是:

  • 把数组分成两部分:n−k 个元素(A)k 个元素(B)
  • 目标是变成:B + A

但问题是:我们不能直接搬动整块数据。

于是,我们换一种思路:先打乱顺序,再局部纠正


🔁 三次反转的步骤

让我们一步一步来看:

① 整体反转

reverse(nums, 0, n - 1);

→ 把整个数组倒过来,变成 rev(B) + rev(A)
此时,B 的元素在前面,A 在后面,但各自内部顺序是反的

② 反转前 k

reverse(nums, 0, k - 1);

→ 把 rev(B) 变回 B,现在是 B + rev(A)

③ 反转后 n−k

reverse(nums, k, n - 1);

→ 把 rev(A) 变回 A,最终得到 B + A


💡 举个例子

原数组:[1,2,3,4,5], k=2

步骤数组状态说明
初始[1,2,3,4,5]A=[1,2,3], B=[4,5]
① 全反转[5,4,3,2,1]= rev(B) + rev(A)
② 前2个反转[4,5,3,2,1]= B + rev(A)
③ 后3个反转[4,5,1,2,3]= B + A ✅

完美!


💻 代码实现

var rotate = function(nums, k) {
    const n = nums.length;
    k = k % n;

    // 三次反转
    reverse(nums, 0, n - 1);      // 整体反转
    reverse(nums, 0, k - 1);      // 前 k 个反转
    reverse(nums, k, n - 1);      // 后 n-k 个反转
};

function reverse(arr, start, end) {
    while (start < end) {
        [arr[start], arr[end]] = [arr[end], arr[start]];
        start++;
        end--;
    }
}

⚖️ 复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

✅ 完美满足“原地修改”要求
✅ 优雅、高效,体现算法之美


🤔 为什么三次反转能行?

“因为右旋转后,后面 k 个数一定会跑到前面,而前面 n−k 个数一定会被移到后面

所以第一次整体反转,先把这两部分‘放到大致正确的位置’,但顺序是反的;

然后再分别对前 k 个和后 n−k 个进行反转,把它们各自的顺序‘纠正回来’。”

这正是三次反转法的精髓!

  • 第一次反转:位置对了,顺序反了
  • 第二次和第三次反转:顺序正了,位置也对了

这种“先放对位置,再调正顺序”的思维,是解决这类问题的关键。


📌 总结对比

方法是否原地修改时间复杂度空间复杂度推荐场景
新数组法❌(需拷贝)O(n)O(n)快速实现、教学演示
三次反转法O(n)O(1)面试、性能敏感场景

💡 面试建议:先写出新数组法展示思路,再优化为三次反转法,体现你的进阶能力。


🎁 小贴士

  • 别忘了 k = k % n!否则当 k > n 时会出错。
  • 如果允许返回新数组(如某些变种题),第一种方法完全够用。
  • 三次反转法还可用于字符串旋转、环形缓冲区等问题。

✅ 结语

数组旋转看似简单,却藏着精妙的算法思想。从“新开数组”的直觉,到“三次反转”的优雅,我们不仅解决了问题,更理解了如何用简单操作组合出复杂效果

希望这篇文章能帮你彻底掌握这道经典题!如果你也在面试中遇到过类似问题,欢迎评论区分享你的经历~