这道题我在刷力扣的时候一眼就认出来了,这不就是2010年408那张卷子最后那道代码大题吗。数组轮转,力扣叫Problem 189,408里叫"循环左移p位",连解法套路都一模一样。很多考研的同学刷到这道题会嘀咕:力扣里是右移k位,408是左移p位,这俩到底是不是同一道题?有没有通用解法?今天我把题目本身、四种常见思路、三次翻转的原理和边界、还有408考场上到底怎么答才能拿满分,一次性讲透。
这篇文章适合两类人:一类是刷力扣准备面试的,想彻底搞清楚为什么reverse三次就能完成轮转;另一类是准备408统考的,想知道这种代码大题的标准答题格式和拿分策略。两种问法,同一个核心考点,看完这篇你就不会再混淆了。
1. 一道老题,两种问法:此题为何值得反复做
很多考研的同学第一次看到2010年408真题时,会觉得题目描述非常"学术":设将n个整数存放到一维数组R中,设计一个在时间和空间两方面尽可能高效的算法,将R中保存的序列循环左移p个位置。翻译成人话,就是把数组前p个元素挪到后面去,其余元素依次往前补位。这跟力扣189右移k位本质上是一个操作,只是一个向左一个向右。
1.1 2010年408真题到底考了什么
408统考的数据结构代码大题,分值一般是13到15分,2010年这道题是典型的"数组操作"题型。它有三个小问,分别是设计算法思想、写出可读的代码、说明时间复杂度和空间复杂度。注意这个结构,它其实在暗示你:阅卷时不是只看代码本身,设计思想的文字说明也是给分点。
我当年复习的时候有个很直观的感受:408的代码大题很少考特别冷门的算法,基本都是线性表、树、图里的经典操作,而且很多都能在力扣上找到近乎原题的对应。2010年这道轮转数组,在力扣上就是Problem 189,只是力扣把"左移p位"改成了"右移k位",参数方向变了,但解题框架完全一样。所以说它是"母题"一点不为过。
1.2 力扣189的母题地位与考察意图
力扣189题号很靠前,属于"热题100"和各大公司面试高频题里的常客。它考察的核心是三个:第一,是否能识别出轮转操作的本质是元素的整体平移;第二,是否掌握多种复杂度层次的解法;第三,是否具备原地修改数组的能力。恰好这三个能力,也是408数组大题想考察的。
面试官和阅卷老师出这道题,其实都想看到你从"暴力"到"最优"的思考路径。暴力解法人人都能想到,额外数组解法也能写出来,但能写出三次翻转,并且把边界条件处理干净,才说明你真正理解了数组下标变化背后的规律。所以这道题值得反复做,不是因为它难,而是因为它能区分"会写代码"和"理解问题本质"两种层次。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从暴力到最优:四种解法逐层拆解
我在给同学讲解这道题时,习惯按复杂度递进的方式讲。这样有一个好处:即使你在考场上真的忘了最优解,也能迅速退回到暴力或者额外数组的方案,拿到保底分,而不是卡死在原地。
2.1 第一反应:操作一下最坏能过吗
最直白的想法是:每次把最后一个元素拿出来放到开头,整体右移一位,重复k次。写成代码就是两层循环,外层控制移动次数,内层控制整体移动。这个思路在力扣上如果k比较小,比如k=3,n=7,看着还挺快,但一旦k接近n,甚至在O(n)的输入规模下,复杂度直接变成O(kn)。
我在力扣实测过,输入数组长度10万、k取5万时,这个暴力解法几乎卡到超时。但它最大的价值不是效率,而是"一定能写对"。408考场上如果实在想不出别的方案,写一个O(kn)的暴力,同时老老实实说明"时间上不够高效",也能拿到一部分分数,比空白卷强太多。不过,作为目标120+的考生,这道题建议还是把最优解刻在脑子里。
2.2 额外数组:最容易写对的"标准答案"
第二种思路非常符合直觉:新开一个等长数组,把原数组第i个元素放到新数组的(i+k) mod n位置,最后拷贝回去。时间O(n),空间O(n)。这个解法为什么好写?因为它把"轮转"直接翻译成了数学公式,不容易出错。
有一个小细节值得注意:新数组下标要用(i+k) % n,其中n是数组长度。我当时学的时候,一直理解不了为什么要取模,后来换了角度就通了:轮转一圈回到原点,取模就是处理这个"绕圈"问题的自然工具。如果k=3,n=7,那下标0、1、2、3、4的元素分别对应新下标3、4、5、6、0,正好体现了"右移"。
但额外数组的缺陷也很明显,它不符合408题中"空间方面尽可能高效"的要求。当年这道题的标准答案给的是原地算法,空间O(1)。所以额外数组方案适合作为"理解题意"的过渡,不适合做最终答案。
2.3 三次翻转:时间O(n)空间O(1)的最优解
这是真正的黄金解法。核心就一句话:把整个数组翻转,再把前k个翻转,最后把后n-k个翻转。以[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]。结果正是右移3位的效果。
这个解法在408里同样成立,只是左移时翻转的拆分点不同,稍后在3.2节里我会专门讲左右移的顺序区别。三次翻转的时间复杂度是O(n),因为每次翻转都是线性遍历一段区间,三次加起来还是线性;空间复杂度O(1),因为只用了常数个临时变量。这已经达到"时间空间两方面尽可能高效"的满分标准。
2.4 环状替换:另一种原地算法,但为什么考场不做
力扣题解区还流行一种"环状替换"的思路:从某个起点出发,用一个临时变量保存当前元素,每次把它移动到(i+k)%n的位置,然后继续从被覆盖的位置出发,直到回到起点。这样所有元素都能各就各位,时间O(n),空间O(1)。
这个算法的问题在于:当数组长度n和移动步数k不互质时,你需要从gcd(n,k)个不同的起点分别出发,否则会陷入局部循环,永远覆盖不到所有位置。我在自己实现时,边界条件反复调整了好几次才写对,自认为熟练度已经很高了。但说实话,考场上我不会选它,因为一旦紧张,gcd的计算和起点个数判断容易出错,而且写出来篇幅很长,阅卷老师也不一定有你期望的耐心去仔细辨认每一处细节。
所以我的建议很明确:选择三次翻转。它思路直观、代码短、不容易错,是这道题的考场最优解。
3. 三次翻转法拆开揉碎:原理、边界、代码
光知道"翻三次"口诀是不够的,如果你不能解释"为什么翻三次就能完成轮转",那面试官追问一下还是会露怯。这里我提供一个特别容易理解的代数视角。
3.1 为什么"翻三次"就能完成轮转
假设原数组可以分成两段A和B。右移k位的结果,本质上就是把数组从"AB"这个顺序变成"BA"。我们借助一个性质:对任意一个序列X,整体翻转后得到X',那么再翻转一次就会变回X。所以对AB做整体翻转,得到(B')(A'),这里的B'是B的反向,A'是A的反向。
接下来,对前一段(B')做翻转,B'会还原成B;对后一段(A')做翻转,A'会还原成A。最终得到的是BA。看,这个推导过程不需要记忆任何"诀窍",只要知道"翻转两次还原"这一条性质,整个算法就顺理成章了。理解了这一点,你就再也不会把三个reverse的顺序记反。
3.2 右移与左移:翻转顺序千万别记混
我在力扣上用的是右移k位的三道reverse:
cpp复制class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
if (n <= 1) return;
k %= n;
if (k == 0) return;
reverse(nums.begin(), nums.end());
reverse(nums.begin(), nums.begin() + k);
reverse(nums.begin() + k, nums.end());
}
};
但是408考的是左移p位,分段方式就要反过来。如果右移是"先整体翻,再翻前k和后n-k",那左移就是"先翻前p个、再翻后n-p个、最后整体翻",或者等价地,右移k位等价于左移n-k位。
我建议你在记忆时用一个具体例子:R={1,2,3,4,5},p=2,左移后应该是{3,4,5,1,2}。你可以分别试验三种翻转顺序,亲手找出唯一能得到正确结果的顺序。自己走一遍,比死记口诀牢固得多。很多同学考场上把顺序写反,最后结果变成反向轮转,丢分非常可惜。
3.3 边界条件与鲁棒性:k大于n、k等于0、空数组
这是最容易出问题的地方。力扣测试用例里一定会有k大于n的情况,比如n=7、k=10,如果不先取模,第二段reverse的起点begin()+k直接越界,程序崩溃。正确的写法是开头先k %= n,把k规约到[0, n-1]的范围内。
如果k %= n之后恰好等于0,说明数组不需要任何移动,可以直接返回。空数组和长度为1的数组也要做保护,因为对它们执行reverse没有任何意义,但取模时可能出现k%0之类的除零异常。408的题目描述里已经给了0 < p < n的限制,所以相对简单一些,但力扣和实际面试中不会有这么温柔的条件。养成写防御式代码的习惯,不是浪费时间,而是在帮自己避免低级扣分。
4. 按408考场标准写出满分答案
很多同学刷力扣时写得飞快,但到了408考场却不知道该怎么组织答案。原因在于,力扣只要一个函数通过判题就完事,408要求你写设计思想、写注释、算复杂度,是一种"答题范式"的要求,比OJ提交更严格。下面我按考场的真实要求来拆解。
4.1 考场答题三步法:思想、代码、复杂度
第一步,用两三句话写清楚设计思想。比如:"该算法的核心在于利用逆序操作。将数组分成前p个元素和后n-p个元素两部分,分别对它们进行逆序,再对整体逆序,即可实现循环左移。"
第二步,给出完整代码。注意用中文注释写出关键逻辑,变量名尽量有语义。408明确说明可以用C、C++或Java描述,我建议用C语言,因为它最贴近数据结构教材的风格,函数参数传递也符合阅卷老师的阅读习惯。
第三步,用清晰的语言说明复杂度:"每一轮逆序的时间复杂度为O(n),三轮逆序总共O(n),辅助空间为常数个变量,因此空间复杂度为O(1)。"这三步千万不能漏,因为分值是按这三步分配的,缺哪个环节就丢哪部分分。
4.2 一份可以直接抄的C语言答案
c复制#include <stdio.h>
// 将数组R中下标从l到r的元素逆序
void Reverse(int R[], int l, int r) {
int i, j, temp;
for (i = l, j = r; i < j; i++, j--) {
temp = R[i];
R[i] = R[j];
R[j] = temp;
}
}
// 将数组R中长度为n的序列循环左移p位
void Converse(int R[], int n, int p) {
if (p <= 0 || p >= n) return;
Reverse(R, 0, p - 1); // 逆序前p个元素
Reverse(R, p, n - 1); // 逆序后n-p个元素
Reverse(R, 0, n - 1); // 整体逆序
}
这段代码可以直接作为408大题的答案。注意我在函数开头加了参数合法性检查,这既是防御式编程,也向阅卷老师传递出一个信号:你考虑问题足够细致,是真心理解了这个操作。
4.3 时间复杂度和空间复杂度如何规范描述
408阅卷对复杂度的表述有约定俗成的写法。建议写成:"时间复杂度方面,三次调用Reverse分别会处理长度为p、n-p和n的子区间,总处理次数为p+(n-p)+n=2n,故时间复杂度为O(n)。空间复杂度方面,Reverse中仅使用了固定的几个辅助变量,不随数组长度变化,故空间复杂度为O(1)。"
不要只写O(n)和O(1)就结束,最好把为什么是这个量级简单推导一下。这样写能展示出你对算法的理解深度,也更容易拿满复杂度分析那部分的分数。如果时间控制得当,还可以在复杂度描述后补一句"该算法在时间和空间上均已达到最优,因为任何处理每个元素至少一次的算法,时间都不可能低于O(n),而原地操作的辅助空间必然为O(1)"。这句话属于锦上添花,用来拔高印象分。
5. 踩坑记录与判题系统实测体会
这一部分聊点实战经验。我自己在力扣上提交这道题的次数应该不下二十次,也帮朋友排查过各种稀奇古怪的报错。这里挑几个最典型的坑,给你们提前打个预防针。
5.1 常见的5个坑
第一个坑是取模放错位置。k %= n必须放在使用k之前,而不是放在reverse函数内部。有人会把取模写在reverse的参数里,导致第二段reverse的起点计算混乱,正确率很低。
第二个坑是reverse区间边界写错。C++里reverse是左闭右开区间,也就是它反转begin()到end()之间的元素,但不包括end()本身。所以整体翻转后,翻转前k个元素要写reverse(begin(), begin() + k),翻转后n-k个要写reverse(begin() + k, end())。如果你从Python转过来,特别容易把右端点写成闭区间,就会多翻一个元素出来。
第三个坑是左右移翻找规律时思路混乱。我在3.2节提到过,左移和右移的翻转顺序是相反的。不少同学在纸上推演一遍觉得懂了,但过两天又忘记,到了考场临时推导又紧张,最后就写错。建议你把左移版的408答法作为默认记忆,因为它更贴近课本。
第四个坑是没有处理k等于0或者k是n的整数倍的情况。力扣是会测这种用例的,如果你在取模之后没有提前返回,后面的reverse虽然不会报错,但结果确实是错的,因为数组根本不需要移动。
第五个坑我觉得最隐蔽:有人会在循环左移的推导里,把右移的代码原封不动搬过去,只改了函数名。这在n和k特定组合下可能碰巧对,比如k等于p时左移p位和右移k位的结果并不相同,除非移动量正好是n的一半。所以绝不能只改个名字就交差。
5.2 从力扣到408:我建议的刷题路线
从这道题延伸出去,我建议正在准备408的同学把握好一个原则:不要把精力全放在力扣的中等偏难题目上,优先把数组、链表、栈队列、树的基础题刷透。2010年这道题,本质上也就是数组一章里的一段基础算法,难度不大,但它把"设计思想+代码+复杂度"三位一体的答题格式体现得淋漓尽致。
如果你刷力扣,可以按顺序把热题100里数组类题目过一遍,像189轮转数组、283移动零、448找到所有数组中消失的数字、56合并区间,这些题都很有代表性。刷题时不要只看题解,要落笔写,最好能用纸笔模拟完整答题过程。我身边有同学,力扣上200题刷了两遍,结果模拟考408时代码题写得磕磕绊绊,就是因为从没练过手写代码和文字表达。这个教训很深刻。
最后再分享一个实用小技巧
我在反复写这道题的过程中,总结出一个小小的检查办法:无论你用的是C、C++还是Python,写完代码后都取一个具体的例子,比如n=5、k=2或者n=6、k=4,手动在纸上跑一遍三个reverse的调用过程,标记每一轮每个位置上的元素变成了什么。只要这个过程走通了,代码基本就不会出错。这个方法不只在本题适用,几乎所有的数组操作题都能用它来快速验证逻辑,比空想边界条件实用得多。
如果后续想继续深挖,还可以做做它的变体:轮转字符串、判断轮转后的数组是否相等、在轮转过的有序数组里搜索目标值。这几个题目其实都是同一个"轮转"概念在不同数据结构上的变形,吃透一道,就能连带收获好几道。我自己是在把189研究透之后,才真正体会到"做一道题,会一类题"的滋味,希望你也能有这个体验。
