LeetCode 189 轮转数组:三次反转为什么能实现右旋?

前言

第一次看到"三次反转就能完成数组轮转"时,我的第一反应其实是:这更像一个需要背下来的技巧。

先反转整个数组,再反转前 k 个元素,最后反转剩余元素------步骤确实不多,但如果只记住这三行代码,过几天很容易把反转顺序或者区间边界写错。

后来把数组拆成两部分重新看了一遍,才发现这并不是什么魔法。所谓向右轮转,本质上就是把数组末尾的一段搬到开头,同时保持前后两段内部的顺序不变。三次反转正好完成了这件事。

本文就从这个角度,讲清楚 LeetCode 189"轮转数组"的原地反转解法。

一、题目说明

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负整数。

例如:

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

向右轮转一次,数组最后一个元素会移动到最前面:

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

继续轮转两次,最终得到:

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

题目要求我们直接修改原数组,不需要返回新的数组。

二、最容易想到的方法:一步一步移动

最直观的做法,是每轮都保存数组最后一个元素,再把其他元素整体向后移动一位,最后将保存的元素放到数组开头。重复 k 次,就可以得到答案。

这种方法当然能做出来,但存在一个明显问题:每轮都要移动接近 n 个元素,一共执行 k 轮,时间复杂度会达到 O(n × k)

如果数组很长,而且 k 也很大,很多重复移动其实没有必要。我们真正想做的,不是模拟每一步轮转,而是一次性把应该出现在前面的那部分元素放过去。

三、换个角度:右旋其实是在交换两段

仍然看下面这个数组:

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

向右轮转 3 位后,数组末尾的 3 个元素会移动到开头:

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

我们可以把原数组分为两段:

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

原数组是:

复制代码
A + B

轮转后的数组则是:

复制代码
B + A

所以问题变成了:如何在不使用额外数组的情况下,把 A + B 变成 B + A

答案就是三次反转。

四、三次反转的完整过程

第一步:反转整个数组

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

如果用前面的分组表示,A + B 整体反转后会变成:

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

此时 B 已经被移动到了前面,但 AB 两段内部的顺序都反了。

第二步:反转前 k 个元素

当前数组为:

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

反转前 3 个元素:

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

这一步将 reverse(B) 恢复成了原来的 B

复制代码
B + reverse(A)

第三步:反转后 n-k 个元素

继续反转下标 kn - 1 之间的元素:

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

reverse(A) 也恢复成了原来的 A,最终得到:

复制代码
B + A

这正是向右轮转后的结果。

所以,三次反转并不是碰巧有效,它对应着一个很清楚的过程:先借助整体反转交换两段的位置,再分别修复两段内部的顺序。

五、为什么一定要对 k 取模

代码中的这一行很容易被忽略:

复制代码
k = k % len;

假设数组长度为 7,向右轮转 7 次以后,数组会回到原来的状态。因此:

复制代码
轮转 8 次 = 轮转 1 次
轮转 10 次 = 轮转 3 次

真正有效的轮转次数只有 k % len

如果不取模,当 k 大于数组长度时,k - 1 就可能超出数组下标范围。例如,数组长度为 4、k = 6,实际只需要轮转:

复制代码
6 % 4 = 2

取模既消除了重复操作,也保证了后续反转区间合法。

六、Java 完整代码

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

        // 对 k 取模,避免 k 大于数组长度
        k = k % len;

        // 第一次:反转整个数组
        reverse(nums, 0, len - 1);

        // 第二次:反转前 k 个元素
        reverse(nums, 0, k - 1);

        // 第三次:反转剩余的 n-k 个元素
        reverse(nums, k, len - 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--;
        }
    }
}

七、复杂度分析

三次反转分别处理 nkn-k 个元素。虽然代码中调用了三次 reverse,但总操作次数仍然与数组长度成正比,因此:

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

整个过程直接在原数组上交换元素,没有创建长度与输入规模相关的辅助数组,这也是该解法最有价值的地方。

相关推荐
豆沙沙包?17 分钟前
函数重载/类和对象(P14-P60)
前端·算法
lingchen190628 分钟前
用MATLAB r2010b和r2016b求解微分方程组
开发语言·算法·matlab
无定义_1 小时前
Dijkstra——单源最短路径
算法·图论
weixin_307779131 小时前
AI生成代码的隐患与治理:人工审查流程及提示词驱动的修复策略
开发语言·人工智能·算法
zmzb01031 小时前
C++课后习题训练记录Day201
c++·算法·图论
lch2011_yb1 小时前
CSP-J 2023 小苹果
算法
五_谷_丰_登2 小时前
平衡二叉搜索树讲解
数据结构·c++
星星.7222 小时前
【图论】最小生成树|Prim+Kruskal算法
数据结构·c++·算法·图论
圣保罗的大教堂2 小时前
leetcode 3734. 大于目标字符串的最小字典序回文排列 困难
leetcode