Hot 100 --- 轮转数组

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


一、题目

二、题目分析

给定一个整数数组 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实现代码如下

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 个元素放到新数组的后面,最后把新数组复制回原数组

Java 复制代码
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),只用了常数个临时变量
相关推荐
徐小夕2 小时前
Loop Engineering 深度解析与实战指南(全网最全)
前端·算法·github
Qt程序员2 小时前
掌握 Linux 内核调度:从原理到实现(进程篇)
java·开发语言
code bean2 小时前
【LangChain】检索器完全指南:从向量检索到生产级 RAG 架构
java·开发语言·微服务
大白菜和MySQL2 小时前
java应用排查高线程
java·python
KobeSacre2 小时前
ReentrantLock源码
java
嵌入式协会20240722 小时前
(已解决)MinIO python 获取预签名出现forbidden、errornetwork等错误
java·开发语言·python
不才不才不不才3 小时前
Spring AI 实战:聊天、提示词、记忆三件套
java·人工智能·spring·ai
北域码匠3 小时前
SHA-1算法:安全哈希原理与应用解析
算法·c#·哈希算法
手写码匠3 小时前
手写 GraphRAG:从零实现图增强检索增强生成系统
人工智能·深度学习·算法·aigc