【LeetCode 热题 100】No.283—— 移动零
大家好!今天继续更新 LeetCode 热题 100 系列,第四题我们来学习简单题 “移动零”。这道题看似简单,但涉及到 “原地操作” 和 “最少操作次数” 的优化思想,是面试中考察代码细节的常见题目。接下来,我们从题目分析、思路推导到 Java 代码实现,一步步掌握这道题的最优解法。
一、题目描述
首先明确题目要求(基于 LeetCode 官方原题):
- 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
- 请注意,必须在不复制数组的情况下原地对数组进行操作。
- 元素的相对顺序必须保持一致。
- 尽量减少操作次数。
示例
示例 1:输入:nums = [0,1,0,3,12]
输出:[1,3,12,0,0]
示例 2:输入:nums = [0]
输出:[0]
提示
- 1 <= nums.length <= 10⁴
- -2³¹ <= nums[i] <= 2³¹ - 1
二、解题思路分析
这道题的核心要求是 “原地操作” 和 “保持非零元素顺序”,同时要 “减少操作次数”。我们可以从基础思路入手,逐步优化到最优解法。
1. 基础思路:两次遍历(符合所有要求)
核心逻辑
第一次遍历:把所有非零元素 “前移”,用一个指针记录非零元素的位置;第二次遍历:将剩余位置填充为 0。
步骤拆解
- 定义指针
nonZeroIndex
,初始值 0,用于记录下一个非零元素应该存放的位置。 - 第一次遍历数组:
- 若当前元素
nums[i]
非零,则将其放到nums[nonZeroIndex]
的位置,然后nonZeroIndex
加 1。 - 若当前元素是 0,直接跳过。
- 若当前元素
- 第一次遍历结束后,
nonZeroIndex
之前的位置已全部填充非零元素,且保持原顺序。 - 第二次遍历数组:从
nonZeroIndex
开始到数组末尾,将所有位置填充为 0。
优势
- 原地操作,无需额外空间(除了几个指针变量)。
- 非零元素只移动一次,操作次数少。
- 时间复杂度 O (n),两次遍历总次数为 2n,属于线性时间。
2. 常见误区:交换法的注意事项
另一种思路是 “遍历数组时,遇到 0 就与后面的非零元素交换”,但这种方法可能导致非零元素被多次移动(例如[0,0,0,1]
需要交换 3 次),操作次数更多,不符合 “尽量减少操作次数” 的要求,因此不推荐。
三、Java 代码实现(两次遍历法)
java
运行
public class Solution {public void moveZeroes(int[] nums) {if (nums == null || nums.length == 0) {return; // 边界处理:数组为空或长度为0,直接返回}int nonZeroIndex = 0; // 记录下一个非零元素的存放位置// 第一次遍历:将所有非零元素前移for (int i = 0; i < nums.length; i++) {if (nums[i] != 0) {nums[nonZeroIndex] = nums[i];nonZeroIndex++;}}// 第二次遍历:将剩余位置填充为0for (int i = nonZeroIndex; i < nums.length; i++) {nums[i] = 0;}}
}
代码解析
- 边界处理:先判断数组是否为空或长度为 0,直接返回,避免无效操作。
- 非零元素前移:通过
nonZeroIndex
指针跟踪非零元素的位置,遍历过程中只处理非零元素,确保它们按原顺序排列在数组前部。 - 填充零:第一次遍历结束后,
nonZeroIndex
之后的位置原本要么是 0,要么是被前移元素覆盖的旧值,因此直接填充 0 即可完成所有零的移动。
四、优化:一次遍历(减少写操作)
上述两次遍历法已经很好,但可以进一步优化:在一次遍历中同时完成 “前移非零元素” 和 “记录零的位置”,减少对数组的写操作(尤其当零元素较少时)。
public class Solution {public void moveZeroes(int[] nums) {if (nums == null || nums.length == 0) {return;}int zeroIndex = 0; // 记录第一个零元素的位置for (int i = 0; i < nums.length; i++) {if (nums[i] != 0) {// 若当前元素非零,且与零元素位置不同,则交换(避免自我交换)if (i != zeroIndex) {nums[zeroIndex] = nums[i];nums[i] = 0;}zeroIndex++; // 无论是否交换,零元素位置都后移}}}
}
优化点解析
- 用
zeroIndex
跟踪第一个零元素的位置,遍历过程中:- 若遇到非零元素,且当前位置
i
大于zeroIndex
(说明存在零元素),则将非零元素与zeroIndex
位置的零交换,同时zeroIndex
后移。 - 若
i == zeroIndex
(说明还未遇到零元素),则直接zeroIndex
后移,不进行交换(减少无效写操作)。
- 若遇到非零元素,且当前位置
- 优势:当数组中零元素较少时,写操作次数更少(例如
[1,2,3,0,4]
只需 1 次交换)。
五、测试案例验证
测试案例 1:基础示例
输入:[0,1,0,3,12]
两次遍历法处理:
- 第一次遍历后:
[1,3,12,3,12]
(nonZeroIndex=3
)。 - 第二次遍历后:
[1,3,12,0,0]
,正确。
为了更直观的感受到执行过程,我将对执行过程逐步拆解
初始状态
- 数组:
[0, 1, 0, 3, 12]
zeroIndex = 0
(初始指向第一个元素)i
从 0 开始遍历
第 1 步:i=0
- 当前元素
nums[0] = 0
(是零) - 不进入
if (nums[i] != 0)
逻辑 zeroIndex
保持 0 不变- 数组无变化:
[0, 1, 0, 3, 12]
第 2 步:i=1
- 当前元素
nums[1] = 1
(非零) - 判断
i != zeroIndex
?1 != 0
→ 成立- 执行交换:
nums[zeroIndex] = nums[i]
→nums[0] = 1
;nums[i] = 0
→nums[1] = 0
- 执行交换:
- 执行
zeroIndex++
→zeroIndex = 1
- 数组变为:
[1, 0, 0, 3, 12]
第 3 步:i=2
- 当前元素
nums[2] = 0
(是零) - 不进入
if (nums[i] != 0)
逻辑 zeroIndex
保持 1 不变- 数组无变化:
[1, 0, 0, 3, 12]
第 4 步:i=3
- 当前元素
nums[3] = 3
(非零) - 判断
i != zeroIndex
?3 != 1
→ 成立- 执行交换:
nums[1] = 3
;nums[3] = 0
- 执行交换:
- 执行
zeroIndex++
→zeroIndex = 2
- 数组变为:
[1, 3, 0, 0, 12]
第 5 步:i=4
- 当前元素
nums[4] = 12
(非零) - 判断
i != zeroIndex
?4 != 2
→ 成立- 执行交换:
nums[2] = 12
;nums[4] = 0
- 执行交换:
- 执行
zeroIndex++
→zeroIndex = 3
- 数组变为:
[1, 3, 12, 0, 0]
测试案例 2:全为非零元素
输入:[0] 处理:第一次遍历后 nonZeroIndex=0,第二次遍历将从索引 0 到 0 的位置填充为 0,数组保持不变,正确
六、复杂度分析
- 时间复杂度:O (n)。无论是两次遍历还是一次遍历,都只对数组进行线性扫描,总操作次数为 n 级。
- 空间复杂度:O (1)。仅使用常数个额外变量,符合原地操作要求。
七、总结
“移动零” 的解题关键是在原地操作的基础上,减少不必要的元素移动,核心要点如下:
- 两次遍历法是最直观的解法,逻辑清晰,操作次数少,适合面试快速实现。
- 一次遍历的优化版通过交换进一步减少写操作,在零元素较少时更高效,但逻辑稍复杂。
- 避免多次移动同一非零元素,这是减少操作次数的核心原则。
这道题虽然简单,但能体现对代码细节的把控能力,尤其是 “最少操作次数” 的优化思想在实际开发中很有价值。下一篇我们继续讲解 LeetCode 热题 100 的下一道题。