Hot 100 --- 轮转数组

本文概览:本文以LeetCode经典题目"轮转数组"为例,从暴力解法入手,逐步优化到 O(n) 额外空间的解法,再通过三次翻转法实现 O(1) 空间复杂度的原地修改,系统讲解如何用红黑笔的比喻理解三次翻转的巧妙之处


一、题目

![[轮转数组题目.png]]

二、题目分析

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

目标:原地修改数组,实现右轮转 k 位

从过程看轮转

最直观的理解是"过程视角":每次把数组所有元素右移一位,重复 k 次。但这种方式时间复杂度极高

从结果看轮转

如果只看结果而不看过程,轮转其实就是把后面长度为 k 的子数组和前面长度为 n-k 的子数组交换位置。例如 nums = [1,2,3,4,5,6,7], k = 3,轮转后就是 [5,6,7,1,2,3,4],本质上是把 [5,6,7] 放到了开头,[1,2,3,4] 放到了后面

所以下面的解法都是基于"把后面长度为 k 的子数组和前面长度为 n-k 的子数组交换位置"这个视角来的

思路概览

Java实现代码如下

public void rotate(int[] nums, int k) {
    if (nums == null || nums.length == 0 || k <= 0) {
        return;
    }
    int left = 0;
    int right = nums.length - 1;
    k = k % nums.length; // 处理 k 大于数组长度的情况
    // 1. 反转整个数组
    while (left < right) {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        left++;
        right--;
    }
    // 2. 反转前 k 个元素
    left = 0;
    right = k - 1;
    while (left < right) {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        left++;
        right--;
    }
    // 3. 反转剩余的元素
    left = k;
    right = nums.length - 1;
    while (left < right) {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        left++;
        right--;
    }
}

思路简要说明

  1. 反转整个数组:将数组完全翻转

  2. 反转前 k 个元素:将翻转后位于前 k 位的元素(即原来的后 k 个元素)再翻转回来,恢复正确顺序

  3. 反转剩余元素:将翻转后位于后 n-k 位的元素(即原来的前 n-k 个元素)再翻转回来,恢复正确顺序

三、思路详解

暴力解法入手

最直接的暴力做法是:每次将数组所有元素右移一位,重复 k 次。每次右移需要遍历整个数组,共 k 次

  • 时间复杂度:O(n × k),当 k 很大时效率极低
  • 核心瓶颈:每次只移动一位,做了大量重复的挪动操作
  • 关键思考:能否一次性完成交换,而不是逐位挪动?
O(n) 额外空间解法

既然轮转的本质是把后面长度为 k 的子数组放到开头,前面长度为 n-k 的子数组放到后面,那最简单的做法就是新开一个数组,直接把后 k 个元素放到新数组的开头,前 n-k 个元素放到新数组的后面,最后把新数组复制回原数组

public void rotate(int[] nums, int k) {
    int n = nums.length;
    k = k % n;
    int[] arr = new int[n];
    // 后 k 个元素放到开头
    for (int i = 0; i < k; i++) {
        arr[i] = nums[n - k + i];
    }
    // 前 n-k 个元素放到后面
    for (int i = k; i < n; i++) {
        arr[i] = nums[i - k];
    }
    // 复制回原数组
    System.arraycopy(arr, 0, nums, 0, n);
}
  • 时间复杂度:O(n),只需遍历一次
  • 空间复杂度:O(n),需要额外开一个长度为 n 的数组
  • 核心瓶颈:数组比较大时,额外开辟一个同等大小的数组空间开销非常大
  • 关键思考:能否不开新数组,原地完成交换?
三次翻转法(O(1) 空间)

思路分析

不开新数组,直接把后面 k 个元素和前面 n-k 个元素交换位置,在代码层面需要一个中间变量来记录数组,无法实现原地反转

但有一个小巧思——三次翻转法

我们用红笔和黑笔来理解:假设有一排笔排成一排,笔头朝右,黑笔代表后面长度为 k 的子数组,红笔代表前面长度为 n-k 的子数组

轮转前:→→→→  →→→   (红红红红 黑黑黑,笔头都朝右)
轮转后:→→→  →→→→   (黑黑黑 红红红红,笔头都朝右)

我们要的就是黑笔在前面、红笔在后面,且笔头都朝右。最直接的想法就是把红笔和黑笔交换顺序,但这需要额外空间

三次翻转的巧妙之处

第一步:反转整个数组

反转前:→→→→  →→→   (红红红红 黑黑黑)
反转后:←←←  ←←←←   (黑黑黑 红红红红)

反转后,黑笔已经到了前面,红笔已经到了后面,位置已经符合要求了!但是有个问题:所有笔的笔头朝向都反了——原来朝右,现在朝左

第二步:反转前 k 个元素(把黑笔的笔头翻回去)

反转前:←←←  ←←←←
反转后:→→→  ←←←←   (黑笔笔头恢复朝右)

第三步:反转后 n-k 个元素(把红笔的笔头翻回去)

反转前:→→→  ←←←←
反转后:→→→  →→→→   (红笔笔头恢复朝右)

三次翻转后,黑笔在前面且笔头朝右,红笔在后面且笔头朝右,轮转完成!

为什么三次翻转等价于轮转?

从数学角度理解:

  • 原数组:[A | B],其中 A 是前 n-k 个元素,B 是后 k 个元素
  • 目标:[B | A]
  • 反转整个数组:[A' | B'](A’ 是 A 的反转,B’ 是 B 的反转)
  • 反转前 k 个(即 B’):[B | A']
  • 反转后 n-k 个(即 A’):[B | A]

每一步反转都是原地的,只需要双指针交换,空间复杂度 O(1)

举例说明

nums = [1,2,3,4,5,6,7], k = 3 为例

第一步:反转整个数组

[1,2,3,4,5,6,7] → [7,6,5,4,3,2,1]

第二步:反转前 k=3 个元素

[7,6,5,4,3,2,1] → [5,6,7,4,3,2,1]

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

[5,6,7,4,3,2,1] → [5,6,7,1,2,3,4]

最终结果为 [5,6,7,1,2,3,4],轮转正确

  • 时间复杂度:O(n),三次反转各遍历一部分,总共遍历 2n 次
  • 空间复杂度:O(1),只用了常数个临时变量
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值