跳到主要内容

283. 移动零

题目描述​

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

输入: nums = [0]
输出: [0]

提示:

  • 1 <= nums.length <= 10^4
  • -2^31 <= nums[i] <= 2^31 - 1

进阶: 你能尽量减少完成的操作次数吗?


解题思路​

第一步:理解问题本质​

需要在原地修改数组,将非零元素移到前面,零移到后面,同时保持非零元素的相对顺序。

这是一个典型的数组分区问题,可以使用双指针解决。

第二步:暴力解法​

创建一个新数组,先复制所有非零元素,再填充零,最后复制回原数组。

def moveZeroes_brute(nums):
n = len(nums)
# 借一个临时数组存结果,最后再整体倒回 nums
result = []

# 先复制非零元素
# 按原顺序追加,非零元素之间的相对次序因此不会变
for num in nums:
if num != 0:
result.append(num)

# 再填充零
# 缺几个就补几个 0,保证长度和原数组一致
while len(result) < n:
result.append(0)

# 复制回原数组
# 题目要求原地修改,只能逐位写回 nums
for i in range(n):
nums[i] = result[i]

为什么不够好?

  • 使用了 O(n) 额外空间,不符合题目要求
  • 需要多次遍历数组

第三步:优化解法 - 两次遍历​

第一遍将所有非零元素移到前面,第二遍将剩余位置填充为零。

def moveZeroes_two_pass(nums):
n = len(nums)
# left 是下一个非零元素该落的位置,也等于已写入的非零个数
left = 0

# 第一遍:将非零元素移到前面
# 用赋值而不是交换:i 扫过的旧值之后会被覆盖,不用保留
for i in range(n):
if nums[i] != 0:
# 非零才写;零被跳过,相当于被后面的元素挤到了末尾
nums[left] = nums[i]
left += 1

# 第二遍:填充零
# left 之后都是多出来的空位,补 0 就能保持非零元素的顺序
for i in range(left, n):
nums[i] = 0

复杂度分析:

  • 时间复杂度 O(n) - 两次遍历
  • 空间复杂度 O(1) - 原地修改

第四步:最优解法 - 快慢指针​

使用两个指针,一次遍历完成:

  • 慢指针:指向下一个非零元素应该放置的位置
  • 快指针:遍历数组,寻找非零元素

当快指针找到非零元素时,与慢指针交换,然后慢指针前进。


完整代码实现​

算法可视化 · 逐帧回放这道题的代码39 帧 · 录制输入 [0,1,0,3,12]
from typing import List


class Solution:
"""
283. 移动零 - 双指针解法

核心思路:
使用双指针将数组分为两部分:左边是非零元素,右边是零。

解法一:两次遍历(当前实现)
- 第一遍:统计零的个数
- 第二遍:将所有非零元素移到前面
- 第三遍:将剩余位置填充为零

解法二:一次遍历(快慢指针)
- 慢指针指向当前可以放置非零元素的位置
- 快指针遍历数组,遇到非零元素就与慢指针交换

时间复杂度: O(n) - 遍历数组一次或两次
空间复杂度: O(1) - 原地修改,只使用常数额外空间
"""

def moveZeroes(self, nums: List[int]) -> None:
"""
两次遍历法:先将非零元素前移,再填充零
不返回任何值,直接修改输入数组
"""
n = len(nums)
# 统计零的个数
count = 0
for i in range(n):
if nums[i] == 0:
count += 1

# 将所有非零元素移到数组前面
left = 0 # 指向下一个可以放置非零元素的位置
for i in range(n):
if nums[i] != 0:
nums[left] = nums[i]
left += 1

# 将剩余位置填充为零
for i in range(n - count, n):
nums[i] = 0

def moveZeroes_two_pointers(self, nums: List[int]) -> None:
"""
快慢指针法:一次遍历完成
- 慢指针:指向下一个非零元素应该放置的位置
- 快指针:遍历数组寻找非零元素
"""
slow = 0 # 慢指针:指向下一个非零元素的位置
for fast in range(len(nums)):
if nums[fast] != 0:
# 交换快慢指针指向的元素
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1

