2010年那道408代码题,我到现在都记得。那年是408统考第一年,题目里考了一个数组循环左移,后来我在力扣上刷到189题“轮转数组”,发现这不就是同一道题的“复刻”吗?一个左移,一个右移,本质完全一样。刷过这道题的人,去看408真题会发现考场上的题目甚至比力扣版本还要简单一点。
这篇我打算把这题彻底掰开揉碎,从题目本身到三种主流解法,再到三次反转法的原理推导,最后结合408考场上真实的评分逻辑,说说到底怎么写才能拿满分。无论你是准备考研408,还是刷题准备面试,这篇文章都能帮你把这道题吃透。
1. 题目是什么?为什么这道题能同时“混迹”考研和面试圈
1.1 先看题:题目描述与示例
力扣189题的描述非常直白:给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
看两个官方示例就明白了:
示例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]
就这么一道题,力扣的标签是“中等”,但实际难度在中等里偏简单。真正让它值钱的不只是力扣,而是2010年那次408统考直接考了它的“孪生兄弟”——循环左移。
1.2 这道题的两副面孔:408真题与力扣189的渊源
2010年408代码题原文大概是这样的:设将n(n>1)个整数存放到一维数组R中,设计一个在时间和空间两方面都尽可能高效的算法,将R中保存的序列循环左移p(0<p<n)个位置。
我把两个题目并排对照着看:
| 对比项 | 力扣189 | 2010年408真题 |
|---|---|---|
| 移动方向 | 向右 | 向左 |
| 输入形式 | 数组+非负整数k | 数组+整数p |
| 限制条件 | k可能大于n | 0<p<n |
| 要求 | 实现函数 | 设计算法并说明空间时间 |
| 语言环境 | C++/Java/Python等 | C语言(考场上手写) |
方向相反,但核心思路完全是一个模子。你只要会做其中一个,另一个稍微转个弯就行。这也是为什么力扣189至今仍然是热点题——刷它不仅仅是为了应付面试,这是“一鱼两吃”,考研人和求职党都绕不开。
1.3 考察的能力拆解:简单题不简单
很多人觉得这题简单,不就是“把最后的元素移到最前面”吗?循环做k次不就行了?但真这么做,你就掉坑里了。
这题真正考验的是三件事:
- 空间意识:考研408的代码题,评分标准里明确写了“空间尽可能高效”。暴力解法开一个临时数组,空间O(n),在LeetCode上能过,但在408考场上很可能直接被扣分。
- 边界处理:
k可能大于数组长度,可能等于0,数组可能只有一个元素。这些边界条件不处理好,代码就崩。 - 对反转操作的理解:三次反转法是这道题的灵魂,也是408标准答案的核心。它用O(1)的额外空间完成了整个轮转,考察的正是对数组操作的底层理解。
所以别小看这道“简单题”,它是考研408和面试算法题的交叉点,是所有算法学习的“试金石”。
需要模型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[-1]
for i in range(n - 1, 0, -1):
nums[i] = nums[i - 1]
nums[0] = last
这个解法在力扣上能过示例,但一旦数组变长、k变大,立刻超时。为什么?因为时间复杂度是O(n*k),每一轮都要把整个数组移动一次。举个例子,n=100000,k=50000,那就是50亿次操作,放到考场上不仅时间撑不住,阅卷老师看到这个复杂度直接就不想给分了。
这个问题暴露了一个核心考点:算法不能只看“能出结果”,更要看复杂度和空间占用。这也是408考试和面试算法题“筛人”的关键。
2.2 额外数组解法:空间换时间的典型代表
既然暴力超时,那换一个思路:能不能新开一个数组,把每个元素放到它最终该去的位置上?
python复制def rotate(nums, k):
n = len(nums)
ans = [0] * n
for i in range(n):
ans[(i + k) % n] = nums[i]
for i in range(n):
nums[i] = ans[i]
新数组的第 (i+k)%n 个位置,就是原数组第 i 个元素移动后的位置。这个公式需要写对——很多人第一次会写成 (i+k) % n 还是 (i-k) % n 搞混,实际上多推两步就明白了。
这个解法时间O(n),空间O(n),在力扣上能过,但拿不到“最优化”的头衔。408考场上如果明文要求空间尽可能高效,开额外数组就是在送分给阅卷老师。这就是为什么三次反转法是标准答案——它才是这道题真正的核心。
2.3 三次反转法:考场上的标准答案长什么样
三次反转是这道题最经典的解法,也几乎是408标准答案的代名词。核心思路一句话:先整体反转,再反转前k个,再反转后n-k个。
直接用例子推演一遍:nums = [1,2,3,4,5,6,7], k = 3
第一次整体反转:[7,6,5,4,3,2,1]
第二次反转前3个元素(下标0到2):[5,6,7,4,3,2,1]
第三次反转后4个元素(下标3到6):[5,6,7,1,2,3,4]
结果与轮转后的目标完全一致。
写成代码,C++版本长这样:
cpp复制class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k = k % n;
reverse(nums, 0, n - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, n - 1);
}
void reverse(vector<int>& nums, int left, int right) {
while (left < right) {
swap(nums[left++], nums[right--]);
}
}
};
时间复杂度O(n),空间复杂度O(1)。这个复杂度组合让它在408和面试里都是满分答案。
2.4 三种解法的对比
| 解法 | 时间复杂度 | 空间复杂度 | 力扣能否通过 | 408考场上评价 |
|---|---|---|---|---|
| 暴力循环 | O(n*k) | O(1) | 大概率超时 | 直接淘汰 |
| 额外数组 | O(n) | O(n) | 能过 | 空间不达标,可能扣分 |
| 三次反转 | O(n) | O(1) | 高效通过 | 满分标准答案 |
三道坎走下来你就明白了,为什么三道解法看着都能“轮转”,408考高分的人偏偏只认第三种。
3. 把三次反转啃透:S级原理与推导
3.1 用一个小例子带你手推整个流程
光看上面的代码还不够,我自己带学生的时候发现,很多人即使会写三次反转,也说不出为什么它是对的。这就很危险——面试官一问“为什么三次反转能实现轮转?”,当场就蒙了。
我建议你拿纸笔,按下面的步骤自己推一遍。以 nums = [1,2,3,4,5], k = 2 为例:
第一步:整体反转 [1,2,3,4,5],得到 [5,4,3,2,1]
第二步:反转前2个元素 [5,4],得到 [4,5,3,2,1]
第三步:反转后3个元素 [3,2,1],得到 [4,5,1,2,3]
最终结果 [4,5,1,2,3] 正是原数组右移2位得到的数组。每一步都清晰可验。
3.2 为什么三次反转能成立?核心逆运算思维
这里的关键在于理解一个数学直觉:数组整体反转后,原来靠前的元素跑到了后面,原来靠后的元素跑到了前面;然后分两段各自反转内部顺序,就能让两段元素分别“归位”,同时两段的相对位置已经交换。
你可以把数组想象成两摞扑克牌。整体反转相当于把整副牌上下颠倒,左半拉右半拉互换了位置,但每张牌在各自段落内部是反着的。接着分别把每一段再反向翻一次,翻回正面,这时候两段牌的位置正好完成了交换。
这个思路在算法圈叫“两次逆序抵消”:一个元素在整体反转后从位置i变到了n-1-i,再在段落内反转一次,就刚好能从段落的头尾颠倒回正确的位置。用数学表示就是:右移k位等价于“把所有元素逆序,再让前k个元素内部逆序,再让后n-k个元素内部逆序”。
还有一个更抽象的理解方式:轮转右移k位,本质上就是把数组分成“前n-k个”和“后k个”两个部分,把后一部分放到前一部分前面去。三次反转做的事情,就是不借助额外空间地完成两个子数组的交换。
3.3 左移怎么办?一题学会两类变换
既然408真题考的是左移,你光会右移是不够的。我直接说结论:
右移k位,等价于左移n-k位。所以你可以把右移问题全部转成左移来处理,反之亦然。
如果题目明确要求左移p位,比如408真题那样,解法就变成:
- 反转整个数组
- 反转后p个元素
- 反转前n-p个元素
举个例子,nums = [1,2,3,4,5],左移2位期望结果是 [3,4,5,1,2]:
第一次整体反转:[5,4,3,2,1]
第二次反转后2个元素:[5,4,3,1,2]
第三次反转前3个元素:[3,4,5,1,2]
看出规律了吗?三次反转和右移的区别,只在于“第二段”和“第三段”的范围划分不同。右移是反转前k个再反转后n-k个,左移是反转后p个再反转前n-p个。本质上都是抓住一个分段点,把两段数组交换位置。
所以你在刷题的时候,只需要把“右移”和“左移”两个模板都写一遍,考试出哪个都能秒杀。
3.4 代码实现:C++版和408考场的C版
408考场不允许用STL,所以手写reverse函数和swap函数是基本功。很多人习惯LeetCode上用C++的std::reverse一把梭,到了考场直接傻眼。
408考场上推荐的C代码写法:
c复制void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void reverse(int arr[], int left, int right) {
while (left < right) {
swap(&arr[left], &arr[right]);
left++;
right--;
}
}
// 数组R,长度n,循环左移p个位置
void rotateLeft(int R[], int n, int p) {
p = p % n; // 防止p大于n
if (p == 0) return;
reverse(R, 0, n - 1);
reverse(R, 0, n - p - 1);
reverse(R, n - p, n - 1);
}
这里有个细节很多人第一次写会出错:第二次反转的结束下标是 n-p-1,不是 n-p。第三段从 n-p 开始。为什么?因为前n-p个元素的下标范围是0到n-p-1,而后p个元素是n-p到n-1。下标边界弄错就是数组越界,在408考场上这种低级错误直接被扣分。
4. 408考场上的应试要点与评分逻辑
4.1 考场上第一步:审题时需要圈出关键词
408代码题不是让你直接写代码就完了,它有个隐性要求:题目里文字描述的每一个约束,都是在划重点。
2010年那道题,题目明确说了“设计一个在时间和空间两方面都尽可能高效的算法”。这句话翻译一下就是:时间复杂度要低,空间复杂度也要低。你在做题时看到这个表述,第一反应就应该是三次反转,而不是开一个新数组。
408的阅卷评分逻辑,通常按下面几个维度来:
- 算法思路是否清晰(有时候要写算法思想)
- 数据结构定义是否合理
- 代码是否完整可运行
- 时间、空间复杂度分析是否正确
- 边界条件是否处理
很多考生栽在最后一点上。p=0的时候,什么操作都不需要做,直接返回;p=n的时候,相当于轮转了一整圈,数组保持不变。这两个边界在代码里不加判断,理论上也“碰巧能过”,但如果你分析复杂度的时候把循环写成了O(n)扫描,还是会浪费不必要的操作。
4.2 代码书写规范:阅卷老师到底在看什么
408考场上写字空间有限,人手一卷答题卡,你不可能写一大坨代码。阅卷老师看的核心是关键步骤有没有到、函数名和逻辑是否自洽、有没有明显的越界或逻辑漏洞。
我的习惯是,在写代码之前,先用一两句话写出算法思想,例如:“先将数组整体逆置,再逆置前n-p个元素,最后逆置后p个元素,即可完成左移”。然后代码写出来,尽量控制在15行以内,确保n=1、p=0的时候不会出问题。
三次反转的代码本身就短,逻辑清晰,阅卷老师一眼就能看出你会不会。如果你写了额外数组的方案,时间上虽然是O(n),但空间O(n)不满足题干“空间尽可能高效”的要求,在408阅卷标准里是会被扣分的。
4.3 几种“扣分陷阱”:数组越界、负数取模
我在帮学生批改代码题的时候,总结出几个高频翻车点:
- 取模遗漏:力扣版本里k可能远大于n,不取模就直接反转,结果完全错误。
- 右移和左移搞混:左移的时候,第二次反转的范围是
0~n-p-1,第三次是n-p~n-1。右移时,第二次是0~k-1,第三次是k~n-1。这两个模板不能记串。 - 算法思想没写:408要求“说明算法的设计思想”,你不写,阅卷老师可能认为你只是碰巧背了代码。
- 复杂度分析错误:三次反转里调用三次reverse,每次找中点,总时间复杂度是O(n/2)+O(k/2)+O((n-k)/2)=O(n),空间O(1)。有一次我批改时看到有人写“时间复杂度O(3n)”,虽然也不算错,但不如直接写O(n)规范。
注意:408考场上,代码题一定要把“算法思想”写在代码前面,让阅卷老师第一眼就能看到你的思路。这是拿分的关键。
4.4 真题与力扣的对应:用198页真题的功夫刷一道题
再回来看那道2010年408真题,和力扣189的对应关系其实非常精妙。408真题是“左移”p位,力扣是“右移”k位;但408的p被限制在0<p<n,所以不需要处理k>n的取模情况。换句话说,力扣拿掉最麻烦的那个边界条件,就变成408真题;408真题加上一个取模,就变成力扣189。所以刷这一道题,相当于同时拿下了两类变体。
我建议你刷题的时候,不要只满足于提交通过,而是主动做三件事:把右移改成左移再写一遍;把C++换成C语言手写swap再写一遍;把k改成可能大于n的情况再检查一遍。这三遍下来,这道题才算真正吃透。这个过程比连续刷十道简单题都有价值。
5. 常见问题与调试实战
5.1 问题速查表
按照我平时答疑的习惯,把这道题的高频问题整理成了一张表,方便你对照排查:
| 现象 | 原因 | 解法 |
|---|---|---|
| k=7,数组长度7,输出和原来一样 | 轮转完整一圈,所有元素归位 | 先 k %= n |
| k很大,比如k=100,数组长度7 | 没取模,反转范围越界 | 在进入反转前加取模 |
| 反转后数组整体是正确的,但前半段乱序 | 第二次反转的指针写错 | 检查reverse的left/right边界 |
| 左移实现后结果变右移了 | 三次反转的顺序或范围弄反 | 对照题目方向重新确认 |
| 单元素数组轮转后出错 | 边界条件n=1未处理 | 反转前先判断 n <= 1 直接返回 |
| C语言数组越界报错 | swap函数指针使用不当 | 确认传入的是数组元素地址 |
5.2 实战调试:一段错代码的逐步修正
我给你看一段真实调试过程。有一个学生最初写了这样的代码:
c复制void rotateLeft(int R[], int n, int p) {
reverse(R, 0, n - 1);
reverse(R, 0, p - 1);
reverse(R, p, n - 1);
}
他以为“左移p位”就是“反转整体+反转前p个+反转后n-p个”,结果输出完全不对。他用 R = [1,2,3,4,5], p = 2 手动推了一遍:
第一次整体反转:[5,4,3,2,1]
第二次反转前p=2个:[4,5,3,2,1]
第三次反转后3个:[4,5,1,2,3]
期望输出是 [3,4,5,1,2],结果差了十万八千里。问题出在哪?他把“左移p位”当成了“前p个元素要放到最后”。实际上左移p位,应该把数组看成前p个元素移到最后,等价于“前p个元素”和“后n-p个元素”交换位置。正确写法是反转前n-p个和后p个,而不是前p个和后n-p个。
修正后:
c复制void rotateLeft(int R[], int n, int p) {
reverse(R, 0, n - 1);
reverse(R, 0, n - p - 1);
reverse(R, n - p, n - 1);
}
再验证一次:[1,2,3,4,5], p=2
整体反转:[5,4,3,2,1]
反转前3个(0~2):[3,4,5,2,1]
反转后2个(3~4):[3,4,5,1,2]
正确。
这个坑就是“反转范围到底是p还是n-p”的问题,几乎每个初学者都踩过一次。记住一句话:左移p位就是把前p个元素挪到后面去,所以后面那一段是“前p个元素的归宿”,反转的后半段范围是p到n-1;但为了开始交换,我们要把“前面要留下来的n-p个元素”先反转到正确位置,所以中间那一次反向的是n-p个。 这逻辑搞通了,就再也不会记混。
5.3 记忆口诀与小技巧
我分享一个自己用的记忆口诀,右边打不过就反着来:
- 右移k位:反全 + 反前k + 反后n-k
- 左移p位:反全 + 反前n-p + 反后p
为什么这个口诀不容易记错?因为它对应的是“哪一段要挪到前头”。右移时后k个挪到前头,所以最后一段反转的是后n-k个;左移时前p个挪到后头,所以中间那一次反转的是前n-p个。每当你写代码前犹豫了,动手推一个5元素的小例子,10秒钟就能确认。
6. 延伸与扩展:一道题背后的面试题库
6.1 力扣上的变种题
189这道题刷透彻后,你可以顺藤摸瓜刷几个变种,这些变种都是面试里的高频题:
| 题目 | 与189的关系 |
|---|---|
| 力扣153题:寻找旋转排序数组中的最小值 | 对“轮转后的数组”做二分查找 |
| 力扣33题:搜索旋转排序数组 | 轮转后数组的二分查找变体 |
| 力扣48题:旋转图像 | 二维矩阵的“旋转”,底层用到的也是四次交换或反转思想 |
| 力扣796题:旋转字符串 | 字符串轮转后判断包含关系 |
刷这些题的时候你会发现一个规律:轮转的本质是“分段交换”。不管是数组还是一维字符串,不管是左移还是右移,核心都是先找到分段点,再把两部分交换。而交换两个子数组,不借助额外空间的做法,最经典的就是三次反转。
6.2 面试官追问的深层考点
面试官问到189题,通常不会只要你给出一个解就结束。我遇到过多次,面试官会在你写完三次反转之后追问:
- “你能证明三次反转的正确性吗?”→ 用逆序抵消的思路回答,把两次反转的作用等价于“未反转”,但整体位置已经交换。
- “如果数据量大到内存放不下怎么办?”→ 这就不是普通数组轮转,要涉及分块和外部排序的思维。
- “如果不仅要轮转,还要返回每个元素轮转后的下标,怎么写?”→
(i+k)%n这个公式顺手就答上来了。
这里我想强调“为什么刷力扣有用”这个热搜词背后的逻辑。大厂面试官考189,并不是真的需要你会轮转数组,而是通过这道题看你有没有空间意识、能不能分析边界、愿不愿意优化。三次反转这个解法的出现,本身就代表了“在O(1)空间内完成数组变换”的思维模式。这种思维刷多了,你看到任意数组变换题的第一反应就会是“能不能原地完成”。
6.3 从这道题到“算法思维”的升华
我个人觉得,189题是少有的“一道题能打通三条路”的代表:
- 对考研党,它完美对应408真题,是线性表章节的经典考点。
- 对面试党,它是考察“原地算法”和“数组操作”的窗口。
- 对算法初学者,它是理解“逆序”“取模”“分段交换”的绝佳载具。
我经常跟学生说,刷题不在多在精。你能把189题从暴力到额外数组到三次反转都讲清楚,能徒手推演左移和右移两个版本,能在纸上写出C语言的完整代码并且指出边界条件,这道题就真正成为你的东西了。
如果再遇到轮转数组,你脑海里应该直接浮现三条路:最笨的循环移动、最直观的额外数组、最优雅的三次反转。然后毫不犹豫地选择第三条。
最后再分享一个实战里的小技巧。面试的时候如果面试官让你写“轮转数组”,你可以在写完三次反转后顺手补一句:“如果允许额外空间,还可以用 (i+k)%n 直接把元素放到新数组对应位置,但空间O(n),不够优。”这句话能展示你的知识广度。而408考场上,我建议你在写算法思想时一定要提到“空间复杂度O(1)”这个关键词,阅卷标准里看到它,印象分会拉满。踩了这么多次坑之后,我现在看到轮转数组,闭着眼都能写出那三行reverse。对你来说,多练几遍,背下那个口诀,这道题很快也会变成你的送分题。
