LeetCode283-移动零
题目
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
- 输入:
nums = [0,1,0,3,12] - 输出:
[1,3,12,0,0]
示例 2:
- 输入:
nums = [0] - 输出:
[0]
提示:
1 <= nums.length <= 104-231 <= nums[i] <= 231 - 1
进阶:你能尽量减少完成的操作次数吗?
分析
问题描述
给定一个数组 nums,你需要编写一个函数,将数组中的所有 0 元素移动到数组的末尾。这个过程必须满足两个条件:
- 非零元素的相对顺序必须保持不变。
- 必须在原数组上直接进行修改(原地操作),不能创建新的数组副本。
解题思路分析
这道题的核心在于如何在移动 0 的同时,不打乱其他非零元素的原始顺序。一个直观但错误的想法是遍历数组,每当遇到一个 0,就将它与数组末尾的元素交换。这种方法的问题在于,无法保证非零元素的相对顺序。例如,[0, 1, 0, 3, 12],如果将第一个 0 与 12 交换,数组会变成 [12, 1, 0, 3, 0],此时 1 和 12 的相对顺序就被改变了。
因此,我们需要一种更巧妙的方法。一个非常高效且经典的的思路是 双指针法。
双指针法
我们可以把这个问题看作是将所有非零元素“向前移动”或者说“紧凑排列”,而将 0 “留在后面”。
想象一下,我们使用两个指针,一个 慢指针(slow) 和一个 快指针(fast)。
slow指针:它的作用是指向下一个应该被非零元素填充的位置。换句话说,slow左边的所有元素(不包括slow自身)都应该是处理好的、按原始顺序排列的非零元素。fast指针:它的作用是向前遍历整个数组,寻找非零元素。
算法流程如下:
初始化
slow和fast两个指针,都指向数组的起始位置,即索引0。fast指针开始向后遍历数组(for循环或者while循环)。在遍历过程中:
- 如果
fast指针指向的元素nums[fast]不是0:- 这意味着我们找到了一个需要保留顺序的非零元素。
- 我们将这个非零元素
nums[fast]赋值给slow指针指向的位置nums[slow]。 - 然后,将
slow指针向后移动一位(slow++),为下一个非零元素做准备。
- 如果
fast指针指向的元素nums[fast]是0:- 我们什么也不做,
slow指针保持不动。fast指针继续向后移动,去寻找下一个非零元素。
- 我们什么也不做,
- 如果
当
fast指针遍历完整个数组后,所有非零元素都已经被slow指针按照原始相对顺序,紧凑地移动到了数组的前部。此时slow指针所在的位置,以及它之后的所有位置,都应该被填充为0。最后,我们从
slow指针当前的位置开始,到数组的末尾,将所有元素都赋值为0。
举例说明:nums = [0, 1, 0, 3, 12]
初始状态:
slow = 0,fast = 0nums->[0, 1, 0, 3, 12]
fast= 0:nums[0]是0。slow不动。fast继续。slow = 0,fast = 1nums->[0, 1, 0, 3, 12]
fast= 1:nums[1]是1(非零)。- 将
nums[fast](1) 赋值给nums[slow](nums[0])。数组变为[1, 1, 0, 3, 12]。 slow前进,slow变为1。slow = 1,fast = 2nums->[1, 1, 0, 3, 12]
- 将
fast= 2:nums[2]是0。slow不动。fast继续。slow = 1,fast = 3nums->[1, 1, 0, 3, 12]
fast= 3:nums[3]是3(非零)。- 将
nums[fast](3) 赋值给nums[slow](nums[1])。数组变为[1, 3, 0, 3, 12]。 slow前进,slow变为2。slow = 2,fast = 4nums->[1, 3, 0, 3, 12]
- 将
fast= 4:nums[4]是12(非零)。- 将
nums[fast](12) 赋值给nums[slow](nums[2])。数组变为[1, 3, 12, 3, 12]。 slow前进,slow变为3。slow = 3,fast = 5nums->[1, 3, 12, 3, 12]
- 将
fast遍历结束。此时slow的值为3。这表示数组的前3个元素 ([0..2]) 已经是非零元素了。填充
0: 从索引slow(3) 开始,到数组末尾,全部填充为0。nums[3] = 0nums[4] = 0
最终结果:
nums->[1, 3, 12, 0, 0]
这种方法只遍历了数组一次,并且是在原地操作,完全符合题目要求。
进阶思考:减少操作次数
上面的双指针法已经非常高效了,时间复杂度是 O(n),空间复杂度是 O(1)。但我们还可以稍微优化一下赋值操作。
在上面的流程中,当非零元素本身就在它 “应该” 在的位置时(即 slow 和 fast 指针重合时),我们还是执行了 nums[slow] = nums[fast] 这样的赋值操作,这其实是多余的。比如数组 [1, 2, 3, 0, 4],前三次遍历 slow 和 fast 始终相等,但我们还是执行了 nums[0]=1, nums[1]=2, nums[2]=3。
我们可以通过 交换 的方式来避免这种冗余操作,但这会稍微改变逻辑。
一个更直接的优化是,只有当 slow 和 fast 不在同一位置时,才进行赋值操作,并在赋值后将原 nums[fast] 位置置为0。这种思路会稍微复杂一些,但最经典的双指针解法(第一种)已经足够优秀,并且逻辑清晰,易于理解和实现,通常面试中给出第一种解法即可。我们主要还是采用第一种思路,因为它在逻辑上更像是一种“覆盖”而非“交换”,能更好地保证非零元素的相对顺序。
下面我们给出基于第一种双指针思路的 Java 代码实现。
算法复杂度分析
时间复杂度: $O(n)$
其中 $n$ 是数组nums的长度。快指针fast从头到尾完整地遍历了一次数组。填充0的循环也最多遍历 $n$ 次。总的操作次数与 $n$ 呈线性关系。空间复杂度: $O(1)$
我们只使用了slow和fast两个额外的指针变量,占用的空间是常数级别的。操作是在原数组上进行的,没有使用额外的数组空间。因此空间复杂度为 $O(1)$。
答案
1 | class Solution { |









