这道题目要求我们将数组中的所有0元素移动到末尾,同时保持非零元素的原始相对顺序。看似简单的要求背后,隐藏着几个关键约束:
很多同学的第一反应可能是使用类似冒泡排序的方法,遇到0就与后面的元素交换,逐步将0"冒泡"到数组末尾。这种方法虽然可行,但存在明显缺陷:
双指针算法之所以适合解决这类问题,是因为它能够:
我们定义两个指针:
dest:指向已处理部分的最后一个非零元素cur:当前遍历位置的指针初始化时:
cpp复制int dest = -1; // 表示尚未处理任何元素
int cur = 0; // 从第一个元素开始遍历
这种初始化方式清晰地表达了"尚未处理任何元素"的状态。当然,也可以将dest初始化为0,表示已处理非零区间的长度为0,两种方式本质相同。
算法执行过程可以分为以下几个步骤:
cur从0开始逐个检查数组元素nums[cur] != 0时:
dest先自增,扩展非零区间nums[dest]和nums[cur]的值cur自增继续遍历nums[cur] == 0时:
cur自增以数组[0,1,0,3,12]为例:
初始状态:
code复制dest = -1, cur = 0
数组:[0,1,0,3,12]
第1步:cur=0,元素为0
code复制dest = -1, cur = 1
第2步:cur=1,元素为1
code复制数组:[1,0,0,3,12]
dest = 0, cur = 2
第3步:cur=2,元素为0
code复制dest = 0, cur = 3
第4步:cur=3,元素为3
code复制数组:[1,3,0,0,12]
dest = 1, cur = 4
第5步:cur=4,元素为12
code复制最终数组:[1,3,12,0,0]
cpp复制void moveZeroes(vector<int>& nums) {
int dest = -1;
for (int cur = 0; cur < nums.size(); ++cur) {
if (nums[cur] != 0) {
++dest;
swap(nums[dest], nums[cur]);
}
}
}
虽然上述代码已经足够高效,但我们还可以做一些微优化:
dest == cur时,交换是多余的优化后的代码:
cpp复制void moveZeroes(vector<int>& nums) {
int dest = -1;
for (int cur = 0; cur < nums.size(); ++cur) {
if (nums[cur] != 0) {
++dest;
if (dest != cur) { // 避免自交换
swap(nums[dest], nums[cur]);
}
}
}
}
在实际编码中,我们需要考虑以下特殊情况:
幸运的是,我们的算法对这些边界情况都能正确处理。
指针越界:
dest自增前未检查是否超出数组范围顺序混乱:
dest,导致非零元素顺序错乱效率问题:
打印中间状态:
cpp复制cout << "cur=" << cur << " dest=" << dest << " array: ";
for (int num : nums) cout << num << " ";
cout << endl;
使用小型测试用例:
边界值测试:
移除指定元素:
去重问题:
颜色分类:
如果题目要求变为:
这些变种都可以通过调整双指针的逻辑来实现,核心思想保持不变。
这种数组分区技术在实际开发中有广泛应用:
内存管理:
数据处理:
系统编程:
在实际编码实践中,我总结了以下几点经验:
指针初始化的选择:
dest = -1比dest = 0更能直观表达"尚未处理"的概念交换操作的优化:
if(dest != cur)判断在大多数情况下并不能显著提升性能代码可读性:
测试的重要性:
这个算法虽然简单,但体现了分治思想在数组操作中的巧妙应用。掌握这类基础算法后,面对更复杂的问题时,往往能够通过组合和变通找到解决方案。