示例推演​

以 nums = [0, 1, 0, 3, 12] 为例:

两次遍历法:​

第一遍(统计零):

nums = [0, 1, 0, 3, 12]
零的个数 count = 2

第二遍(移动非零元素):

初始: left = 0
i=0: nums[0]=0, 跳过
i=1: nums[1]=1, nums[0]=1, left=1
i=2: nums[2]=0, 跳过
i=3: nums[3]=3, nums[1]=3, left=2
i=4: nums[4]=12, nums[2]=12, left=3

此时 nums = [1, 3, 12, 3, 12]

第三遍(填充零):

从索引 n-count=3 到 n-1=4 填充零
nums[3] = 0
nums[4] = 0

最终结果: [1, 3, 12, 0, 0]

快慢指针法:​

初始: slow = 0, nums = [0, 1, 0, 3, 12]

fast=0: nums[0]=0, 是零,不交换
nums = [0, 1, 0, 3, 12], slow=0

fast=1: nums[1]=1, 非零,交换 nums[0] 和 nums[1]
nums = [1, 0, 0, 3, 12], slow=1

fast=2: nums[2]=0, 是零,不交换
nums = [1, 0, 0, 3, 12], slow=1

fast=3: nums[3]=3, 非零,交换 nums[1] 和 nums[3]
nums = [1, 3, 0, 0, 12], slow=2

fast=4: nums[4]=12, 非零,交换 nums[2] 和 nums[4]
nums = [1, 3, 12, 0, 0], slow=3

最终结果: [1, 3, 12, 0, 0]

复杂度分析​

解法时间复杂度空间复杂度操作次数说明
暴力O(n)O(n)3n需要额外数组
两次遍历O(n)O(1)2n两次遍历数组
快慢指针O(n)O(1)n一次遍历,最优

易错点总结​

1. 交换 vs 赋值​

两次遍历法使用赋值:

# 两次遍历只要最终结果,被覆盖的旧值不再需要
nums[left] = nums[i] # 赋值

快慢指针法使用交换:

# 一次遍历必须交换:慢指针位置上的零要被换到后面,不能直接覆盖
nums[slow], nums[fast] = nums[fast], nums[slow] # 交换

2. 慢指针的更新时机​

正确做法: 只有在发生交换/赋值后才移动慢指针。

# 交换完才把 slow 后移,slow 之前始终是已就位的非零元素
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1 # 只有交换后才移动

3. 相对顺序的保持​

快慢指针法之所以能保持相对顺序,是因为:

  • 慢指针总是指向零(或已经处理过的位置)
  • 快指针找到的非零元素被交换到慢指针位置
  • 由于快指针从左到右遍历,非零元素的相对顺序不变

4. 边界条件​

  • 数组全为零:[0, 0, 0] -> [0, 0, 0]
  • 数组全为非零:[1, 2, 3] -> [1, 2, 3]
  • 单个元素:[0] 或 [1]

扩展思考​

1. 相关题目​

2. 变体问题​

题目:将数组中的负数移到前面,正数移到后面

解法相同,只需修改判断条件:

def moveNegatives(nums):
# 框架和「移动零」完全一样,只把判定条件换成「小于 0」
slow = 0
for fast in range(len(nums)):
if nums[fast] < 0: # 负数移到前面
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1

3. 荷兰国旗问题​

将数组分成三部分(红、白、蓝),需要三个指针:

def sortColors(nums):
# 三段区间:[0, red) 是 0,[red, white) 是 1,(blue, n-1] 是 2
red, white, blue = 0, 0, len(nums) - 1
# white 是当前待判定的位置,blue 之后都已确定是 2
while white <= blue:
if nums[white] == 0:
# 换来的值一定是 1,所以 red 和 white 一起后移
nums[red], nums[white] = nums[white], nums[red]
red += 1
white += 1
elif nums[white] == 1:
# 本来就是 1,white 直接后移
white += 1
else:
# 换来的值还没检查过,white 不能动,只收缩右端
nums[white], nums[blue] = nums[blue], nums[white]
blue -= 1

自测​

加载评论中...