LeetCode 283 - 移动零
问题描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意,必须在不复制数组的情况下原地对数组进行操作。
示例
示例 1:
1 | 输入: nums = [0,1,0,3,12] |
示例 2:
1 | 输入: nums = [0] |
约束条件
1 <= nums.length <= 10^4-2^31 <= nums[i] <= 2^31 - 1
进阶要求
你能尽量减少完成的操作次数吗?
解题思路
这道题的核心是双指针技巧,通过维护两个指针来实现原地操作:
核心思想
使用两个指针:
- 慢指针(slow):指向下一个非零元素应该放置的位置
- 快指针(fast):遍历整个数组,寻找非零元素
算法步骤
- 初始化:
slow = 0,fast = 0 - 遍历数组:使用快指针遍历整个数组
- 移动非零元素:当遇到非零元素时,将其移动到慢指针位置
- 填充零:遍历完成后,将剩余位置填充为零
可视化过程
以 nums = [0,1,0,3,12] 为例:
1 | 初始状态: [0,1,0,3,12] |
实现细节
关键点分析
- 原地操作:通过双指针避免使用额外空间
- 保持顺序:慢指针确保非零元素按原顺序排列
- 效率优化:只需要一次遍历,时间复杂度为 O(n)
边界情况处理
- 数组长度为 1:直接返回
- 数组全为零:慢指针保持为 0
- 数组全为非零:慢指针等于数组长度
代码实现
方法一:双指针(推荐)
1 | public void moveZeroes(int[] nums) { |
方法二:优化版本(减少操作次数)
1 | public void moveZeroes(int[] nums) { |
方法三:交换版本
1 | public void moveZeroes(int[] nums) { |
方法比较
| 方面 | 方法一(双指针) | 方法二(优化版) | 方法三(交换版) |
|---|---|---|---|
| 时间复杂度 | O(n) | O(n) | O(n) |
| 空间复杂度 | O(1) | O(1) | O(1) |
| 操作次数 | 2n | 最少 | n |
| 优点 | 逻辑清晰 | 操作最少 | 一次遍历 |
| 缺点 | 需要两次遍历 | 逻辑稍复杂 | 可能有不必要交换 |
| 推荐度 | ★★★★☆ | ★★★★★ | ★★★☆☆ |
复杂度分析
时间复杂度
- 方法一:$O(n)$ - 需要两次遍历数组
- 方法二:$O(n)$ - 只需要一次遍历
- 方法三:$O(n)$ - 只需要一次遍历
空间复杂度
所有方法的空间复杂度都是 $O(1)$,因为我们只使用了常数额外空间。
操作次数分析
对于数组 [0,1,0,3,12]:
- 方法一:5次赋值 + 2次填充 = 7次操作
- 方法二:3次赋值 + 2次置零 = 5次操作
- 方法三:3次交换 = 3次操作
关键收获
核心技巧
- 双指针模式:一个指针遍历,一个指针维护结果位置
- 原地操作:通过覆盖和填充实现原地修改
- 保持顺序:慢指针确保元素相对顺序不变
常见陷阱
- 忘记填充零:第一遍遍历后需要将剩余位置置零
- 边界条件:处理空数组或单元素数组
- 不必要的操作:避免在
slow == fast时进行无意义的赋值
相关问题
实际应用
- 数组整理:清理数据中的无效值
- 内存优化:压缩稀疏数组
- 数据预处理:为后续算法准备数据
这道题是双指针技巧的经典应用,通过巧妙的指针操作实现了高效的原地数组修改,是面试中的高频题目。