Skip to content

题目链接 · 灵神原题解(署名来源)

思路讲解

看示例 1,我们要把 [1,2,3,4,5,6,7] 变成 [5,6,7,1,2,3,4]

[5,6,7,1,2,3,4] 视作 [5,6,7]+[1,2,3,4]。我们首先要保证 [5,6,7] [1,2,3,4] 前面,这可以通过反转 [1,2,3,4,5,6,7] 得到。

反转后数组变成 [7,6,5,4,3,2,1],即 [7,6,5]+[4,3,2,1]。对比最终目标,只需把 [7,6,5] 反转,把 [4,3,2,1] 反转,就得到了 [5,6,7,1,2,3,4]

lc189.png

这用到了「负负得正」的想法:把一个子数组反转两次,子数组不变

在第一次反转中,我们不仅反转了子数组 [1,2,3,4][5,6,7],还交换了这两个子数组的位置。然后,把这两个子数组分别再反转一次,就抵消掉了反转,相当于我们只交换了 [1,2,3,4][5,6,7],得到 [5,6,7,1,2,3,4]

算法

这里假设 0k<n,对于 kn 的情况,可以转换成 0k<n 的情况(证明见后文)。

nums=A+B,其中 Anums 的前 nk 个数,B 是后 k 个数。在上例中,A=[1,2,3,4]B=[5,6,7]

题目要求把 A+B 变成 B+A,这可以用三次反转实现:

  1. nums=A+B 反转,我们得到了 rev(B)+rev(A),其中 rev(A) 表示数组 A 反转后的结果。在上例中,rev(B)+rev(A)=[7,6,5]+[4,3,2,1]
  2. 单独反转 rev(B),因为一个数组反转两次是不变的,所以 rev(rev(B))=B,我们得到了 B
  3. 单独反转 rev(A),得到 rev(rev(A))=A
  4. 现在数组变成 B+A。在上例中,B+A=[5,6,7]+[1,2,3,4],这正是我们想要的结果。
python
# 注:请勿使用切片,会产生额外空间
class Solution:
    def rotate(self, nums: List[int], k: int) -> None:
        def reverse(i: int, j: int) -> None:
            while i < j:
                nums[i], nums[j] = nums[j], nums[i]
                i += 1
                j -= 1

        n = len(nums)
        k %= n  # 轮转 k 次等同于轮转 k % n 次
        reverse(0, n - 1)
        reverse(0, k - 1)
        reverse(k, n - 1)
cpp
// C++ 版待补充
cpp
class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        k %= nums.size(); // 轮转 k 次等同于轮转 k % n 次
        ranges::reverse(nums);
        reverse(nums.begin(), nums.begin() + k);
        reverse(nums.begin() + k, nums.end());
    }
};
cpp
class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        auto reverse = [&](int i, int j) {
            while (i < j) {
                swap(nums[i++], nums[j--]);
            }
        };

        int n = nums.size();
        k %= n; // 轮转 k 次等同于轮转 k % n 次
        reverse(0, n - 1);
        reverse(0, k - 1);
        reverse(k, n - 1);
    }
};

复杂度分析

  • 时间复杂度:O(n),其中 nnums 的长度。
  • 空间复杂度:O(1)

附:正确性证明

向右轮转 k 个位置后,下标 i 的元素移动到下标 (i+k)modn 上,其中 nnums 的长度。

下面证明「三次反转法」的正确性。

假设 0k<n,也就是 0kn1,分类讨论:

  • 如果 nkin1,那么第一次反转(整个数组的反转)会把下标 i 的元素交换到下标 n1i 上(注意 0n1ik1,在第二次反转的范围中),第二次反转会把下标 n1i 的元素交换到下标 k1(n1i)=i+kn 上。根据 nkin1 可得 0i+knk1,所以 i+kn=(i+k)modn,符合题目要求。
  • 如果 0ink1,那么第一次反转(整个数组的反转)会把下标 i 的元素交换到下标 n1i 上(注意 kn1in1,在第三次反转的范围中),第三次反转会把下标 n1i 的元素交换到下标 k+n1(n1i)=i+k 上(注*)。根据 0ink1 可得 ki+kn1,所以 i+k=(i+k)modn,符合题目要求。

对于 kn 的情况,由于轮转 n 次等同于没有轮转,轮转 n+1 等同于轮转 1 次……依此类推,轮转 k 次等同于轮转 kmodn 次。由于 0kmodnn1,所以 kn 的情况也是正确的。

综上所述,对于任意非负整数 k,按照图中三次反转的方法,可以把下标 i 的元素移动到下标 (i+k)modn 上。

:对于下标区间 [L,R] 的反转,由于 i=L 会反转到 Ri=L+1 会反转到 R1i=L+2 会反转到 R2,依此类推,下标 i 和反转后的位置 j 满足 i+j=L+R,所以 i 会反转到 L+Ri

变形题

  1. 改成向左轮转 k 个位置。
  2. 如果 nums 是个二维数组呢?见 1260. 二维网格迁移

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

本文整理自灵茶山艾府(endlesscheng)的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传