LeetCode 189 轮转数组:三次反转原地解决,图解 Java 实现

在数组题中,"轮转数组"是一道很典型的题。它的代码并不长,但同时考查了数组下标、空间复杂度、边界处理,以及能否从结果反推出操作过程。

这篇文章用 Java 解决 LeetCode 189「轮转数组」,重点讲清楚一个看起来有些巧妙的方法:为什么只需要三次反转,就能把数组向右轮转 k****个位置。

一、题目描述

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,要求直接修改原数组。

例如:

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

向右轮转 3 个位置,可以理解为:把数组末尾的 3 个元素 [5,6,7] 移到数组开头,其余元素 [1,2,3,4] 整体向后移动。

二、最容易想到的做法:使用额外数组

原数组下标为 i 的元素,轮转后会出现在:

复制代码
(i + k) % n

因此,可以创建一个长度相同的新数组,把每个元素放入新位置,再复制回原数组:

复制代码
public void rotate(int[] nums, int k) {
    int n = nums.length;
    k %= n;

    int[] temp = new int[n];
    for (int i = 0; i < n; i++) {
        temp[(i + k) % n] = nums[i];
    }

    System.arraycopy(temp, 0, nums, 0, n);
}

这个方法很直观,时间复杂度为 O(n),但需要 O(n) 的额外空间。如果面试官进一步要求空间复杂度为 O(1),就需要考虑原地操作。

三、最优解:三次反转

仍然以数组为例:

复制代码
[1, 2, 3, 4, 5, 6, 7]

向右轮转 3 位后,希望得到:

复制代码
[5, 6, 7, 1, 2, 3, 4]

可以把原数组分成两部分:

复制代码
A = [1, 2, 3, 4]
B = [5, 6, 7]

轮转的目标,本质上就是把 A + B 变成 B + A

具体分三步。

第一步:反转整个数组

复制代码
[1, 2, 3, 4, 5, 6, 7]
                 ↓
[7, 6, 5, 4, 3, 2, 1]

此时两部分的位置已经交换,但每个部分内部的顺序也被反转了。可以表示为:

复制代码
reverse(B) + reverse(A)

第二步:反转前 k 个元素

前 3 个元素是 [7,6,5],将其反转:

复制代码
[7, 6, 5, 4, 3, 2, 1]
       ↓
[5, 6, 7, 4, 3, 2, 1]

这样,原数组末尾的 B 已经恢复成正确顺序。

第三步:反转剩余元素

再把下标 kn - 1 的部分 [4,3,2,1] 反转:

复制代码
[5, 6, 7, 4, 3, 2, 1]
             ↓
[5, 6, 7, 1, 2, 3, 4]

最终得到目标结果。

整个过程可以概括为:

复制代码
整体反转 → 反转前 k 个元素 → 反转后 n-k 个元素

四、为什么三次反转一定成立?

假设原数组由两部分组成:

复制代码
A + B

其中 B 是需要移到前面的最后 k 个元素。

第一次整体反转后得到:

复制代码
reverse(B) + reverse(A)

然后分别反转前后两部分:

复制代码
B + A

这正是向右轮转后的结果。因此,三次反转不是只对某个示例有效,而是对任意数组都成立。

五、完整 Java 代码

复制代码
class Solution {
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        k %= n;

        // 1. 反转整个数组
        reverse(nums, 0, n - 1);

        // 2. 反转前 k 个元素
        reverse(nums, 0, k - 1);

        // 3. 反转剩余元素
        reverse(nums, k, n - 1);
    }

    private void reverse(int[] nums, int left, int right) {
        while (left < right) {
            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
            left++;
            right--;
        }
    }
}

reverse 方法使用双指针:left 从左向右移动,right 从右向左移动,每次交换两个位置的元素,直到两个指针相遇。

六、为什么必须对 k 取模?

如果数组长度为 7,而 k = 10,向右轮转 10 次和向右轮转 3 次的结果相同:

复制代码
10 % 7 = 3

因为每轮转 n 次,数组都会恢复原状,所以真正有效的轮转次数是:

复制代码
k %= nums.length;

这一步还能避免 k 大于数组长度时,后续反转区间越界。

k = 0 时,第二次反转调用的区间是 [0, -1]。由于 reverse 中的条件是 left < right,循环不会执行,代码仍然能够正确运行。

七、复杂度分析

虽然进行了三次反转,但每个元素只会被交换有限次,因此:

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

它没有创建与数组长度相关的辅助空间,满足原地修改的要求。

八、常见错误

1. 忘记对 k 取模

k > nums.length 时,反转区间可能越界。进入反转逻辑前,应先执行 k %= n

2. 反转顺序写错

向右轮转的常见写法是:

复制代码
整体 → 前 k 个 → 后 n-k 个

不要把 k 误写成 n - k。可以始终记住:向右轮转后,原数组最后的 k****个元素要出现在最前面。

3. 逐个移动导致超时

如果每轮转一次,就把整个数组移动一遍,那么时间复杂度是 O(n × k)。当数组和 k 都很大时,这种方法容易超时。

4. 只修改了局部变量

题目要求原地修改 nums,不能只让一个新数组变量指向结果而不复制回去。三次反转直接操作原数组,不存在这个问题。

相关推荐
孫治AllenSun4 分钟前
【LangChain4J-04】Tool 工具的使用
java·人工智能
南城以南溫暖如初1476 分钟前
树洞交友系统架构设计与匿名聊天实战指南
java·spring boot·mysql·系统架构·vue·mybatis·交友
郝学胜-神的一滴9 分钟前
C++20 高级编程 004:从初始化、内存到const系列关键字
开发语言·算法·编程·软件构建·c++20
lemon_sjdk13 分钟前
JavaFX 响应式核心:从属性绑定、失效通知到生命周期管理
java
kyle~15 分钟前
点云配准--- 迭代最近点 ICP 求解
线性代数·算法·机器学习
qq74223498425 分钟前
Gradio 极简入门:三分钟为AI模型打造交互界面,并对比Streamlit与Dash如何选型
人工智能·算法·大模型·交互·dash
QCodingDev34 分钟前
Spring AI Alibaba Graph实战:从ReAct Agent到Workflow,企业AI复杂流程该如何编排?
java·人工智能·spring·ai·ai编程·企业ai
一嘴一个橘子36 分钟前
SpringDataRedis 操作 redis
java·redis
JasmineWr39 分钟前
Spring BeanDefinition 与 Bean 生命周期、循环依赖解析
java·后端·spring
dzl8439440 分钟前
微服务基座
java