移动零 (Move Zeroes) - LeetCode 题解
题目描述
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。要求必须原地修改数组,不能使用额外的数组空间。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
解题思路
方法一:双指针法(推荐)
-
初始化指针:
lastNonZero
指针记录下一个非零元素应该放置的位置
-
第一次遍历:
- 遍历数组,将非零元素移动到数组前部
- 每遇到一个非零元素,就将其放到
lastNonZero
位置,然后lastNonZero
后移
-
第二次遍历:
- 将
lastNonZero
之后的所有位置填充为0
- 将
-
复杂度分析:
- 时间复杂度:O(n),需要两次遍历
- 空间复杂度:O(1),原地操作,只使用了常数空间
方法二:交换法(优化版)
-
单次遍历:
- 使用双指针,一个指针用于遍历,另一个指针记录非零位置
- 遇到非零元素时,与记录位置的元素交换
-
优点:
- 只需要一次遍历
- 不需要最后的填充0操作
-
复杂度分析:
- 时间复杂度: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)
}
}
}
复杂度分析
-
时间复杂度:
- 方法一:O(2n) = O(n),两次遍历
- 方法二:O(n),一次遍历
-
空间复杂度:
- 两种方法都是 O(1),只使用了常数空间
优化思路
-
减少赋值操作:
- 方法一在填充0时可能有冗余操作
- 方法二通过交换避免了额外的填充操作
-
边界条件处理:
- 空数组处理
- 全零数组处理
- 无零数组处理
-
代码简洁性:
- 方法二代码更简洁,逻辑更紧凑
总结
这道题考察了对数组的双指针操作,关键在于如何在保持非零元素顺序的同时移动零元素。两种方法各有优劣:
- 方法一:思路直观,容易理解,适合初学者
- 方法二:效率更高,代码更简洁,适合追求性能的场景
掌握这种双指针技巧对解决类似的数组操作问题(如删除重复元素、特定元素移动等)很有帮助。
扩展思考
- 如果要求将所有非零元素移动到数组开头,零元素保持原有顺序,该如何修改算法?
- 如何修改算法以同时满足移动零和保持零元素的相对顺序?
- 如果数组非常大,如何优化算法以减少内存访问次数?