力扣解题-189. 轮转数组

8 阅读7分钟

力扣解题-189. 轮转数组

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

示例 1: 输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4]

示例 2: 输入:nums = [-1,-100,3,99], k = 2 输出:[3,99,-1,-100] 解释: 向右轮转 1 步: [99,-1,-100,3] 向右轮转 2 步: [3,99,-1,-100]

提示:

1 <= nums.length <= 105

-231 <= nums[i] <= 231 - 1

0 <= k <= 105

进阶:

尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。

你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

Related Topics 数组、数学、双指针


第一种解答(超时)

解题思路

核心方法:逐次右移模拟,通过嵌套循环模拟“每次右移1步,共移k步”的过程,每一步都将数组元素逐个右移,最后将最后一个元素放到首位,逻辑直观但时间复杂度极高,无法适配大数据量。

具体步骤:

  1. 初始化临时变量temp(暂存当前元素)、write(待写入的元素值)、length(数组长度);
  2. 外层循环控制轮转次数:循环k次,每次实现“右移1步”;
  3. 内层循环实现单次右移
    • 从索引1开始遍历数组,先暂存前一个位置的元素(write);
    • 将当前位置的元素暂存到temp,再把write写入当前位置;
    • 遍历到最后一个元素时,将temp(最后一个元素)写入数组首位;
  4. 无返回值:直接修改原数组。

超时原因说明

该解法在数据量较大时(如nums.length=10⁵、k=10⁵)会直接超时,核心问题:

  1. 时间复杂度爆炸:嵌套循环的时间复杂度为O(k×n),当k和n均为10⁵时,总操作数达到10¹⁰,远超力扣时间限制(通常1秒仅能处理10⁸次操作);
  2. 逻辑冗余:逐次右移完全没必要,可通过数学计算直接定位元素最终位置,无需重复遍历;
  3. 无性能数据(超时):该思路仅适用于k和n极小的场景,完全不满足题目数据规模要求。
    public void rotate(int[] nums, int k) {
        int temp=0;
        int write=0;
        int length=nums.length;
        for(int i=0;i<k;i++){
            for(int j=1;j<length;j++){
                if(j==1) {
                    write = nums[j - 1];
                }else {
                    write=temp;
                }
                temp=nums[j];
                nums[j] = write;
                if(j+1==length){
                    nums[0] = temp;
                }
            }
        }
    }

第二种解答

解题思路

核心方法:哈希表映射位置,通过HashMap记录“原索引元素”对应的“目标索引位置”,再遍历哈希表将元素写回原数组,利用哈希表实现位置映射,但存在额外的哈希操作开销。

核心原理铺垫

右轮转k个位置的数学规律:原索引为i的元素,轮转后的目标索引为(i + k) % length(取模避免索引越界,适配k>length的场景)。

具体步骤:

  1. 边界快速处理:若数组长度<2,无需轮转,直接返回;
  2. 初始化哈希表Map<Integer, Integer>,键为目标索引,值为对应元素;
  3. 遍历映射位置:遍历数组每个元素,计算其目标索引end = (i + k) % length,将(end, nums[i])存入哈希表;
  4. 写回原数组:遍历哈希表的键(目标索引),将对应值写入nums的指定索引位置,完成轮转。

性能劣势说明

该解法耗时15ms仅击败3.10%用户、内存66.33MB仅击败5.18%用户,核心问题:

  1. 哈希表的额外开销putget操作涉及哈希计算、冲突处理,且遍历keySet的顺序不确定(但不影响结果),时间复杂度虽为O(n),但常数因子极大;
  2. 空间复杂度高:哈希表需存储n个键值对,空间复杂度O(n),且哈希表的存储结构(数组+链表/红黑树)比普通数组更占内存;
  3. 逻辑冗余:无需通过哈希表映射,直接用数组即可完成位置存储,哈希表是“过度设计”。
public void rotate(int[] nums, int k) {
        if(nums.length<2){
            return;
        }
        int length=nums.length;

        Map<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < nums.length; i++){
            int end = (i + k) % length;
            map.put(end, nums[i]);
        }
        for(Integer key:map.keySet()){
            nums[key]=map.get(key);
        }
    }

第三种解答

