LeetCode 283 题 (Move Zeroes) 是一道经典的数组操作题。题目要求将数组中所有的0移动到末尾,同时保持非零元素的相对顺序,且必须原地 (In-place)操作,不能使用额外的数组空间。
本文提供两种时间复杂度的 Java 解法,分别对应“覆盖后填充”和“交换”两种核心思维。
方法一:覆盖 + 补零(推荐:逻辑最清晰)
核心思路
我们可以把数组nums想象成一个栈。维护一个指针j,用来指向下一个存放非零元素的位置。
第一次遍历(归位):
遍历整个数组,只要遇到 非零元素,就直接把它“写”到 nums[j] 的位置,然后 j 后移一位。
注意:这里我们通过直接覆盖来移动数据,不用管被覆盖的数字,也不用管 0 去哪了。
第二次处理(补零):
遍历结束后,j 之前的位置存储的都是按顺序排列好的非零数。那么,从 j 到数组末尾的所有位置,理应全是 0。直接批量赋值即可。
Java 代码实现
Java
class Solution { public void moveZeroes(int[] nums) { int j = 0; // j 指向当前非零元素应该存放的位置 // 1. 第一步:将所有非零元素移到数组开头 for (int i = 0; i < nums.length; i++) { if (nums[i] != 0) { nums[j] = nums[i]; // 直接覆盖 j++; } } // 2. 第二步:将剩余位置全部填充为 0 // 使用 Arrays.fill 是 Java 中处理批量赋值最高效的方式 Arrays.fill(nums, j, nums.length, 0); } }复杂度分析
时间复杂度:
。我们需要遍历数组一次来移动非零数,
Arrays.fill底层虽然也是循环但极快,整体仍为线性时间。空间复杂度:
。只使用了几个整数变量。
方法二:双指针交换(进阶:一次遍历)
核心思路
方法一在逻辑上分成了“移数”和“补零”两步。方法二试图通过交换 (Swap),在一次遍历中完成所有任务。
维护一个指针j,它的含义是:当前最左边的 0 的位置(或者说是等待被非零元素交换的位置)。
用指针
i遍历数组。当
nums[i]是非零数时:将其与
nums[j]进行交换。交换后,非零数归位到了左边,原来的 0 被换到了当前位置。
j向后移动一位,准备接收下一个非零数。
当
nums[i]是0时:i继续走,j停在原地(标记这里有个 0 等待被换走)。
这种方法形象地被称为“滚雪球”:j和i中间的区域就是积累的 0,我们不断把后面的非零数扔到雪球前面去。
Java 代码实现
Java
class Solution { public void moveZeroes(int[] nums) { int j = 0; // j 指向第一个 0 的位置 for (int i = 0; i < nums.length; i++) { // 只有遇到非零元素才执行操作 if (nums[i] != 0) { // 优化:只有当 i > j 时才交换 // (避免数组开头全是非零数时,自己和自己交换) if (i > j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } j++; } } } }复杂度分析
时间复杂度:
。严格的一次遍历。
空间复杂度:
。
总结:哪种方法更好?
| 特性 | 方法一 (覆盖+补零) | 方法二 (双指针交换) |
| 操作次数 | 总是 | 最少 0 次(如果全是非零),最多 $N$ 次 |
| 代码可读性 | ⭐⭐⭐⭐⭐ (利用了 Java API) | ⭐⭐⭐⭐ (交换逻辑稍显复杂) |
| 推荐场景 | 通用推荐。逻辑分离,不易出错。 | 特殊优化。如果已知数组中 0 很少,或者写操作代价很高时使用。 |
对于 Java 选手,通常推荐使用方法一,因为它利用了Arrays.fill,代码更简洁,且在大多数测试用例下性能非常稳定。