本文概览:本文以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--;
}
}
思路简要说明
-
反转整个数组:将数组完全翻转
-
反转前 k 个元素:将翻转后位于前 k 位的元素(即原来的后 k 个元素)再翻转回来,恢复正确顺序
-
反转剩余元素:将翻转后位于后 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),只用了常数个临时变量