1. 这道题为什么会成为面试高频题
轮转数组(Rotate Array)在LeetCode上是第189题,难度标的是中等,但说实话,这道题的上限和下限差距非常大。我见过不少候选人在这道题上栽跟头,也见过有人靠这道题直接拿到offer——区别不在于会不会做,而在于能给出几层解法的递进。
题目本身很简单:给你一个数组,将数组中的元素向右轮转 k 个位置。比如 [1,2,3,4,5,6,7],k=3,结果是 [5,6,7,1,2,3,4]。
之所以高频,是因为它考察的不是某个冷门算法,恰恰是数组操作中最基础也最容易出错的两个能力:索引下标的数学推导能力,以及空间复杂度优化的思维层次。很多人一上来就能给出第一种解法,但能一口气写出三种解法并讲清楚每种解法的取舍,就完全是两个水平了。
另外还有个实际原因:这道题的变体在真实业务里太常用了。消息队列的消费位点回退、图片轮播图的循环切换、日程表按周循环排列,甚至游戏里音效数组的循环播放,本质都是轮转。刷题不能只为了面试,很多题你刷完就忘了,但这道题的思路是会真正写进业务代码里的。
这篇文章我按照自己刷这道题时的思考路径来写:先讲最直观的暴力解,再优化到空间换时间,最后给出最优的三次翻转法。每一层我都会讲清楚为什么这样做、复杂度怎么算、以及面试官可能会追问什么。最后再补充一些扩展变形和实际面试中的小建议。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先从暴力解法入手:能跑通,但别止步于此
2.1 最容易想到的写法
拿到这道题,大部分人第一反应是:每次向右轮转一个位置,执行 k 次。右移一个位置的操作就是保存最后一个元素,然后把前面所有元素往后挪一位,最后把保存的元素放到开头。
python复制def rotate(nums, k):
n = len(nums)
k = k % n # 注意这里,后面会解释
for _ in range(k):
last = nums[n - 1]
for i in range(n - 1, 0, -1):
nums[i] = nums[i - 1]
nums[0] = last
这段代码在LeetCode上能通过,但效率很惨。外层循环执行 k 次,内层循环每次要移动 n-1 个元素,时间复杂度是 O(k×n)。当数组长度是一万、k 是五千的时候,就是五千万次赋值操作,跑起来明显卡顿。
2.2 为什么必须对 k 取模
这里有个新手很容易忽略的细节:k 可能大于数组长度。比如数组长度是 7,k=10,轮转 10 次和轮转 3 次的效果完全一样,因为每轮转 7 次数组就回到原样了。
所以第一步一定先取模:
python复制k = k % n
取模之后 k 一定小于 n,这样能避免大量无效循环。一个长度为 7 的数组,k=1000000,不取模的话暴力解法要执行一百万次循环,取模后只需要执行 3 次。
注意:取模这一步不是可选项,是必须的。LeetCode的测试用例里一定有 k > n 的数据。哪怕你用的是后面更优的解法,第一步也要先取模。
2.3 暴力解法的真实定位
暴力解法虽然效率低,但它的价值在于:它是验证其他解法正确性的基准。我自己在写最优解的时候,会先用暴力解跑一遍小样本数据,把结果当作标准答案,然后用新解法去对拍。这个习惯帮我避免了至少三次因为边界条件写错导致的“看似正确实则跑偏”。
另外,在面试中如果你先给出暴力解,面试官通常会点头,然后追问“能不能优化”。这是正常的面试节奏,不是否定你。你完全可以顺着这条线继续深入。但如果你只能停在暴力解,那就危险了。所以暴力解的意义是保底,而不是终点。
3. 第二层解法:用额外数组实现空间换时间
3.1 核心思路
暴力解法慢在每次只移动一位,大量时间浪费在重复的搬运上。既然这样,干脆一步到位:创建一个新数组,直接把每个元素放到它最终应该去的位置上。
关键问题来了:元素在新数组中的位置怎么算?
对于原数组下标 i 的元素,向右轮转 k 位后,它的新下标是 (i + k) % n。这个公式是整道题的核心,一旦理解了这个公式,第二层和第三层解法都能顺畅推导出来。
套用公式的代码如下:
python复制def rotate(nums, k):
n = len(nums)
k = k % n
new_arr = [0] * n
for i in range(n):
new_arr[(i + k) % n] = nums[i]
for i in range(n):
nums[i] = new_arr[i]
注意最后一步,因为题目要求原地修改,所以必须把新数组的值拷回原数组。如果你直接返回新数组,那不算通过。
3.2 这个解法的复杂度分析
时间复杂度:遍历两次数组,O(n)。第一次是把元素放入新位置,第二次是拷贝回原数组。
空间复杂度:O(n),因为额外创建了一个长度为 n 的数组。
到了这一步,时间上已经是最优了——任何解法都要遍历数组才能完成移动,O(n) 已经到天花板了。但空间上还有优化空间。
3.3 面试官的典型追问:能不能原地完成?
这里就是区分普通候选人和优秀候选人的分水岭了。只要你能写出第二种解法,面试官大概率会接着问:“能不能不用额外数组,原地完成?”
有些人的第一反应是用一个临时变量去交换。但这个思路在轮转问题里很容易走死,因为一个临时变量只够交换两个元素,而轮转涉及的是 n 个元素的循环替换。
还有人说“用链表”,把最后一个节点指向第一个节点,然后走 n-k 步——这个思路在数组上不适用,因为题目明确要求数组,而且数组的随机访问特性决定了我们不该用链表思维去解。
正确的方向是:既然我们不能用额外空间,那就要想办法通过元素之间的多次交换来达到整体移动的效果。这就是下一章要讲的三次翻转法。
3.4 一个容易踩的坑
我在辅导别人刷题时发现,很多人在写第二种解法时会忘记:如果数组长度是 0 或者 1,代码要能直接返回,不能报下标越界。虽然取模之后 k 会是 0,但如果你的循环里依赖了某个元素的索引计算,边界情况仍然可能触发异常。
稳妥的做法是在函数开头加一行:
python复制if not nums or len(nums) == 1:
return
这道题很多题解不会特意提这个边界,但在真实面试中,边界处理本身就是考察点之一。你主动处理了,比面试官追问了再补,印象分完全不同。
4. 最优解:三次翻转法的原理与证明
4.1 从“整体移动”到“局部翻转”
三次翻转法是我认为这道题最优雅的解法,也是面试中应该重点展示的能力。先看代码,再讲原理:
python复制def rotate(nums, k):
n = len(nums)
k = k % n
def reverse(start, end):
while start < end:
nums[start], nums[end] = nums[end], nums[start]
start += 1
end -= 1
reverse(0, n - 1) # 第一步:整体翻转
reverse(0, k - 1) # 第二步:翻转前 k 个
reverse(k, n - 1) # 第三步:翻转剩余部分
以 [1,2,3,4,5,6,7],k=3 为例走一遍:
- 整体翻转:
[7,6,5,4,3,2,1] - 翻转前 3 个:
[5,6,7,4,3,2,1] - 翻转后 4 个:
[5,6,7,1,2,3,4]
结果完全正确。
4.2 为什么三次翻转能达到轮转效果
这是整篇博客最核心的问题,也是面试时你可以主动讲解的亮点。
整体翻转一次,数组的顺序完全反过来了。此时,原本的后 k 个元素跑到了数组最前面,但它们的内部顺序是反的。再看前 n-k 个元素,它们的内部顺序也是反的。所以第二步和第三步分别把这两段内部的顺序翻转回来,让两段的内部顺序恢复原样。
用数学语言来说:设原数组为 A + B,其中 B 是最后 k 个元素。整体翻转后变成 rev(B) + rev(A)。翻转前 k 个元素得到 B + rev(A),翻转后 n-k 个元素得到 B + A,也就是轮转后的结果。
一句话总结:整体翻转让后 k 个元素到达正确区域,两次局部翻转修正了各自内部的逆序。
4.3 时间复杂度与空间复杂度
时间复杂度:三次翻转,每次翻转都是 O(n) 的双指针遍历,总时间复杂度 O(n)。
空间复杂度:O(1),只用了常数个临时变量(双指针的 start、end 和交换用的中间变量)。
这个复杂度组合已经是这道题的最优解了,时间上不可能低于 O(n),空间上已经压到了 O(1)。
4.4 三次翻转法的边界条件和细节
边界条件比前两种解法要多,这也是你得分的机会。
**边界一:k=0。**取模后 k=0,第二步翻转前 0 个不需要执行,第三步翻转整个数组,但第一步整体翻转和第三步翻转互相抵消,结果数组不变。代码不会报错,但会白白执行两次 O(n) 的反转。可以加一个提前判断:
python复制if k == 0:
return
**边界二:k 恰好等于 n 的一半。**比如 n=6,k=3,整体翻转后前 3 个和后 3 个长度相同,局部翻转边界依然清晰,不会出错。
**边界三:reverse 函数里 start 和 end 的取值。**注意 (0, k - 1) 和 (k, n - 1),一个是闭区间到 k-1,一个从 k 开始到 n-1,两边没有重叠也没有遗漏。写错一位就会导致结果错误,而且很难发现,因为普通用例可能碰巧正确。
提示:强烈建议自己手写一遍 reverse 的调用过程,把每一步数组的状态写在纸上。这个习惯能帮你快速定位边界错误,比盯着屏幕空想有效得多。
4.5 为什么面试官偏爱这个解法
我在模拟面试中经常问这道题。如果候选人写到了额外数组解法,我会继续追问优化;如果候选人直接写出了三次翻转,我会让他讲一遍原理。
我真正在意的不是代码本身,而是他能不能讲清楚“为什么反转两次等于轮转”。很多人代码写出来了,但讲原理时支支吾吾,这说明他是背的题解,没有真正理解。反过来,能流畅讲出 4.2 节那段推导的候选人,我基本会打高分,因为这说明他有把“操作”抽象成“规律”的能力——这才是工程上最重要的能力。
5. 三种解法的横向对比与选型建议
5.1 复杂度与适用场景总览
把三种解法放在一张表里,结论一目了然:
| 解法 | 时间复杂度 | 空间复杂度 | 实现难度 | 适用场景 |
|---|---|---|---|---|
| 暴力移动 | O(k×n) | O(1) | 低 | 仅在数据量极小且 k 很小时可用 |
| 额外数组 | O(n) | O(n) | 低 | 不限制空间、代码要最简单直接时 |
| 三次翻转 | O(n) | O(1) | 中 | 面试答题、大规模数据、嵌入式等内存受限场景 |
从纯粹的性能角度看,三次翻转法全面碾压暴力法,和额外数组法在时间上打平、空间上胜出。那为什么还需要额外数组法呢?因为它的代码更直观、更不容易出错,在业务代码里可读性往往比微小的性能提升更值钱。
5.2 业务场景下的取舍逻辑
如果是在真实项目里,我的建议是分情况:
- 数据量小(几百个以内):随便写,暴力法都行,不用过度设计。
- 数据量中等(几万到几十万)且内存充足:额外数组法更清晰,团队维护成本低。
- 数据量很大(百万级以上)或内存受限:三次翻转法是唯一合理选择。O(n) 的额外空间在百万级数组上就是几个 MB 的内存,在嵌入式或移动端可能直接导致崩溃。
还有一个容易忽略的点:如果数组元素不是整数,而是复杂的对象,额外数组的拷贝成本会明显上升,这时候三次翻转法只需要交换引用,优势更突出。
5.3 我在实际项目里遇到的一个例子
有一次做图片轮播组件,产品要求支持任意步长的循环切换。最初实现版本就是暴力移动——每次切换都循环搬移整个图片数组,图片数量少的时候没问题,后来接入几百张图的数据源后,每次切换都卡顿明显。
后来改成三次翻转法,核心代码不到十行,切换一次从几十毫秒降到接近零耗时。而且这段代码只依赖数组本身,不引入任何额外依赖,测试也容易写。
那次经历让我对“刷题无用论”有了新的认识:不是题没用,是你还没遇到那个用得上的场景。
6. 变体问题与扩展思考
6.1 向左轮转怎么处理
题目默认向右轮转,但如果面试官问向左轮转 k 位呢?很简单,向左轮转 k 位等价于向右轮转 n-k 位。用三次翻转法时,先翻转整体,然后翻转前 n-k 个,再翻转后 k 个即可。
python复制def rotate_left(nums, k):
n = len(nums)
k = k % n
# 翻转前 n-k 个,再翻转后 k 个
reverse(0, n - k - 1)
reverse(n - k, n - 1)
reverse(0, n - 1)
理解这个变体的关键在于:左右轮转本质上是同一个问题的两个方向,核心仍然是下标运算和翻转思想。
6.2 数组反转的更多应用
三次翻转法的思想不只是用在轮转数组上。LeetCode上还有一道“反转字符串中的单词”,比如 "the sky is blue" 变成 "blue is sky the",标准做法就是先整体反转整个字符串,再逐个反转每个单词。思路和轮转数组如出一辙:先让元素到达目标区域,再修正各区域的内部顺序。
再比如“旋转二维数组”或者“旋转链表”的题目,核心思路也是找规律 + 局部修正,只是数据结构变了,翻转的具体实现方式不同。我建议你刷完这道题之后,立刻去做反转字符串中的单词和旋转链表,对比三者的共性与差异,这个对比过程比多刷十道新题更有价值。
6.3 如果 k 是负数怎么办
LeetCode原题不会给负数,但我在半结构化面试中问过这个问题。负数 k 表示向左轮转,只需要做一步转换:
python复制if k < 0:
k = n - (abs(k) % n)
或者更简洁地:k = (k % n + n) % n,这样无论正负都能统一处理。
这里有个小坑:Python 的取模运算对负数返回的是非负结果,-3 % 7 = 4,可以直接用。但在 C++ 或 Java 里,-3 % 7 = -3,必须手动调整。如果你应聘的岗位主要用 C++/Java,建议把这一步写成显式判断,不要依赖语言特性。
6.4 上升到抽象思维层面
三种解法的本质是什么?暴力法是在“模拟过程”,额外数组法是在“建立映射”,三次翻转法是在“利用逆变换”。这三个层次对应了解决数组类问题的三种思维模型:
- 模拟:一步一步执行,适合小数据量,思路最简单。
- 映射:找到元素最终位置的数学公式,一次放置到位。
- 组合变换:把复杂操作分解成多个简单操作的叠加,利用操作的数学性质达到目的。
当你面对一道陌生的数组题时,先问自己:这个操作能不能用数学公式表达?如果能,就直接用映射;如果不能,看看能不能拆成几个简单操作的组合。这两种思路几乎覆盖了所有数组类算法题。
7. 刷题之外的几点私房建议
这道题我刷过不下十遍,每次带新人或者模拟面试都会重新讲一遍。最后分享几个个人体会。
面试的时候,如果你先写暴力解,不要急着擦掉重写。明确告诉面试官:“这是最基础的解法,复杂度是 O(k×n),接下来我优化到 O(n)。”然后一步步递进。这个过程本身就是展示思维能力的窗口,比直接甩出最优解更能体现你的工程素养。
关于取模,很多人只记得写法,不理解它是求“有效位移数”。你可以这样记:轮转 n 次等于没轮转,所以有效位移数 k 要对 n 取余。这个理解能帮你自动处理 k > n 的情况,也能应对负数的变体。
写翻转函数的时候,建议单独提取一个 reverse 函数,不要在三个地方复制粘贴。不仅是为了代码整洁,更是为了减少出错概率。我在代码评审中见过不少因为翻转区间写错而导致的 bug,绝大多数都源于翻转逻辑散落多处、改动时只改了一处。
最后一点,也是最重要的一点:不要只看题解就觉得自己会了。合上代码,自己在编辑器里从零写一遍,再用几个不同 k 值和边界用例去验证。能独立写对三遍以上,这道题才真正属于你。
