283. 移动零

难度:简单 | 主题:数组 / 双指针

题目

给定一个数组 nums,将所有 0 移动到数组末尾,同时保持非零元素的相对顺序。必须 原地 操作。

示例

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

思路

先讲个故事:排队买奶茶

奶茶店排队,队伍里混了几个”0 号客人”(不想喝奶茶,站在队伍里发呆)。店长说:所有 0 号客人到队尾去,其他人保持原有顺序。

店长策略:左手(left)指向”下一个非零客人该站的位置”,右手(right)挨个扫人。遇到非零的 → 和左手位置的人交换,左手右移一格。


引导式推导:从朴素到指针

朴素想法:遍历数组,遇到 0 就删掉,然后在末尾追加一个 0。

pop(i) 之后所有元素左移,索引偏移 → 噩梦。而且 pop + append 是 O(n²)。

双指针思路:不让 0”移动”,而是让非零元素”挤到前面”。


graph LR

    subgraph 数组 [0, 1, 0, 3, 12]

        L["left<br/>= 0"]

        R["right<br/>= 0"]

    end

    L -->|"扫描开始"| R

 
初始:      [0, 1, 0, 3, 12]
 
            L,R
 
right=0 → nums[0]=0 → 不交换,R 右移
 
           [0, 1, 0, 3, 12]
 
           L  R
 
right=1 → nums[1]=1 → 交换(L,R),L++,R++
 
           [1, 0, 0, 3, 12]
 
              L     R
 
right=3 → nums[3]=3 → 交换(L,R),L++,R++
 
           [1, 3, 0, 0, 12]
 
                 L     R
 
right=4 → nums[4]=12 → 交换(L,R)
 
           [1, 3, 12, 0, 0]
 
                    L   R    ← 完成
 

这里的关键洞察:left 和 right 之间全是 0,所以交换相当于把非零元素往前放,0 自动被换到后面。


为什么交换而不是覆盖?

 
# 覆盖法:先移非零,再补零
 
pos = 0
 
for num in nums:
 
    if num != 0:
 
        nums[pos] = num
 
        pos += 1
 
while pos < len(nums):
 
    nums[pos] = 0
 
    pos += 1
 

覆盖法也需要两次遍历。交换法一次遍历搞定,且更优雅。两者本质一样。


代码

 
def moveZeroes(self, nums):
 
    left = 0                           # left 指向下一个非零元素的位置
 
    for right in range(len(nums)):     # right 扫描整个数组
 
        if nums[right] != 0:           # 找到非零 → 交换到 left 位置
 
            nums[left], nums[right] = nums[right], nums[left]
 
            left += 1                  # left 右移
 

复杂度

指标解释
时间O(n)一次遍历,每步 O(1)
空间O(1)原地交换

实战考量

延伸思考

Q:为什么交换能保持非零元素的相对顺序?

A:left 指向”下一个非零元素应该放的位置”,right 扫描时按原始顺序遇到非零元素。把非零元素换到 left 位置时,left 位置原来的元素(一定是 0)被换到 right 位置。因为 left ≤ right,且 left 到 right 之间全是 0,所以不会打乱非零顺序。

Q:覆盖法比交换法差在哪?

A:覆盖法需要两次遍历(先移非零,再补零)。交换法一次遍历到位,且代码更简洁。

Q:如果要求把负数也移到前面(三分区)呢?

A:荷兰国旗问题——三指针分区,分负数、零、正数三区。

Q:如果要求移动零到开头呢?

A:从右往左扫描,或者同样逻辑但把条件改成”遇到非零就往后换”。

易错点

  • 用交换不是覆盖——覆盖会丢失数据(除非显式补零)

  • left 和 right 同向移动,不是相向

  • 不要用 pop + append——O(n²) 且索引偏移

  • nums[-1] = nums.pop(i) 是覆盖末尾元素,不是追加


生活类比

双指针 → 奶茶店排队

left = “下一个非零客人的站位”,right = “正在检查的客人”。

遇到非零客人 → 请他站到 left 位置(跟那个位置的 0 号客人换位置)。

所有非零客人排好了,零号客人自然被挤到队尾。


相关题目

题目关系
26删除有序数组中的重复项同族原地操作,快慢指针模式

→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)

速记卡(面试闪卡)

Q1:一句话讲清「283. 移动零」到底是什么?

A:把数组里所有 0 移到末尾、非零保持原顺序,且必须原地操作——双指针一次扫描搞定。

Q2:一、题目与约束 —— 怎么理解?

A:奶茶店排队类比:队伍里混了几个「0 号客人」发呆,店长让他们去队尾,其他人保持顺序。这就是题面——把 0 挪到末尾、非零顺序不变,且不能开新数组,必须原地(in-place,不占额外空间)改。

Q3:二、双指针思路 —— 怎么理解?

A:不用 pop+append(O(n²) 还索引偏移)。双指针(Two Pointers,一快一慢同向扫):left 指「下一个非零该站的位置」,right 挨个扫,遇非零就和 left 交换、left 右移。因 left~right 之间全是 0,交换天然把非零前挤、0 后挪。

Q4:三、为什么交换保序 —— 怎么理解?

A:left 永远 ≤ right,且 left 到 right 之间全是 0。把非零换到 left 时,left 原位(定是 0)被换到 right——不碰任何已有非零,故相对顺序不乱。覆盖法也行(先移非零再补零)但需两次遍历,交换法一次到位更优雅。

Q5:四、复杂度与易错点 —— 怎么理解?

A:时间 O(n) 一次遍历每步 O(1),空间 O(1) 原地交换。易错:用 pop+append 变 O(n²);left/right 同向非相向;nums[-1]=nums.pop(i) 是覆盖末尾非追加;忘补零会丢数据。

Q6:核心速记主线有哪些?

  • 双指针:left 指非零落位,right 扫描,遇非零就交换

  • 交换法一次遍历 O(n),比覆盖/ pop 更优雅

  • left≤right 且区间内全 0,故交换保非零顺序

  • 原地 O(1),易错点是 pop+append 与相向误用

口诀

A:移动零,双指针;

左定位右扫,非零就交换。

一遍 O(n),原地 O(1);

顺序不乱,零到尾端。

相关链接