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,不能只让一个新数组变量指向结果而不复制回去。三次反转直接操作原数组,不存在这个问题。

相关推荐
歌_顿1 小时前
Drawing_To_Model
算法
龍德明宇1 小时前
AI不会疼-龍德明宇
人工智能·算法·大语言模型llm·负主体性·ai存在论
302wanger2 小时前
程序员写 SOP:把脑子里的流程固化下来
算法
科学实验家2 小时前
完全背包
算法
我命由我123452 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
Smoothcloud润云2 小时前
GPU租赁数据安全怎么做?
人工智能·算法·ai·aigc·gpu算力·gpu
呜喵王阿尔萨斯2 小时前
extern “C“ in C++
c语言·c++·算法
晚安code2 小时前
Java 异常与日志规范:错误码、异常处理、日志规约一次讲清
java
深海呐2 小时前
仓颉语言是ArkTs的上层语言吗?就像kotlin和Java
java·开发语言·kotlin·仓颉