Hot100 189 | 轮转数组:额外数组到三次反转
系列导航:LeetCode Hot100 刷题导航(系列导航) · 上一篇:Hot100 56 | 合并区间:排序后逐段吞并 · 下一篇:Hot100 238 | 除自身以外数组的乘积:前缀积和后缀积
这题表面上看只是“把数组整体往右挪几格”,但真正有意思的地方在于:题目不只是让你做出来,它还希望你进一步思考——能不能尽量在原数组上完成,而不是简单开一个新数组完事。
专栏导读
这题很适合作为“数组原地操作”的入门题。
因为你会依次碰到三层理解:
- 先把题意真正看懂
- 先写出最直观、最容易对的做法
- 再去理解为什么还能优化成原地修改
这篇会重点讲清楚下面几件事:
- 什么叫“右轮转”
- 为什么要先做
k %= n - 额外数组法的下标公式是怎么来的
- 三次反转为什么能做到同样的事情
- 为什么“先整体反,再局部反”不是拍脑袋技巧
1. 题目到底在说什么
题目给你:
- 一个数组
nums - 一个整数
k
你要把数组整体向右轮转 k 次。
例如:
1 | nums = [1,2,3,4,5,6,7], k = 3 |
结果应该变成:
1 | [5,6,7,1,2,3,4] |
意思就是:
- 原来最后的 3 个元素
- 被转到了最前面
2. 什么叫“轮转”
这题第一眼最容易看漏的地方,不是代码,而是“轮转”这个动作到底意味着什么。
向右轮转 1 次,表示:
- 最后一个元素跑到最前面
- 其他元素整体右移一格
例如:
1 | [1,2,3,4] |
向右轮转 1 次会变成:
1 | [4,1,2,3] |
如果再轮转 1 次,就变成:
1 | [3,4,1,2] |
所以向右轮转 k 次,本质上就是重复这个过程 k 次。
3. 这题最容易误解的点
这题通常有两个容易混的地方:
3.1 k 可能比数组长度还大
例如数组长度是 7,但 k = 10。
这时候你不能真的机械地转 10 次,而要想到:
- 转满一整圈之后,数组其实和原来一样
所以:
- 向右转 10 次
- 和向右转
10 % 7 = 3次
效果完全相同。
3.2 题目更想看到“原地修改”
最简单的方法当然可以新开数组,但这题更值得学的地方在于:
- 如何在不额外开完整新数组的情况下完成操作
所以这题是一个很好的“先会做,再会优雅做”的例子。
4. 先把题目翻译成人话
如果把这题翻译成最直白的话,其实就是:
- 后面那一段元素,应该搬到最前面
- 前面那一段元素,应该顺次往后挪
对于:
1 | [1,2,3,4,5,6,7], k = 3 |
你可以先把它想成两段:
- 前半段:
[1,2,3,4] - 后半段:
[5,6,7]
轮转以后应该变成:
1 | [5,6,7,1,2,3,4] |
这其实已经把最终结构看出来了。
5. 方法一:额外数组法
5.1 为什么先讲这个方法
虽然题目更喜欢原地修改,但对刷题新手来说,最稳的顺序永远是:
- 先写一个最容易理解的版本
- 再思考怎么优化
所以这题最直观的方法就是:
- 新开一个等长数组
- 算出每个元素轮转后的新位置
- 把它放到新位置上
- 最后再写回原数组
5.2 Python Tutor 版代码
1 | def rotate(nums, k): |
5.3 LeetCode 可提交版代码
1 | class Solution: |
5.4 每一行代码在做什么
先看:
1 | n = len(nums) |
这里:
n是数组长度k %= n是把轮转次数压缩到最小等价值
例如:
- 长度是 7
k = 10
那真正有意义的其实只是:
1 | 10 % 7 = 3 |
再看:
1 | new_nums = [0] * n |
表示创建一个和原数组一样长的新数组,用来接收轮转后的结果。
最关键的一句是:
1 | new_index = (i + k) % n |
这句一定要彻底理解。
它表示:
- 原来在位置
i的元素 - 轮转后应该去
(i + k) % n这个位置
为什么是这个公式?
因为向右轮转 k 次,本质上就是:
- 下标整体向右加
k
但如果加完超出了数组长度,就要从头开始接,所以需要 % n 折回来。
再看:
1 | new_nums[new_index] = nums[i] |
表示把原数组里的元素放到新位置。
最后:
1 | for i in range(n): |
这一步表示:
- 虽然我们用了新数组辅助
- 但题目最终希望原数组
nums被改掉 - 所以最后再把结果写回原数组
5.5 用示例手动走一遍
还是:
1 | [1,2,3,4,5,6,7], k = 3 |
看几个关键位置:
- 原来
1在下标 0,轮转后去(0 + 3) % 7 = 3 - 原来
5在下标 4,轮转后去(4 + 3) % 7 = 0 - 原来
7在下标 6,轮转后去(6 + 3) % 7 = 2
于是新数组最后变成:
1 | [5,6,7,1,2,3,4] |
5.6 这个方法的问题在哪
它当然是正确的,而且很好理解。
但问题是:
- 它额外开了一个完整新数组
所以如果题目想考“原地修改”,这还不是最漂亮的答案。
6. 方法二:三次反转(真正推荐)
6.1 为什么这方法第一次看会觉得魔法
很多人第一次看到“三次反转”时,会感觉像技巧题:
- 为什么整体反一下就行?
- 为什么再反两段就能恢复成想要的样子?
其实它不是魔法,而是利用了这样一个事实:
- 轮转后的数组,本质上就是“后半段放前面,前半段放后面”
而“反转”恰好是一种很适合重排顺序又能原地完成的操作。
6.2 核心想法
例如:
1 | [1,2,3,4,5,6,7], k = 3 |
目标是:
1 | [5,6,7,1,2,3,4] |
可以这样拆:
- 前半段:
[1,2,3,4] - 后半段:
[5,6,7]
现在想办法把后半段搬到前面,同时保持这两段内部顺序正确。
三次反转的步骤就是:
- 先把整个数组反过来
- 再把前
k个元素反回来 - 再把后面那一段反回来
6.3 Python Tutor 版代码
1 | def rotate(nums, k): |
6.4 LeetCode 可提交版代码
1 | class Solution: |
7. 这段代码每一行为什么成立
先看:
1 | nums.reverse() |
这一步表示:
- 把整个数组彻底反过来
例如:
1 | [1,2,3,4,5,6,7] |
会变成:
1 | [7,6,5,4,3,2,1] |
这一步的意义是:
- 原来应该被转到最前面的那几项,已经被整体搬到了左边
- 只是它们当前顺序是反的
再看:
1 | nums[:k] = reversed(nums[:k]) |
这一步表示:
- 把前
k个元素再单独反回来
如果 k = 3,那前 3 个元素当前是:
1 | [7,6,5] |
把它们反回来后就变成:
1 | [5,6,7] |
这正好和目标答案前半部分一致。
最后:
1 | nums[k:] = reversed(nums[k:]) |
这一步是把剩余后半段再反回来。
原来剩余部分是:
1 | [4,3,2,1] |
反回来后就变成:
1 | [1,2,3,4] |
于是整个数组最终变成:
1 | [5,6,7,1,2,3,4] |
8. 用示例手动走一遍
还是:
1 | [1,2,3,4,5,6,7], k = 3 |
第一步:整体反转
1 | [7,6,5,4,3,2,1] |
第二步:前 3 个反转
1 | [5,6,7,4,3,2,1] |
第三步:后半段反转
1 | [5,6,7,1,2,3,4] |
这正好就是目标答案。
9. 为什么三次反转值得学
这题最重要的不是以后死记“反三次”,而是看懂:
- 一个看起来像“整体平移”的操作
- 有时候可以拆成若干次更基础的原地操作
这是一种很有价值的数组思维。
也就是说,这题真正练到的不是公式,而是:
- 如何把复杂重排拆成简单可执行的小步骤
10. 这题顺手学到的 Python 语法
10.1 k %= n
表示把轮转次数化简成最小等价值。
10.2 nums.reverse()
原地反转列表。
10.3 切片赋值
1 | nums[:k] = reversed(nums[:k]) |
这表示把前一段切片替换成反转后的结果。
11. 这题最容易犯的错
11.1 忘了先做 k %= n
这样在 k 很大时逻辑会变得混乱。
11.2 三次反转的顺序写错
这题顺序非常重要:
- 先整体反
- 再反前半段
- 最后反后半段
11.3 在 Python 中把 reversed(...) 当成普通列表直接理解
reversed() 返回的是一个可迭代对象,这里是借助切片赋值把结果真正写回列表。
11.4 只会背结论,不理解为什么成立
如果不理解“后半段搬到前面”的结构关系,下次就很容易忘。
12. 一句话复盘
这题的本质不是机械地把数组右移,而是看懂:右轮转等价于“后半段放前面、前半段放后面”,而三次反转正好能原地实现这个重排。
13. 下一步怎么学
下一题 238. 除自身以外数组的乘积 会继续留在数组专题,但重点会切到“把左边信息和右边信息拆开处理”。
