题目描述
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
输入
整数数组 nums 和非负整数 k
输出
原地修改 nums,将其向右轮转 k 步
约束条件
1 <= nums.length <= 10^5-2^31 <= nums[i] <= 2^31 - 10 <= k <= 10^5
进阶约束
- 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
- 你可以使用空间复杂度为 的 原地 算法解决这个问题吗?
示例1
输入:nums = [1,2,3,4,5,6,7], k = 3输出:[5,6,7,1,2,3,4]解释:
向右轮转 1 步: [7,1,2,3,4,5,6]向右轮转 2 步: [6,7,1,2,3,4,5]向右轮转 3 步: [5,6,7,1,2,3,4]示例2
输入:nums = [-1,-100,3,99], k = 2输出:[3,99,-1,-100]解释:
向右轮转 1 步: [99,-1,-100,3]向右轮转 2 步: [3,99,-1,-100]解题思路
向右轮转 k 步,本质上是把数组末尾的 k 个元素”搬”到数组开头。最直接的想法是——创建一个新数组,把每个元素放到它该去的位置。
💡 一句话概括:轮转数组有三种经典解法——额外数组(最直观)、环状替换(最巧妙)、三次反转(最优雅)。其中三次反转法代码最短、最容易写对,是面试的首选。
从额外数组出发
最简单的思路:创建一个新数组 ans,将 nums[i] 放到 ans[(i + k) % n] 的位置,最后把新数组拷贝回 nums:
class Solution: def rotate(self, nums: List[int], k: int) -> None: n = len(nums) k %= n # 处理 k >= n 的情况 ans = [0] * n
for i in range(n): ans[(i + k) % n] = nums[i]
nums[:] = ans # 拷贝回原数组⚠️ 注意:
k %= n这一行很关键。当k >= n时(如k = 10, n = 7),轮转k步等价于轮转k % n = 3步。虽然不取模的话(i + k) % n也能算出正确位置,但轮转超过一整圈纯属浪费。
这里用 nums[:] = ans 而不是 nums = ans——前者修改列表的内容(原地替换),后者只是让局部变量 nums 指向新列表(不会影响函数外部的原数组)。
瓶颈分析:这个方法需要 的额外空间。 时, 个整数的数组大约占 800KB——可以通过,但进阶要求挑战 空间。
🧠 直觉理解:创建新数组就像搬家时先把所有家具搬到临时仓库,再按新布局搬回去——稳,但需要两倍的空间。能不能直接在原地”挪”家具?
三次反转法
这是本题最优雅的解法。将 nums 向右轮转 k 步,等价于以下三步:
- 反转整个数组——让末尾的
k个元素跑到开头(此时是逆序的),开头的n-k个元素跑到末尾(同样逆序) - 反转前
k个元素——恢复它们原本的顺序 - 反转后
n-k个元素——恢复它们原本的顺序
用数学语言描述:设原数组为 ,目标是将 变为 。注意到:
因此 ,恰好是目标结果。
执行过程可视化
以 nums = [1, 2, 3, 4, 5, 6, 7], k = 3 为例:
| 步骤 | 操作 | 数组状态 |
|---|---|---|
| 初始 | — | [1, 2, 3, 4, 5, 6, 7] |
| 1 | nums.reverse() —— 反转整个数组 | [7, 6, 5, 4, 3, 2, 1] |
| 2 | 反转前 k=3 个元素 [7, 6, 5] | [5, 6, 7, 4, 3, 2, 1] |
| 3 | 反转后 n-k=4 个元素 [4, 3, 2, 1] | [5, 6, 7, 1, 2, 3, 4] ✅ |
步骤 1 之后发生了什么?原本在末尾的 [5, 6, 7] 被翻到了开头(变成 [7, 6, 5]),原本在开头的 [1, 2, 3, 4] 被翻到了末尾(变成 [4, 3, 2, 1])。此时两段的位置已经正确了,只是各自内部是逆序的——于是步骤 2 和 3 各翻转一次,恢复原序。
🧠 直觉理解:想象你手里有一叠牌,要把最上面的 3 张挪到最下面。三次反转法就是——① 整叠牌翻个面 ② 前 3 张翻回来 ③ 剩下的翻回来。每张牌经过两次翻转,方向最终还是正的,但整体顺序已经发生了变化。
再以 nums = [-1, -100, 3, 99], k = 2 为例:
| 步骤 | 操作 | 数组状态 |
|---|---|---|
| 初始 | — | [-1, -100, 3, 99] |
| 1 | 反转整个数组 | [99, 3, -100, -1] |
| 2 | 反转前 k=2 个元素 [99, 3] | [3, 99, -100, -1] |
| 3 | 反转后 n-k=2 个元素 [-100, -1] | [3, 99, -1, -100] ✅ |
完整代码
class Solution: def rotate(self, nums: List[int], k: int) -> None: """ Do not return anything, modify nums in-place instead. """ k %= len(nums) nums.reverse() nums[:] = nums[:k][::-1] + nums[k:][::-1]逐行解释:
-
k %= len(nums):处理k >= n的情况。当k = n时,轮转后数组不变;当k > n时,每n步回到原位,只需轮转k % n步。 -
nums.reverse():反转整个数组。这是 Python 列表的原地方法,将数组首尾颠倒。 -
nums[:k][::-1]:取前k个元素(即原数组末尾k个元素的逆序),再[::-1]反转一次——恢复成原本的顺序。 -
nums[k:][::-1]:取后n-k个元素(即原数组开头n-k个元素的逆序),同样反转恢复原序。 -
nums[:] = ...:用全切片赋值,确保原地修改列表内容,而非创建新列表。
为什么三次反转是对的?
从索引的角度推导:设 。原数组中位置 i 的元素,经过第一次整体反转后到了位置 。然后:
- 若 (即元素原本在末尾
k个中),它会在步骤 2 中被再次翻转,最终位置是 - 若 (即元素原本在开头
n-k个中),它会在步骤 3 中被再次翻转,最终位置是
两种情况恰好对应了轮转 k 步后每个元素应有的位置。
💡 小贴士:索引推导虽然严谨,但不如画图直观。建议用纸笔跑一遍示例,比公式更容易理解。
复杂度
- 时间复杂度:。反转整个数组是 ,两次切片反转分别是 和 ,总复杂度 。
- 空间复杂度:(算法层面)。
nums.reverse()是原地操作。nums[:k][::-1]会创建 的临时列表,nums[k:][::-1]会创建 的临时列表,但这些临时对象在赋值完成后立即被 GC 回收。若面试官要求严格的 ,见下方”代码优化”章节的手动反转版本。
解题思路(环状替换)
除三次反转外,另一种 空间的原地算法是环状替换。思路是:从位置 0 开始,将 nums[0] 移到 nums[(0 + k) % n],再将被挤走的元素移到它的目标位置,如此往复,直到回到起点。如果一轮环结束后还有元素未处理,从下一个位置开启新环。
class Solution: def rotate(self, nums: List[int], k: int) -> None: n = len(nums) k %= n count = 0 # 已移动的元素个数
for start in range(n): if count >= n: # 所有元素都已就位 break
cur = start prev = nums[start]
while True: nxt = (cur + k) % n prev, nums[nxt] = nums[nxt], prev cur = nxt count += 1
if cur == start: # 回到起点,本轮环结束 break外层循环 for start 恰好执行 次——每个独立的环包含 个元素。
💡 小贴士:环状替换的思路非常精妙,体现了置换群中”轮换分解”的思想。但实现时容易出错的点很多——何时停止、
prev的更新顺序、start的推进条件……相比之下,三次反转法几乎不会写错。
三种方法的对比:
| 对比项 | 额外数组 | 环状替换 | 三次反转 |
|---|---|---|---|
| 时间复杂度 | |||
| 空间复杂度 | |||
| 代码简洁度 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 思维难度 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 容易写对 | ✅ | ⚠️ 容易出错 | ✅ |
🧠 面试建议:三种方法都值得了解,但如果只记一种——记三次反转。它代码最短、最不容易出错,且能自然扩展到”向左轮转”(先反转前
k个和后n-k个,再整体反转)。
代码优化
手动实现子区间反转(严格的 空间)
用户代码中 nums[:k][::-1] 虽然简洁,但切片 [::-1] 会创建临时列表。如果面试官追问空间复杂度,可以手动实现区间反转:
class Solution: def rotate(self, nums: List[int], k: int) -> None: n = len(nums) k %= n
def reverse(l: int, r: int) -> None: """反转 nums[l:r](左闭右开)""" r -= 1 while l < r: nums[l], nums[r] = nums[r], nums[l] l += 1 r -= 1
reverse(0, n) # 反转整个数组 reverse(0, k) # 反转前 k 个 reverse(k, n) # 反转后 n-k 个这个版本没有任何额外数组分配,空间复杂度严格为 。reverse 函数使用双指针交换,每对元素交换一次,总共 次交换。
切片反转 vs 手动反转
| 对比项 | nums[:k][::-1] | reverse(0, k) |
|---|---|---|
| 空间复杂度 | (创建临时列表) | |
| 代码行数 | 1 行 | 需要辅助函数 |
| Pythonic | ✅ 非常 Pythonic | ⭐ 更偏底层 |
| 适合场景 | 日常刷题、快速通过 | 面试官要求严格 |
两种写法都是正确的,选择取决于场景。日常刷题用第一种(够简洁),面试建议先写第一种,然后主动提一句”如果需要严格 ,可以手动实现子区间反转”——这会让面试官觉得你对细节有掌控力。
向左轮转
如果题目改为向左轮转 k 步,只需调整反转顺序:
# 向右轮转 k 步:整体反转 → 反转前 k → 反转后 n-kreverse(0, n)reverse(0, k)reverse(k, n)
# 向左轮转 k 步:反转前 k → 反转后 n-k → 整体反转reverse(0, k)reverse(k, n)reverse(0, n)💡 小贴士:想不通为什么?“向右轮转 步”等价于”向左轮转 步”,把 换成 代入向右轮转的公式就清楚了。
总结
「轮转数组」是数组操作的经典题目,它的核心在于理解”轮转”的等价变换——将末尾元素移到开头。
三种解法各有千秋:
- 额外数组:最直观,适合快速确认思路
- 环状替换:最巧妙,体现了置换群中轮换分解的思想
- 三次反转:最优雅,代码短且不易出错,是面试首选
🧠 核心收获:数组轮转 = 三次反转。这个技巧不仅适用于本题,在「反转字符串中的单词」(LeetCode 151)、「左旋转字符串」(剑指 Offer 58-II)等题目中也有类似的思路——先整体反转,再局部修正。
解决本题后,可以挑战以下延伸题目:
- 61. 旋转链表:将数组换成链表——同样是轮转,但链表无法直接反转,需要找断点重连
- 153. 寻找旋转排序数组中的最小值:一个已排序数组被轮转后,如何用二分查找找到最小值?
- 796. 旋转字符串:判断一个字符串是否是另一个字符串轮转得到的——巧妙的拼接技巧
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









