移动零是LeetCode热题100中的经典数组操作问题,题目要求将一个包含零元素的整数数组中的所有零移动到数组末尾,同时保持非零元素的相对顺序不变。这个问题看似简单,却考察了程序员对数组操作、指针技巧和算法效率的深刻理解。
在实际开发中,类似的操作场景并不少见。比如在处理用户提交的表单数据时,可能需要过滤掉无效的零值但保留有效数据的顺序;或者在图像处理中,需要将特定像素值(如黑色)集中到图像边缘。这类问题的核心在于如何在保证数据完整性的前提下,高效地重组数据结构。
最直观的解法是创建一个新数组,先遍历原数组收集所有非零元素,再在末尾补零。这种方法时间复杂度O(n),空间复杂度O(n)。虽然能解决问题,但不符合题目"原地操作"的隐含要求,且当数组很大时会浪费额外空间。
python复制def moveZeroes_naive(nums):
non_zeros = [x for x in nums if x != 0]
return non_zeros + [0] * (len(nums) - len(non_zeros))
更优的解法是使用双指针技巧,这也是面试官最希望看到的解决方案。其核心思想是维护一个"慢指针"指向下一个非零元素应该存放的位置,同时用"快指针"遍历数组。当快指针遇到非零元素时,就将其与慢指针位置交换(或直接覆盖),然后两个指针都前进;遇到零时则只移动快指针。
这种方法的精妙之处在于:
以下是Python的标准实现版本:
python复制def moveZeroes(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
这个实现有几个关键点需要注意:
当fast和slow指针指向同一位置时(即前面没有零时),交换操作是多余的。可以添加条件判断来优化:
python复制def moveZeroes_optimized(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
if fast != slow:
nums[slow] = nums[fast]
nums[fast] = 0
slow += 1
这个优化版本在数组前面没有零的情况下能减少不必要的赋值操作,对于某些特定情况(如前半部分都是非零元素)性能更好。
在实现和测试时,需要考虑以下边界情况:
python复制test_cases = [
([], []), # 空数组
([0], [0]), # 单零
([1], [1]), # 单非零
([1,0,2,0,3], [1,2,3,0,0]), # 混合情况
([0,0,1], [1,0,0]), # 零在前
([1,2,0,0], [1,2,0,0]), # 零在后
([1,2,3], [1,2,3]), # 无零
([0,0,0], [0,0,0]) # 全零
]
双指针法只需要一次遍历数组,时间复杂度为O(n),其中n是数组长度。这是最优的时间复杂度,因为至少需要检查每个元素一次。
算法只使用了固定数量的额外空间(几个指针变量),因此空间复杂度为O(1),满足原地操作的要求。
虽然时间复杂度相同,但不同实现的实际运行时间可能有差异:
在实际应用中,对于小数组差异不大,但对于极大数组,优化版本可能更有优势。
双指针法是解决数组/链表操作问题的强大工具,常见模式包括:
掌握这些模式可以解决大量类似问题,如:
在数据预处理中,经常需要过滤无效值并保留有效数据的顺序。例如:
在资源受限的环境中,原地操作算法非常宝贵:
这个问题虽然简单,但能有效考察候选人的:
以输入[0,1,0,3,12]为例:
初始状态:[0,1,0,3,12], slow=0, fast=0
fast=0: 零,跳过 → slow=0, fast=1
fast=1: 非零,交换 → [1,0,0,3,12], slow=1, fast=2
fast=2: 零,跳过 → slow=1, fast=3
fast=3: 非零,交换 → [1,3,0,0,12], slow=2, fast=4
fast=4: 非零,交换 → [1,3,12,0,0], slow=3, fast=5
java复制public void moveZeroes(int[] nums) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != 0) {
int temp = nums[slow];
nums[slow] = nums[fast];
nums[fast] = temp;
slow++;
}
}
}
cpp复制void moveZeroes(vector<int>& nums) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++) {
if (nums[fast] != 0) {
swap(nums[slow], nums[fast]);
slow++;
}
}
}
javascript复制function moveZeroes(nums) {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== 0) {
[nums[slow], nums[fast]] = [nums[fast], nums[slow]];
slow++;
}
}
}
不同语言的实现大同小异,主要区别在于:
标准交换版本在最坏情况下(全非零数组)会有2n次写操作(每个元素交换两次)。可以优化为先覆盖非零元素,最后统一补零:
python复制def moveZeroes_minimal_write(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow] = nums[fast]
slow += 1
for i in range(slow, len(nums)):
nums[i] = 0
这种版本在最坏情况下只有n次写操作(n次覆盖+n次补零),对于写操作昂贵的场景(如闪存)更有优势。
对于极大数组,可以考虑分块处理:
虽然复杂度相同,但可以利用多核优势加速处理。
如果需要保持其他属性的稳定性(如关联数据的对应关系),或者处理更复杂的移动条件,算法需要相应调整。这时双指针法的基础框架仍然适用,但判断条件和移动逻辑会更复杂。