轮转数组 图解题解
整体翻、前段翻、后段翻——三次反转原地轮转数组,O(n) 时间 O(1) 空间。
想把一条绳子右移 k 格,只需三次翻转:先把整条绳子翻过来,再把前 k 段翻回来,最后把剩余段翻回来——三刀还原了正确顺序,不用逐格搬动,O(n) 时间、原地完成。
这道题到底在问什么
- 输入
- nums=[1,2,3,4,5,6,7], k=3
- 输出
- [5,6,7,1,2,3,4]
最优解:为什么这么做
一句话答案:LeetCode 189 轮转数组的经典原地解法是三次反转:先把整个数组反转,再把前 k 个和后 n-k 个各自反转回正序,结果恰好等于向右轮转 k 位;动手前先让 k 对 n 取模。时间 O(n)、空间 O(1),满足题目原地操作的要求。
这道题真正在问什么
把数组整体向右轮转 k 个位置:尾部的 k 个元素绕到最前面,其余元素整体后移。比如 [1,2,3,4,5,6,7] 右移 3 位得到 [5,6,7,1,2,3,4]。难点不在轮转本身,而在附加要求:原地完成、额外空间 O(1)——「开一个新数组、把每个数放到 (i+k) % n」这条最顺手的路一开始就被堵死了。
为什么开新数组和逐位右移都不行
开新数组按 (i+k) % n 搬运最好写,但要 O(n) 额外空间,不满足原地。另一个直觉是「整体右移一位、重复 k 次」:每移一位要挪动全部 n 个元素,k 次合计 O(nk),k 接近 n 时退化成平方级。两条路都堵住之后,问题变成:能不能只靠原地交换、让每个元素只动常数次,就落到最终位置?三次反转正是这样一个答案。
三次反转为什么恰好等于轮转
观察轮转后的形态:它就是「尾部 k 个元素作为一块搬到前面,前 n-k 个作为一块跟在后面」——两块内部顺序不变,只是块的位置对调。而反转有个可利用的性质:整体反转一次,两个块的位置就对调了,代价是每块内部也被反了过来。那就再各补一次反转把内部掰正:整体反转后,前 k 个正是原来的尾块(内部倒序)、后 n-k 个正是原来的头块(内部倒序),分别再反转一次,两块内部恢复正序,整个数组恰好就是轮转结果。
拿示例验证一遍:[1,2,3,4,5,6,7] 整体反转得 [7,6,5,4,3,2,1];反转前 3 个得 [5,6,7,4,3,2,1];再反转后 4 个得 [5,6,7,1,2,3,4],正是右移 3 位的答案。
k 取模和区间端点为什么容易错
第一步必须 k %= n:轮转 n 次等于没转,k 大于等于 n 时真正有效的位移只有 k % n,不取模轻则做无用功,重则反转区间越界。第二个易错点是三段区间的端点:整体是 [0, n-1],前段是 [0, k-1],后段是 [k, n-1],分界点差一格结果就整体错位。取模后 k 为 0 时前段区间为空,反转自然什么都不做,结果依旧正确,不需要特判。
复杂度怎么算,还有别的原地做法吗
三次反转合计每个元素至多被交换常数次,时间 O(n);全程只在原数组上交换,空间 O(1)。另一种 O(1) 空间的做法是环状替换:从某个下标出发,把每个数直接送到 (i+k) % n 的目标位置、沿环走完,环的个数与 gcd(n,k) 有关。它同样是线性时间,但下标推导容易出错,面试和实战里三次反转都是更直观稳妥的首选。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3这里 k=3,n=7,k%n=3。记住「整体翻一次,再分两段各翻回正序」,下面逐对交换演给你看。
- 4这是反转前的原始数组。整个过程只在原数组上交换元素,不另开数组。
- 5第①步「整体反转」:左右指针放在数组两端,对撞着把每一对元素互换。
- 6左指针指向 nums[0]=1,右指针指向 nums[6]=7,准备把这两个数互换。
- 7两格的值已互换(绿色高亮)。接着左指针右移、右指针左移,继续对撞,直到 l 和 r 相遇。
- 8左指针指向 nums[1]=2,右指针指向 nums[5]=6,准备把这两个数互换。
- 9两格的值已互换(绿色高亮)。接着左指针右移、右指针左移,继续对撞,直到 l 和 r 相遇。
- 10左指针指向 nums[2]=3,右指针指向 nums[4]=5,准备把这两个数互换。
- 11两格的值已互换(绿色高亮)。接着左指针右移、右指针左移,继续对撞,直到 l 和 r 相遇。
- 12整体反转完成。区间内已全部反转到位,进入下一步。
- 13第②步「反转前 3 个」:只在区间 [0, 2] 内对撞交换,把前段恢复成正序。
- 14左指针指向 nums[0]=7,右指针指向 nums[2]=5,准备把这两个数互换。
- 15两格的值已互换(绿色高亮)。接着左指针右移、右指针左移,继续对撞,直到 l 和 r 相遇。
- 16反转前 3 个完成。区间内已全部反转到位,进入下一步。
- 17第③步「反转后 4 个」:只在区间 [3, 6] 内对撞交换,把后段恢复成正序。
- 18左指针指向 nums[3]=4,右指针指向 nums[6]=1,准备把这两个数互换。
- 19两格的值已互换(绿色高亮)。接着左指针右移、右指针左移,继续对撞,直到 l 和 r 相遇。
- 20左指针指向 nums[4]=3,右指针指向 nums[5]=2,准备把这两个数互换。
- 21两格的值已互换(绿色高亮)。接着左指针右移、右指针左移,继续对撞,直到 l 和 r 相遇。
- 22反转后 4 个完成。区间内已全部反转到位,进入下一步。
- 23三次反转后,整个数组正好等于向右轮转 3 位的结果 [5,6,7,1,2,3,4]。全程原地交换,空间 O(1)、时间 O(n)。
⚠️ 容易写错的地方
✗ 错:忘了 k %= n
✓ 对:当 k≥n 时不取模会越界或多转
轮转 n 次回到原样,真正有效的是 k%n
✗ 错:反转区间端点写错
✓ 对:前段是 [0, k-1],后段是 [k, n-1]
差一格就把分界点搞错,结果整体错位
✗ 错:新开一个数组
✓ 对:题目要求原地 O(1) 空间
三次反转的价值正是省掉额外数组
完整代码(Python / C++ / Java)
Python
def rotate(nums, k):
n = len(nums)
k %= n # 先对 n 取模
def rev(lo, hi):
while lo < hi:
nums[lo], nums[hi] = nums[hi], nums[lo]
lo += 1; hi -= 1
rev(0, n - 1) # ① 整体反转
rev(0, k - 1) # ② 反转前 k 个
rev(k, n - 1) # ③ 反转后 n-k 个C++
void rotate(vector<int>& nums, int k){
int n = nums.size();
k %= n; // 先对 n 取模
reverse(nums.begin(), nums.end()); // ① 整体
reverse(nums.begin(), nums.begin() + k); // ② 前 k
reverse(nums.begin() + k, nums.end()); // ③ 后 n-k
}Java
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n; // 先对 n 取模
reverse(nums, 0, n - 1); // ① 整体反转
reverse(nums, 0, k - 1); // ② 反转前 k 个
reverse(nums, k, n - 1); // ③ 反转后 n-k 个
}
private void reverse(int[] a, int lo, int hi) {
while (lo < hi) {
int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
lo++; hi--;
}
}复杂度
时间
O(n)
三次反转各扫一段,合计每个元素被换常数次
空间
O(1)
只在原数组上交换,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 轮转数组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
除了三次反转,还有什么 O(1) 空间的做法?+
环状替换(juggling):从下标 i 出发,把 nums[i] 一路放到它该去的位置 (i+k)%n,跟踪已放数量,处理 gcd(n,k) 个环。空间也是 O(1),但下标推导更容易出错,反转法更直观稳妥。
为什么不直接开一个新数组放 (i+k)%n?+
那样空间是 O(n)。本题明确要求原地 O(1),所以用三次反转或环状替换。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 轮转数组 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。