解题思路

核心方法:辅助数组暂存,利用额外数组存储元素的目标位置,再通过数组拷贝写回原数组,逻辑简洁且性能优于前两种,是空间换时间的经典思路。

核心原理铺垫

与第二种解答的位置映射逻辑一致,仅将哈希表替换为普通数组,避免哈希操作的额外开销:原索引i的元素 → 目标索引(i + k) % length

具体步骤:

  1. 获取数组长度length = nums.length
  2. 创建辅助数组tempNums = new int[length],用于暂存轮转后的元素;
  3. 填充辅助数组:遍历原数组,将nums[i]写入tempNums[(i + k) % length]
  4. 写回原数组:调用System.arraycopy(高效数组拷贝方法),将tempNums的所有元素拷贝到nums中,完成原地修改。

性能说明

该解法耗时1ms击败47.46%用户、内存61.9MB击败28.98%用户,是较优的“空间换时间”方案:

  1. 时间复杂度最优:仅两次遍历(填充辅助数组+拷贝),时间复杂度O(n),无额外冗余操作;
  2. 空间复杂度O(n):辅助数组占用n个元素的空间,不满足进阶要求的“O(1)原地算法”,但在工程中是易实现、易理解的方案;
  3. System.arraycopy的优势:该方法是JVM底层优化的原生方法,拷贝效率远高于手动遍历赋值,是该解法性能优于第二种的核心原因。
    public void rotate(int[] nums, int k) {
        int length=nums.length;
        int[] tempNums=new int[nums.length];
        for(int i = 0; i < nums.length; i++){
            int end = (i + k) % length;
            tempNums[end]=nums[i];
        }
        System.arraycopy(tempNums,0,nums,0,length);
    }

示例解答

解题思路

核心方法:三次反转法(原地算法,空间O(1)),利用数组反转的特性,通过三次反转实现无额外空间的右轮转,是本题的最优解(进阶要求的标准答案)。

核心原理铺垫

右轮转k个位置的本质:将数组后k个元素移到前面,前n-k个元素移到后面。通过反转可实现无额外空间的元素迁移:

  1. 先反转整个数组,打破原有顺序;
  2. 反转前k个元素,恢复后k个元素的正确顺序;
  3. 反转后n-k个元素,恢复前n-k个元素的正确顺序。

关键预处理:k = k % length(避免k>length时的无效轮转,如length=7、k=10,等价于k=3)。

具体步骤(补充最优解思路,代码未提供但适配题目进阶要求):

  1. 预处理k值k = k % nums.length(处理k≥数组长度的情况);
  2. 反转整个数组:反转nums[0...length-1];
  3. 反转前k个元素:反转nums[0...k-1];
  4. 反转剩余元素:反转nums[k...length-1]。
public void rotate(int[] nums, int k) {
    int n = nums.length;
    k = k % n;
    reverse(nums, 0, n - 1);      // 全翻转
    reverse(nums, 0, k - 1);      // 前 k 个翻转
    reverse(nums, k, n - 1);      // 后 n-k 个翻转
}

private void reverse(int[] nums, int start, int end) {
    while (start < end) {
        int tmp = nums[start];
        nums[start] = nums[end];
        nums[end] = tmp;
        start++;
        end--;
    }
}
核心优势说明
  1. 时间复杂度O(n):三次反转的总操作数为n(反转n个元素),无冗余;
  2. 空间复杂度O(1):仅使用临时变量交换元素,完全满足进阶要求;
  3. 性能极致:无数组拷贝、哈希操作,是力扣该题的最优解法,耗时通常可击败99%以上用户。

总结

  1. 逐次右移法:逻辑直观但时间复杂度O(k×n),大数据量超时,仅适用于极小规模场景;
  2. 哈希表映射法:位置计算正确但哈希操作开销大,性能/内存均差,无实际使用价值;
  3. 辅助数组法:空间换时间的经典方案,时间O(n)、空间O(n),易实现且性能较好;
  4. 三次反转法:本题最优解,时间O(n)、空间O(1),满足进阶要求,是面试高频考点;
  5. 核心技巧:轮转问题优先通过“取模定位目标位置”或“反转”实现,避免逐次模拟的低效操作。