题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入:
nums = [0, 1, 0, 3, 12]
输出:[1, 3, 12, 0, 0]
示例 2:
输入:
nums = [0]
输出:[0]
提示:
1 <= nums.length <= 10^4-2^31 <= nums[i] <= 2^31 - 1
双指针(交换法)
使用两个指针,left 和 right,right 指针用于遍历数组,left 指针用于指向当前已经处理好的序列的尾部(即非零序列的末尾,也就是下一个非零元素要放置的位置)。当 right 指针遇到非零元素时,就将其与 left 指针指向的元素交换,然后 left 指针右移。这样,非零元素被逐渐交换到前面,而 0 被交换到后面。
注意:交换后,left 指向的位置可能是 0(如果之前 left 指向的是 0)或者非零(如果 left 和 right 相同,即自己交换自己,此时不会改变),但无论如何,left 指针的左侧都是非零元素,并且保持了原有顺序。
1 | public void moveZeroes(int[] nums) { |
执行过程(以 [0, 1, 0, 3, 12] 为例)[0, 1, 0, 3, 12] - right=0: 0 → 跳过[1, 0, 0, 3, 12] - right=1: 1 → 与 left(0) 交换[1, 0, 0, 3, 12] - right=2: 0 → 跳过[1, 3, 0, 0, 12] - right=3: 3 → 与 left(1) 交换[1, 3, 12, 0, 0] - right=4: 12 → 与 left(2) 交换
复杂度分析
- 时间复杂度:
O(n),只需一次遍历数组。 - 空间复杂度:
O(1),原地操作,仅使用常数空间。
双指针(覆盖法)
使用一个指针 cur,表示当前非零元素应该存放的位置。遍历数组,当遇到非零元素时,将其复制到 cur 位置,然后 cur 指针右移。遍历完成后,所有非零元素都被按顺序移动到了数组的前部,然后将 cur 之后的元素全部置为 0。
1 | public void moveZeroes(int[] nums) { |
执行过程(以 [0, 1, 0, 3, 12] 为例)
第一阶段:
i=0: 0→ 跳过i=1: 1→nums[0]=1(cur=1)i=2: 0→ 跳过i=3: 3→nums[1]=3(cur=2)i=4: 12→nums[2]=12(cur=3)
第二阶段:
- 填充
nums[3]=0,nums[4]=0 - 结果:
[1, 3, 12, 0, 0]
复杂度分析
- 时间复杂度:
O(n),两次独立遍历数组。 - 空间复杂度:
O(1),原地操作,仅使用常数空间。
总结
两种方法都有效地解决了移动零的问题:
- 交换法更高效,单次遍历完成操作,代码更简洁
- 覆盖法写操作更少,但需要两次遍历
在实际应用中,交换法通常是更优选择,因为它只需要一次遍历且代码更简洁。理解这两种双指针策略有助于解决类似的数组重排问题,如移除指定元素、删除排序数组中的重复项等。