双指针算法实战:移动零与复写零详解
基础打牢后,实战演练必不可少。今天用两道经典题目巩固双指针思想,重点在于原地操作和边界处理。
一、移动零
题目描述
给定一个数组 nums,将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。要求必须在原数组上操作,尽量减少总操作次数。
思路解析
这道题的核心是快慢指针。我们需要遍历数组,将非零元素依次移到前面,最后将剩余位置填充为 0。
具体做法如下:
- 定义两个指针,
current用于遍历整个数组,write指向下一个非零元素应该放置的位置。 - 当
current遇到非零元素时,将其与write位置的元素交换(如果两者不重合),然后write向前移动。 - 无论是否交换,
current都继续向后移动。 - 遍历结束后,
write之后的所有位置自然就是 0(或者在交换过程中已经归位)。
注意:这里不需要显式地填充 0,因为交换逻辑保证了非零元素前移,剩下的位置逻辑上即为空,但在实际代码中,为了严谨,通常会在遍历完非零元素后,将 write 到末尾的部分置为 0,或者利用交换特性直接完成。下面的代码采用交换法,逻辑更直观。
代码实现
void Swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void moveZeroes(int* nums, int numsSize) {
int write = 0;
for (int current = 0; current < numsSize; current++) {
if (nums[current] != 0) {
// 只有当两个指针不重合时才需要交换,避免不必要的操作
if (write != current) {
Swap(&nums[write], &nums[current]);
}
write++;
}
}
}
这段代码提交后能顺利通过,关键在于理解 write 指针始终指向当前待填充的非零位置。
二、复写零
题目描述
在一个固定的整数数组中,将每个出现的 0 复制一遍,并将后续元素向右移动。数组长度固定,超出部分将被截断。
思路解析
如果从前往后遍历,遇到 0 就插入,会覆盖掉后面的元素,导致数据丢失。因此,必须从后往前进行复写。
步骤如下:
- 统计扩展后的长度:先遍历一次数组,计算如果所有
0都被复写,最终需要的索引位置dest。这决定了我们从哪里开始写入。 - 处理边界情况:如果最后一个被复写的元素超出了数组范围(即原数组末尾是
0且会被截断),需要特殊处理,将最后一个元素设为0。 - 从后向前写入:根据第一步确定的
dest和当前的cur,从数组末尾开始向前复制。如果是0,则写入两次;否则写入一次。
代码实现
void duplicateZeros(int* arr, int arrSize) {
int cur = 0;
int dest = -1;
// 第一步:找出最后一个元素在原数组中的位置
while (cur < arrSize) {
if (arr[cur]) {
dest++;
} else {
dest += 2;
}
if (dest >= arrSize - 1) break;
cur++;
}
// 第二步:判断边界情况,如果最后一个元素是 0 且刚好溢出
if (dest == arrSize) {
arr[arrSize - 1] = 0;
cur--;
dest -= 2;
}
// 第三步:从后向前实现复写操作
while (cur >= 0) {
if (arr[cur] == 0) {
arr[dest--] = 0;
arr[dest--] = 0;
} else {
arr[dest--] = arr[cur];
}
cur--;
}
}
这个方案避免了频繁的数据移动,时间复杂度为 O(n),空间复杂度为 O(1),是处理此类问题的标准解法。


