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]
操作后:[1,3,12,0,0]
输入:nums = [0]
操作后:[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 后续单元固定一个数+双指针+去重