mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
2405 字
6 分钟
轮转数组
2026-07-01

题目描述#

题目链接

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

输入#

整数数组 nums 和非负整数 k

输出#

原地修改 nums,将其向右轮转 k

约束条件#

  • 1 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31 - 1
  • 0 <= k <= 10^5

进阶约束#

  • 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
  • 你可以使用空间复杂度为 O(1)O(1)原地 算法解决这个问题吗?

示例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 指向新列表(不会影响函数外部的原数组)。

瓶颈分析:这个方法需要 O(n)O(n) 的额外空间。n105n \le 10^5 时,10510^5 个整数的数组大约占 800KB——可以通过,但进阶要求挑战 O(1)O(1) 空间。

🧠 直觉理解:创建新数组就像搬家时先把所有家具搬到临时仓库,再按新布局搬回去——稳,但需要两倍的空间。能不能直接在原地”挪”家具?

三次反转法#

这是本题最优雅的解法。将 nums 向右轮转 k 步,等价于以下三步:

  1. 反转整个数组——让末尾的 k 个元素跑到开头(此时是逆序的),开头的 n-k 个元素跑到末尾(同样逆序)
  2. 反转前 k 个元素——恢复它们原本的顺序
  3. 反转后 n-k 个元素——恢复它们原本的顺序

用数学语言描述:设原数组为 AA,目标是将 AA 变为 A[nk:]+A[:nk]A[n-k:] + A[:n-k]。注意到:

reverse(A)[:k]=reverse(A[nk:])\text{reverse}(A)[:k] = \text{reverse}(A[n-k:]) reverse(A)[k:]=reverse(A[:nk])\text{reverse}(A)[k:] = \text{reverse}(A[:n-k])

因此 reverse(reverse(A)[:k])+reverse(reverse(A)[k:])=A[nk:]+A[:nk]\text{reverse}(\text{reverse}(A)[:k]) + \text{reverse}(\text{reverse}(A)[k:]) = A[n-k:] + A[:n-k],恰好是目标结果。

执行过程可视化#

nums = [1, 2, 3, 4, 5, 6, 7], k = 3 为例:

步骤操作数组状态
初始[1, 2, 3, 4, 5, 6, 7]
1nums.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]

逐行解释:

  1. k %= len(nums):处理 k >= n 的情况。当 k = n 时,轮转后数组不变;当 k > n 时,每 n 步回到原位,只需轮转 k % n 步。

  2. nums.reverse():反转整个数组。这是 Python 列表的原地方法,将数组首尾颠倒。

  3. nums[:k][::-1]:取前 k 个元素(即原数组末尾 k 个元素的逆序),再 [::-1] 反转一次——恢复成原本的顺序。

  4. nums[k:][::-1]:取后 n-k 个元素(即原数组开头 n-k 个元素的逆序),同样反转恢复原序。

  5. nums[:] = ...:用全切片赋值,确保原地修改列表内容,而非创建新列表。

为什么三次反转是对的?#

从索引的角度推导:设 n=len(nums)n = \text{len(nums)}。原数组中位置 i 的元素,经过第一次整体反转后到了位置 n1in - 1 - i。然后:

  • n1i<kn - 1 - i < k(即元素原本在末尾 k 个中),它会在步骤 2 中被再次翻转,最终位置是 k1(n1i)=i+knk - 1 - (n - 1 - i) = i + k - n
  • n1ikn - 1 - i \ge k(即元素原本在开头 n-k 个中),它会在步骤 3 中被再次翻转,最终位置是 (n1)(n1ik)=i+k(n - 1) - (n - 1 - i - k) = i + k

两种情况恰好对应了轮转 k 步后每个元素应有的位置。

💡 小贴士:索引推导虽然严谨,但不如画图直观。建议用纸笔跑一遍示例,比公式更容易理解。

