系列导航:LeetCode Hot100 刷题导航(系列导航) · 上一篇:Hot100 56 | 合并区间:排序后逐段吞并 · 下一篇:Hot100 238 | 除自身以外数组的乘积:前缀积和后缀积

这题表面上看只是“把数组整体往右挪几格”,但真正有意思的地方在于:题目不只是让你做出来,它还希望你进一步思考——能不能尽量在原数组上完成,而不是简单开一个新数组完事。

专栏导读

这题很适合作为“数组原地操作”的入门题。

因为你会依次碰到三层理解:

  1. 先把题意真正看懂
  2. 先写出最直观、最容易对的做法
  3. 再去理解为什么还能优化成原地修改

这篇会重点讲清楚下面几件事:

  1. 什么叫“右轮转”
  2. 为什么要先做 k %= n
  3. 额外数组法的下标公式是怎么来的
  4. 三次反转为什么能做到同样的事情
  5. 为什么“先整体反,再局部反”不是拍脑袋技巧

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 为什么先讲这个方法

虽然题目更喜欢原地修改,但对刷题新手来说,最稳的顺序永远是:

  1. 先写一个最容易理解的版本
  2. 再思考怎么优化

所以这题最直观的方法就是:

  • 新开一个等长数组
  • 算出每个元素轮转后的新位置
  • 把它放到新位置上
  • 最后再写回原数组

5.2 Python Tutor 版代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def rotate(nums, k):
n = len(nums)
k %= n
new_nums = [0] * n

for i in range(n):
new_index = (i + k) % n
new_nums[new_index] = nums[i]

for i in range(n):
nums[i] = new_nums[i]

nums = [1,2,3,4,5,6,7]
rotate(nums, 3)
print(nums)

5.3 LeetCode 可提交版代码

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def rotate(self, nums, k):
n = len(nums)
k %= n
new_nums = [0] * n

for i in range(n):
new_index = (i + k) % n
new_nums[new_index] = nums[i]

for i in range(n):
nums[i] = new_nums[i]

5.4 每一行代码在做什么

先看:

1
2
n = len(nums)
k %= n

这里:

  • 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
2
for i in range(n):
nums[i] = new_nums[i]

这一步表示:

  • 虽然我们用了新数组辅助
  • 但题目最终希望原数组 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]

现在想办法把后半段搬到前面,同时保持这两段内部顺序正确。

三次反转的步骤就是:

  1. 先把整个数组反过来
  2. 再把前 k 个元素反回来
  3. 再把后面那一段反回来

6.3 Python Tutor 版代码

1
2
3
4
5
6
7
8
9
10
11
def rotate(nums, k):
n = len(nums)
k %= n

nums.reverse()
nums[:k] = reversed(nums[:k])
nums[k:] = reversed(nums[k:])

nums = [1,2,3,4,5,6,7]
rotate(nums, 3)
print(nums)

6.4 LeetCode 可提交版代码

1
2
3
4
5
6
7
8
class Solution:
def rotate(self, nums, k):
n = len(nums)
k %= n

nums.reverse()
nums[:k] = reversed(nums[:k])
nums[k:] = reversed(nums[k:])

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 三次反转的顺序写错

这题顺序非常重要:

  1. 先整体反
  2. 再反前半段
  3. 最后反后半段

11.3 在 Python 中把 reversed(...) 当成普通列表直接理解

reversed() 返回的是一个可迭代对象,这里是借助切片赋值把结果真正写回列表。

11.4 只会背结论,不理解为什么成立

如果不理解“后半段搬到前面”的结构关系,下次就很容易忘。

12. 一句话复盘

这题的本质不是机械地把数组右移,而是看懂:右轮转等价于“后半段放前面、前半段放后面”,而三次反转正好能原地实现这个重排。

13. 下一步怎么学

下一题 238. 除自身以外数组的乘积 会继续留在数组专题,但重点会切到“把左边信息和右边信息拆开处理”。