Python算法训练序列

双指针与滑动窗口 · Unit 02

← Unit 01
283
LeetCode 283Move Zeroes · 移动零

fast 负责读完整个数组,
slow 负责维护正确前缀。

本题不仅要求把零移动到末尾,还要求保持所有非零元素的相对顺序,并且必须原地修改数组。双指针的作用不是“寻找两个数”,而是将读取位置与写入位置分离。

LeetCode #283 原题内容 中文转述 · 示例、约束与提交签名

题目要求

给定整数数组 nums,将所有 0 移动到数组末尾,同时保持所有非零元素的相对顺序。必须在原数组上完成操作,不能复制一个额外数组。

class Solution: def moveZeroes(self, nums: List[int]) -> None: # 原地修改 nums,不返回新数组

约束

  • 1 ≤ len(nums) ≤ 10000
  • -2³¹ ≤ nums[i] ≤ 2³¹ - 1
  • 进阶:尽量减少操作次数。

示例

输入:nums = [0,1,0,3,12]
操作后:[1,3,12,0,0]
输入:nums = [0]
操作后:[0]

三个同时满足的要求

  • 所有非零元素位于前部;
  • 非零元素相对顺序不变;
  • 使用 O(1) 额外空间。
1. 理解稳定压缩
2. 体验重复搬移
3. 建立读写指针
4. 代码联动
5. 删除元素迁移
先接回上一单元:双指针不是固定模板,而是给两个位置分工Unit 01 → Unit 02
#125 验证回文:两个指针都负责“读”

left 从左读,right 从右读;过滤无效字符后向中间靠近,比较当前两端是否一致。

→
#283 移动零:一个负责“读”,一个负责“写”

fast 从左到右读完整个数组;slow 指向下一个有效元素应该写入的位置。

可迁移的旧知识:先不要背 left、right、fast、slow。先问“每个指针承担什么角色”,再用循环不变量约束它们如何移动。
演示数组:nums = [0,1,0,3,12]等待操作
[0, slow):已整理非零前缀 [slow, fast):已扫描的零区 fast:当前读取位置

当前循环不变量

[0, slow) 保存已扫描部分中的全部非零元素,且相对顺序不变。

当前移动规则

fast 遇到零只继续扫描;遇到非零则写入 slow,并让 slow 右移。

slow0
fast0
nums[fast]0
已整理长度0
数组状态未完成
朴素搬移的隐藏成本

每发现一个零,就把后面的所有元素左移一格。连续或大量零会导致相同元素被反复搬动,最坏达到 O(n²)。

补全 LeetCode 提交代码卡住可查看参考答案
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
slow =
for fast in range(len(nums)):
if :
class Solution: def moveZeroes(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
为什么交换不会破坏非零顺序?slow始终是最靠左的待填位置,而fast按原顺序遇见非零元素。每个非零元素依次被放到slow,因此前缀中的顺序与原数组一致。若slow==fast,代码只是自交换,不影响结果。
代码—读写指针—数组联动轨迹:[0,1,0,3,12]
1class Solution:
2 def moveZeroes(self, nums: List[int]) -> None:
3 slow = 0
4 for fast in range(len(nums)):
5 if nums[fast] != 0:
6 nums[slow], nums[fast] = nums[fast], nums[slow]
7 slow += 1
点击下一行,观察fast如何读取,slow如何扩展正确前缀。
完成代码联动后进入迁移练习 补全正确、查看答案或播放完整轨迹后解锁。

迁移题:LeetCode #27 移除元素

不再要求你从空白处写完整代码。先找出 #283 与 #27 的共同骨架,再只替换三个真正变化的槽位。

1. 对照旧题与新题
2. 映射三个变化
3. 拼装并运行

已经掌握:#283 移动零

保留条件:nums[fast] != 0 写入方式:交换到 nums[slow] 最终结果:数组被完整整理

准备迁移:#27 移除元素

保留条件:? 写入方式:? 最终结果:返回有效前缀长度
保持不变slow = 0;fast 从左到右扫描;[0, slow) 始终是保留下来的元素。
需要改变什么元素算“有效”、有效元素怎样写入、函数最后返回什么。

先判断迁移幅度:下面哪句话最准确?

槽位 A|什么元素应该被保留?

槽位 B|发现有效元素后,最直接的写入动作是什么?

#27 不关心数组尾部内容,因此不必把被覆盖的元素交换回去。覆盖写入比交换更贴合题意。

槽位 C|扫描结束后应该返回什么?

从 #283 保留下来的骨架slow = 0
for fast in range(len(nums)):
    if ...:
        ...
        slow += 1
本题只新增的输出语义slow 不只是写指针,扫描结束后也等于“保留下来的元素数量”。
认知压缩:你不是重新写一道题,而是在复用“读写指针”骨架,只把三个槽位替换成新题语义。

#283:移动零

if nums[fast] != 0:
    nums[slow], nums[fast] = nums[fast], nums[slow]
    slow += 1
# 不需要返回长度

#27:移除元素

if nums[fast] != val:
    nums[slow] = nums[fast]
    slow += 1
return slow
准备运行
可选:脱离槽位后独立重写一次

这一步不影响完成本单元。建议先运行样例,再尝试凭记忆重写。

本单元形成的算法Skill

当数组需要原地过滤或压缩时,用 fast 扫描全部输入,用 slow 维护有效输出前缀。进入每轮时,[0, slow) 已经是处理完成的结果,[slow, fast) 是已扫描但不应留在前缀中的位置。

LeetCode #283 同向读写指针 稳定压缩 原地修改 时间 O(n) 额外空间 O(1)
#125 已完成相向指针+字符过滤
#283 当前:稳定压缩同向读写指针
#167 下一单元有序数组单调排除
#15 后续单元固定一个数+双指针+去重