复杂度#

  • 时间复杂度O(n)O(n)。反转整个数组是 O(n)O(n),两次切片反转分别是 O(k)O(k)O(nk)O(n-k),总复杂度 O(n)O(n)
  • 空间复杂度O(1)O(1)(算法层面)。nums.reverse() 是原地操作。nums[:k][::-1] 会创建 O(k)O(k) 的临时列表,nums[k:][::-1] 会创建 O(nk)O(n-k) 的临时列表,但这些临时对象在赋值完成后立即被 GC 回收。若面试官要求严格的 O(1)O(1),见下方”代码优化”章节的手动反转版本。

解题思路(环状替换)#

除三次反转外,另一种 O(1)O(1) 空间的原地算法是环状替换。思路是:从位置 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 恰好执行 gcd(n,k)\gcd(n, k) 次——每个独立的环包含 n/gcd(n,k)n / \gcd(n, k) 个元素。

💡 小贴士:环状替换的思路非常精妙,体现了置换群中”轮换分解”的思想。但实现时容易出错的点很多——何时停止、prev 的更新顺序、start 的推进条件……相比之下,三次反转法几乎不会写错。

三种方法的对比:

对比项额外数组环状替换三次反转
时间复杂度O(n)O(n)O(n)O(n)O(n)O(n)
空间复杂度O(n)O(n)O(1)O(1)O(1)O(1)
代码简洁度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
思维难度⭐⭐⭐⭐⭐⭐⭐
容易写对⚠️ 容易出错

🧠 面试建议:三种方法都值得了解,但如果只记一种——记三次反转。它代码最短、最不容易出错,且能自然扩展到”向左轮转”(先反转前 k 个和后 n-k 个,再整体反转)。

代码优化#

手动实现子区间反转(严格的 O(1)O(1) 空间)#

用户代码中 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 个

这个版本没有任何额外数组分配,空间复杂度严格为 O(1)O(1)reverse 函数使用双指针交换,每对元素交换一次,总共 nn 次交换。

切片反转 vs 手动反转#

对比项nums[:k][::-1]reverse(0, k)
空间复杂度O(k)O(k)(创建临时列表)O(1)O(1)
代码行数1 行需要辅助函数
Pythonic✅ 非常 Pythonic⭐ 更偏底层
适合场景日常刷题、快速通过面试官要求严格 O(1)O(1)

两种写法都是正确的,选择取决于场景。日常刷题用第一种(够简洁),面试建议先写第一种,然后主动提一句”如果需要严格 O(1)O(1),可以手动实现子区间反转”——这会让面试官觉得你对细节有掌控力。

向左轮转#

如果题目改为向左轮转 k 步,只需调整反转顺序:

# 向右轮转 k 步:整体反转 → 反转前 k → 反转后 n-k
reverse(0, n)
reverse(0, k)
reverse(k, n)
# 向左轮转 k 步:反转前 k → 反转后 n-k → 整体反转
reverse(0, k)
reverse(k, n)
reverse(0, n)

💡 小贴士:想不通为什么?“向右轮转 kk 步”等价于”向左轮转 nkn-k 步”,把 kk 换成 nkn-k 代入向右轮转的公式就清楚了。

总结#

「轮转数组」是数组操作的经典题目,它的核心在于理解”轮转”的等价变换——将末尾元素移到开头。

三种解法各有千秋:

  • 额外数组:最直观,适合快速确认思路
  • 环状替换:最巧妙,体现了置换群中轮换分解的思想
  • 三次反转:最优雅,代码短且不易出错,是面试首选

🧠 核心收获:数组轮转 = 三次反转。这个技巧不仅适用于本题,在「反转字符串中的单词」(LeetCode 151)、「左旋转字符串」(剑指 Offer 58-II)等题目中也有类似的思路——先整体反转,再局部修正。

解决本题后,可以挑战以下延伸题目:

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

轮转数组
https://www.hygen.red/posts/hot100/05-普通数组/015轮转数组/
作者
Hygen
发布于
2026-07-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录