【每日一道算法题 day4】移动零 (Move Zeroes) - LeetCode 题解

移动零 (Move Zeroes) - LeetCode 题解

题目描述

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。要求必须原地修改数组,不能使用额外的数组空间。

示例 1:

复制代码
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

复制代码
输入: nums = [0]
输出: [0]

解题思路

方法一:双指针法(推荐)

  1. 初始化指针

    • lastNonZero 指针记录下一个非零元素应该放置的位置
  2. 第一次遍历

    • 遍历数组,将非零元素移动到数组前部
    • 每遇到一个非零元素,就将其放到 lastNonZero 位置,然后 lastNonZero 后移
  3. 第二次遍历

    • lastNonZero 之后的所有位置填充为0
  4. 复杂度分析

    • 时间复杂度:O(n),需要两次遍历
    • 空间复杂度:O(1),原地操作,只使用了常数空间

方法二:交换法(优化版)

  1. 单次遍历

    • 使用双指针,一个指针用于遍历,另一个指针记录非零位置
    • 遇到非零元素时,与记录位置的元素交换
  2. 优点

    • 只需要一次遍历
    • 不需要最后的填充0操作
  3. 复杂度分析

    • 时间复杂度:O(n)
    • 空间复杂度:O(1)

Go 代码实现

方法一实现

go 复制代码
func moveZeroes(nums []int) {
    lastNonZero := 0
    // 第一次遍历:将所有非零元素前移
    for _, num := range nums {
        if num != 0 {
            nums[lastNonZero] = num
            lastNonZero++
        }
    }
    
    // 第二次遍历:将剩余位置填充为0
    for i := lastNonZero; i < len(nums); i++ {
        nums[i] = 0
    }
}

方法二实现(优化版)

go 复制代码
func moveZeroesOptimized(nums []int) {
    lastNonZero := 0
    for i := 0; i < len(nums); i++ {
        if nums[i] != 0 {
            // 交换当前元素和lastNonZero位置的元素
            nums[i], nums[lastNonZero] = nums[lastNonZero], nums[i]
            lastNonZero++
        }
    }
}

测试用例

go 复制代码
func TestMoveZeroes(t *testing.T) {
    tests := []struct {
        input  []int
        expect []int
    }{
        {[]int{0, 1, 0, 3, 12}, []int{1, 3, 12, 0, 0}},
        {[]int{0}, []int{0}},
        {[]int{1, 2, 0, 4, 0, 5}, []int{1, 2, 4, 5, 0, 0}},
        {[]int{0, 0, 1}, []int{1, 0, 0}},
        {[]int{1, 2, 3, 4}, []int{1, 2, 3, 4}},
        {[]int{}, []int{}},
    }

    for _, tt := range tests {
        // 复制原数组,避免测试间相互影响
        nums := make([]int, len(tt.input))
        copy(nums, tt.input)
        
        moveZeroes(nums)
        if !reflect.DeepEqual(nums, tt.expect) {
            t.Errorf("moveZeroes(%v) = %v, want %v", tt.input, nums, tt.expect)
        }
    }
}

复杂度分析

  1. 时间复杂度

    • 方法一:O(2n) = O(n),两次遍历
    • 方法二:O(n),一次遍历
  2. 空间复杂度

    • 两种方法都是 O(1),只使用了常数空间

优化思路

  1. 减少赋值操作

    • 方法一在填充0时可能有冗余操作
    • 方法二通过交换避免了额外的填充操作
  2. 边界条件处理

    • 空数组处理
    • 全零数组处理
    • 无零数组处理
  3. 代码简洁性

    • 方法二代码更简洁,逻辑更紧凑

总结

这道题考察了对数组的双指针操作,关键在于如何在保持非零元素顺序的同时移动零元素。两种方法各有优劣:

  • 方法一:思路直观,容易理解,适合初学者
  • 方法二:效率更高,代码更简洁,适合追求性能的场景

掌握这种双指针技巧对解决类似的数组操作问题(如删除重复元素、特定元素移动等)很有帮助。

扩展思考

  1. 如果要求将所有非零元素移动到数组开头,零元素保持原有顺序,该如何修改算法?
  2. 如何修改算法以同时满足移动零和保持零元素的相对顺序?
  3. 如果数组非常大,如何优化算法以减少内存访问次数?