说实话,看到4月1日阿里春招开发岗第三题的时候,我第一反应是愣了一秒,第二反应是差点笑出声。倒不是题目简单到离谱,而是它稳稳踩中了我前一周刷到吐的那个点:数组原地稳定分区。网上很多人用“中,太中了”来形容这场笔试,真不是夸张——这道题把排序、双指针、稳定性三个高频考点揉在了一起,属于那种“平时复习到的人秒懂,没复习到的人写完才发现自己踩了坑”的典型题目。这篇就当一次完整复盘,把题目还原、推导思路、三种语言实现和在线测试方法都整理出来,给接下来要参加大厂笔试的朋友们做个参考,也给我自己留个记录。
1. 题目复盘:阿里春招第三题到底考了什么
1.1 还原理的题目描述
这道题其实不复杂,但我更建议先把原题意思看透。复述一下:
给定一个长度为 n 的整数数组 nums,要求把所有偶数移动到数组的前半部分,把所有奇数移动到后半部分,同时保持偶数之间、奇数之间的相对顺序不变。实现一个函数完成重排,并说明你的方案在时间复杂度和空间复杂度上的表现。
约束条件大约是:1 ≤ n ≤ 10^6,nums[i] 在 int 范围内。输出要求是重排后的数组。
注意,这里有两个关键词:一个是“相对顺序不变”,另一个是 n 最大到 10^6。前者决定了稳定性要求,后者决定了 O(n²) 级别的暴力写法大概率过不了全量数据。很多同学栽就栽在这两个词上。
1.2 题面里的三个隐藏考点
这道题名义上考的是数组操作,但实际上至少藏了三个考点,缺一个都容易出问题。
第一个是稳定性。题面里明确写了“保持偶数之间、奇数之间的相对顺序不变”,这意味着你不能无脑用左右双指针交换奇偶元素,因为交换会打乱相对顺序。我当时在旁边就看到有人写了个 while(left < right) 交换的版本,跑用例的时候直接短路了。这不是代码能力问题,是读题习惯问题。
第二个是复杂度意识。n 最大 10^6,意味着你的解法最多只能接受 O(n log n) 级别,最好是 O(n)。如果上来写一个两层循环或者每次插入都搬移元素,在最坏情况下会严重超时,笔试环境可不会给你仁慈的测试数据。
第三个是边界处理。空数组、单元素数组、全偶数、全奇数、已经是目标顺序的数组,这些都是必须考虑的。很多人在普通用例上跑得好好的,一换到边界用例就崩,这就是没把边界条件当成题面的一部分。
1.3 为什么它是高频“母题”
说句实话,这道题本身没什么特别惊艳的技巧,但它的地位很特殊。它本质上是“数组稳定分区”问题,而数组稳定分区几乎是所有排序算法、双指针算法和荷兰国旗类题目的母题。
快速排序的 partition 操作、按颜色分类(荷兰国旗)、把 0 移动到数组末尾、按奇偶排序数组,本质上都是同一类逻辑。你只要把这道题的稳定分区思路吃透,上面这些题基本就通了。这就是为什么我说“中,太中了”——刷过这类母题的人,考场上看到它就像看到老朋友,而没刷过的人可能连从哪下手都要犹豫半天。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路:从辅助数组到原地稳定分区
2.1 第一反应:辅助数组法,笔试最稳答案
遇到这种要求稳定分区的题,最直接、最容易写对的方案就是用辅助数组。思路简单粗暴:遍历一遍原数组,把所有偶数按顺序收集到一个新数组里,再遍历一遍把所有奇数收进去,最后把结果复制回原数组。
为什么说这是笔试最稳答案?因为它逻辑简单,不容易写错,而且时间 O(n)、空间 O(n),在 n = 10^6 的约束下完全够用。笔试场景里保证正确率比炫技重要得多,千万不要为了追求 O(1) 空间而冒写错的风险。代码会在下一章节详细展开。
诚然,辅助数组法不是空间上的最优解,但是笔试环境里大多数题目没有硬性限制空间 O(1),所以它是我个人推荐的默认解法。先拿到分数,再考虑下一步优化,这才是正确的考试策略。
2.2 双指针交换法:空间省了,但稳定性没了
如果题目没有“保持相对顺序”这个条件,很多人会想到双指针交换:左指针从左往右找奇数,右指针从右往左找偶数,找到就交换。这样一趟下来,偶数都在左边,奇数都在右边,时间 O(n),空间 O(1)。
但问题也正在这里:交换操作会破坏元素原有的相对顺序。举个例子,数组 [2, 1, 4, 3],左指针找到奇数 1,右指针找到偶数 4,交换后变成 [2, 4, 1, 3]。表面上奇偶分离了,但偶数 4 本来排在偶数 2 后面,交换后 4 跑到了 2 前面,偶数内部的相对顺序已经被打乱了。
这正好解释了为什么这道题用双指针交换会掉进陷阱。考场上如果你只盯着“奇偶分离”这个目标,很容易忽略“保持相对顺序”这个约束,写完了还觉得自己没问题。
2.3 进阶思路:如何做到稳定且节省空间
如果面试官追问“空间复杂度能不能压到 O(1)”,你可以考虑一种类似归并排序的分治思路:把数组分成两半,分别递归稳定分区,然后合并两个已经分好区的部分,合并时用区间旋转的方式调整位置,整个过程不借助额外数组。
这种方法的复杂度是时间 O(n log n)、空间 O(1),在面试中属于加分项,笔试里一般用不上。实现细节比较繁琐,但值得理解核心思想:稳定分区可以像归并一样,先局部有序再整体聚合。能把这个思路讲清楚,面试官会高看你一眼。
2.4 复杂度方案速查表
为了让你一眼看清不同方案的取舍,我整理了一张对比表:
| 方案 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 辅助数组法 | O(n) | O(n) | 稳定 | 笔试默认方案,推荐优先写 |
| 双指针交换法 | O(n) | O(1) | 不稳定 | 题目不要求相对顺序时 |
| 原地平移法 | O(n²) | O(1) | 稳定 | 空间要求严格但数据量小 |
| 分治稳定分区 | O(n log n) | O(1) | 稳定 | 面试追问优化方案时展示 |
你可以看到,没有一个方案是既稳定又 O(n) 时间还 O(1) 空间的,这在数学上是有定论的。所以笔试时要先想清楚题目到底卡哪些资源,再做取舍。这道题里空间没有限制,辅助数组法就是最优选择。
3. Java、C++、Python 三种语言实现与逐行解析
3.1 Java 版实现:辅助数组法 + 原地平移法
Java 版我推荐先写辅助数组法,代码清晰,可读性好:
java复制public int[] stablePartition(int[] nums) {
int n = nums.length;
int[] res = new int[n];
int idx = 0;
// 第一趟:收集所有偶数,保持原顺序
for (int x : nums) {
if ((x & 1) == 0) {
res[idx++] = x;
}
}
// 第二趟:收集所有奇数,保持原顺序
for (int x : nums) {
if ((x & 1) == 1) {
res[idx++] = x;
}
}
return res;
}
这里用 (x & 1) == 0 判断偶数,比 x % 2 == 0 更高效,因为位运算直接操作二进制,不需要走除法流程。笔试中如果追求极致性能,这种小细节可以积累起来。
如果题目要求不能返回新数组,而是原地修改 nums,可以把最后一步改为把 res 的元素复制回 nums。这也不难,多一个 System.arraycopy(res, 0, nums, 0, n) 就行。
另外再给你一个原地稳定版本的 Java 写法思路:用一个 evenIdx 记录下一个偶数应该放的位置,每遇到一个偶数就把这段区间整体右移一位,再把当前偶数放到 evenIdx 上。这种写法稳定且空间 O(1),但最坏是 O(n²),n 大了不建议用。代码我就不贴了,笔试真遇到需要 O(1) 空间的情况,优先选择跟面试官沟通约束,而不是硬写低效版本。
3.2 C++ 版实现:vector 操作与指针细节
C++ 版直接用 std::vector 操作最方便。辅助数组版如下:
cpp复制vector<int> stablePartition(vector<int>& nums) {
vector<int> res;
res.reserve(nums.size()); // 提前分配,避免多次扩容
for (int x : nums) {
if ((x & 1) == 0) {
res.push_back(x);
}
}
for (int x : nums) {
if ((x & 1) == 1) {
res.push_back(x);
}
}
return res;
}
这里有一个容易被忽略的细节:res.reserve(nums.size()) 提前分配好容量,可以避免 push_back 过程中反复扩容导致的不必要拷贝。数组规模到 10^6 量级时,多次扩容带来的性能损耗是实打实的,笔试跑大数据量时能明显感觉到区别。
如果你想省空间,也可以用双指针交换版,但一定要确保题目不要求稳定性:
cpp复制vector<int> partitionInPlace(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
while (left < right && (nums[left] & 1) == 0) left++;
while (left < right && (nums[right] & 1) == 1) right--;
if (left < right) {
swap(nums[left], nums[right]);
left++;
right--;
}
}
return nums;
}
用迭代器也能实现同样的逻辑,但我个人觉得笔试用下标最稳,因为迭代器处理边界条件时心智负担更大。C++ 的指针和数组名本质上是连续内存的抽象,nums.data() 可以拿到裸指针,面试时被问到底层原理也能从容应对。
3.3 Python 版实现:列表推导式与显式循环
Python 的代码可以写得非常简洁。最直接的一种是列表推导式:
python复制def stable_partition(nums):
return [x for x in nums if x % 2 == 0] + [x for x in nums if x % 2 == 1]
两行搞定,可读性极强。这里用 x % 2 == 0 判断偶数,是因为 Python 中 & 的优先级比 == 低,写 x & 1 == 0 在 Python 里会变成 x & (1 == 0),结果完全不是你想的那样,这是个非常容易踩的坑。
如果你更习惯显式循环,也可以这样写:
python复制def stable_partition(nums):
res = []
for x in nums:
if x % 2 == 0:
res.append(x)
for x in nums:
if x % 2 == 1:
res.append(x)
return res
这个版本和 Java、C++ 的思路完全一致,只是换成了 Python 的语法。两种写法都可以,笔试时选自己最熟练的。
不过要提醒一点:Python 原生的切片操作虽然方便,但在这个场景里没有直接用列表推导式来得直观。不要为了炫技用复杂的切片,笔试时简单、正确、能跑才是王道。
3.4 三套代码对比与选型建议
| 语言 | 推荐版本 | 核心优势 | 注意事项 |
|---|---|---|---|
| Java | 辅助数组法 | 类型安全、可读性好 | 注意 System.arraycopy 的用法 |
| C++ | 辅助数组法 + reserve | 性能最好 | 记得提前 reserve 避免扩容开销 |
| Python | 列表推导式 | 代码最短、Pythonic | 不要用 & 判断奇数,用 % |
从笔试角度,我建议按你熟悉程度选择,不必三套都背。但至少要理解一种语言的完整写法,最好能徒手写出辅助数组法,因为它能保证稳定,而且不容易出错。等你有余力了,再研究双指针和分治的变体,用来应对面试追问。
4. 在线测试与自测方法
4.1 去哪里在线测试
这道题的经典变形在很多在线判题平台上都有。最接近的场景是“把 0 移动到数组末尾,同时保持非零元素相对顺序不变”,在 LeetCode 上搜索“移动零”能直接找到可提交的题目;另外“按奇偶排序数组”系列题也包含类似的场景,不过要注意有的版本不要求稳定,提交前先看清题面。
牛客网也有不少数组类训练题,搜索“奇偶”“分区”等关键词能找到合适的练习环境。实际笔试一般在牛客或赛码这类系统上跑,提前熟悉它们的输入输出模板很有必要。注意笔试系统很多要求你把代码写在指定的类里,不要自己加 main 方法随便输出,不然编译不过会白白丢分。
4.2 边界用例构造清单
在线测试时建议把下面这些用例都跑一遍,确认代码没漏判:
- 空数组
[],应该返回空数组 - 单元素数组
[1]和[2],原样返回 - 全偶数数组
[2, 4, 6],顺序不变 - 全奇数数组
[1, 3, 5],顺序不变 - 已经排好的数组
[2, 4, 1, 3],结果不变 - 完全逆序的数组
[3, 1, 4, 2],要变成[4, 2, 3, 1] - 有负数的情况
[-1, 2, -3, 4],负数按奇偶判断,-1 是奇数
这批用例跑完,你的实现基本就稳了。很多笔试不是载在最复杂的用例上,而是载在空数组这种不起眼的边界上,千万注意。
4.3 笔试现场的时间分配技巧
笔试和平时刷题不太一样,时间有限,还有别的题要做。我自己的策略是:先花一分钟通读全部题目,评估难度,把最稳的题先做掉。像这道题,如果一眼扫到“保持相对顺序”,马上就可以确定用辅助数组法,五分钟内写完、跑通、提交,不纠结优化。
如果你做到一半发现时间不够,也不要慌,可以先提交一个能过部分数据的暴力解法,至少能拿部分分。笔试系统很多都是按通过的测试点给分,不是非黑即白,先保底再说。这个策略听起来简单,但考场上一紧张就容易忘。
5. 复盘与避坑:这些坑我踩过
5.1 稳定性是最大的考点
这次笔试最典型的一个坑就是把“保持相对顺序”当摆设。我注意到凡是用了双指针交换且没有额外判断的实现,在部分用例上会错得很隐蔽——不是完全错误,而是有一两个元素的顺序不对,容易让人误以为是自己输出格式问题,白花时间排查。
正确的姿势是:把稳定性当作题目的硬性约束,就像你不会去修改题目要求的输出格式一样。读题时可以用笔圈出“相对顺序”等字眼,看到“保持顺序”直接锁定稳定解法,看到“不要求顺序”再考虑双指针。
5.2 边界条件容易丢分
空数组和单元素数组是我的血泪教训。很多次我自信满满地提交代码,结果挂在空数组上。原因很简单:笔试时心理紧张,脑子里全是主流程逻辑,第一遍写出来的代码往往没处理空列表。建议写完主逻辑之后,马上补上 if (nums == null || nums.length <= 1) return; 这种保护语句。这条习惯能帮你减少很多不必要的失分。
5.3 面试官追问怎么接
笔试通过之后还有面试环节,面试官很可能拿这道题延伸追问。最容易被问到的是:你说辅助数组法是 O(n) 空间,那能不能做到 O(1) 空间且保持稳定?这时候你可以先说明工程角度推荐辅助数组法,再补充分治稳定分区的思路。就算不写出完整代码,能把思路讲清楚,面试官也会认为你对复杂度有深刻理解。
另一个常见追问是让你比较双指针交换和辅助数组法的优劣。可以从稳定性、空间占用、代码简洁性三个维度回答,这是送分题,别浪费。
5.4 同类变形题清单
如果你想把这一类题型彻底吃透,我建议按下面这个清单刷一遍:
- 移动零:把 0 移到末尾,保持非零元素相对顺序
- 按奇偶排序数组:非稳定版本和稳定版本都做一遍
- 颜色分类(荷兰国旗):三色分区问题
- 快速排序中的 partition:理解不稳定分区的经典应用
- 归并排序中的 merge:理解稳定合并的思想
这些题目练完,你会发现它们背后的分区逻辑是相通的,考场上再遇到类似的题,自然就能做到“太中了”。
结尾想再多说一句
最后分享一个我自己笔试后的体会:备考阶段的“母题”意识真的很重要。很多人刷题是一道一道刷,刷完就忘;我更建议按“知识点家族”来刷——把一个母题的所有变体集中练透,总结每个变体之间条件不同会导致解法如何变化。像这次第三题,就是典型的稳定分区母题,只要平时把这类题吃透,考场上看到“保持相对顺序”这几个字,你的脑子就会自动弹出辅助数组的解法,整个人都踏实了。希望这篇复盘能帮你少走弯路,笔试卷子上少丢一分是一分。
