图 1:本文封面,一趟原地重排的前后对照。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。原题要求写一个函数:把整型数组里的 0 全部移到末尾,其余非 0 整数移到前面且保持原有相对顺序,函数返回结果中第一个 0 的下标。约束很典型——一次遍历、原地操作,考点是「稳定划分」的写指针设计。
先把题意翻译成算法语言。
「把 0 挪到尾部、非 0 保持顺序前移」,本质是按「是否为 0」这个谓词对数组做稳定划分(stable partition):非 0 元素是一个等价类,要保持内部相对顺序;0 是另一个等价类,全部沉底。返回值也不用额外找——划分完成后,非 0 元素恰好占满前 w 个位置,w 就是第一个 0 的下标;如果数组里没有 0,返回数组长度即可。所以一次遍历结束,结果和返回值同时到手,不需要第二趟扫描。
双指针的分工:读指针找非零,写指针收非零。
设写指针 w 指向「下一个非零元素应该安放的位置」,初始为 0;读指针 i 从头到尾扫描。每当 a[i] 非零,就把 a[i] 与 a[w] 交换,然后 w 前进一步。为什么这样就稳定?关键在于写入顺序:非零元素是按被扫描到的先后顺序,依次写到 0、1、2…… 这些连续位置上的,先遇到的先落位,相对顺序天然不变。而 0 不需要被主动搬运——它只是在与非零元素交换时被动让位,自然被挤到后排。循环结束时,w 左边的都是按原序排列的非零元素,w 及其右边全是 0,w 本身就是要返回的下标。时间复杂度 O(n),额外空间 O(1),满足题目全部约束。
用代码跑一遍验证。
下面这段实现附带三组边界用例和一组带标签的稳定性抽查(Node.js 22.15.0,node movezeros.js):
function moveZeros(a) {
let w = 0; // 写指针:下一个非零应放的位置
for (let i = 0; i < a.length; i++) { // 读指针:扫描每个元素
if (a[i] !== 0) {
[a[w], a[i]] = [a[i], a[w]]; // 非零换到前排,0 被动后移
w++;
}
}
return w; // 第一个 0 的下标(无 0 时为 n)
}
const t1 = [3, 0, 1, 0, 12];
const r1 = moveZeros(t1);
console.log('原数组 [3,0,1,0,12] →', t1, '首个0下标:', r1);
const t2 = [0, 0, 0];
console.log('全0数组 [0,0,0] →', t2, '首个0下标:', moveZeros(t2));
const t3 = [1, 2, 3];
console.log('无0数组 [1,2,3] →', t3, '首个0下标:', moveZeros(t3));
// 稳定性抽查:给非零元素贴标签看重排后的顺序
const tagged = [{v:5,id:'a'},{v:0,id:'x'},{v:7,id:'b'},{v:0,id:'y'},{v:9,id:'c'}];
let w = 0;
for (let i = 0; i < tagged.length; i++) {
if (tagged[i].v !== 0) { [tagged[w], tagged[i]] = [tagged[i], tagged[w]]; w++; }
}
console.log('非零标签顺序:', tagged.slice(0, w).map(e => e.id).join(','), '首个0下标:', w);
实测输出:
原数组 [3,0,1,0,12] → [ 3, 1, 12, 0, 0 ] 首个0下标: 3
全0数组 [0,0,0] → [ 0, 0, 0 ] 首个0下标: 0
无0数组 [1,2,3] → [ 1, 2, 3 ] 首个0下标: 3
非零标签顺序: a,b,c 首个0下标: 3
四行输出分别验证:常规用例结果正确且首个 0 在下标 3;全 0 数组返回 0,算法不崩溃;无 0 数组原样不动、返回数组长度;标签抽查确认非零元素 a、b、c 的相对顺序与输入完全一致,稳定性成立。有一个细节值得注意:当 i 等于 w 时(前缀暂时没有遇到 0),交换是自己换自己,无害,不必特判。
图 2:读写指针的推进过程与稳定性的来源。
失分点与答题结构。
常见的三个坑:一是用「从后往前补 0」的思路先压缩再填零,写完才发现返回值还要单独再扫一遍;二是排序或借助新数组,直接违反原地与保持顺序两个约束;三是只写主流程、漏掉全 0 和无 0 两个边界的返回值约定。面试时建议按这个顺序答:先把题意翻译成稳定划分,点明返回值就是划分边界;再给出读写双指针方案并解释稳定性来源(非零按扫描顺序依次落位);然后口述代码,主动补上边界用例;最后给出复杂度。这题的同款模式在工程里很常见——数据清洗管线里把有效样本前移、把占位或无效标记后移,就是同一个原地稳定划分,写对一次处处复用。你刷题时遇到过哪些「划分边界就是答案」的题目?欢迎交流。