LC26
把重复票号压缩掉,只保留每种一次
双指针 · 原地压缩有序数组的关键不是「删除」,而是「原地压缩」:前 k 个位置变成唯一结果区,后面的格子打灰即可。
输入 · nums(已升序)
0011122334
重复票号一定挨在一起 —— 只要发现 nums[fast] 和「结果区末尾 nums[k-1]」相同就跳过。
判题器视角
返回 k = 5
有效区 nums[0..4] = [0, 1, 2, 3, 4]
忽略区 nums[5..9]:判题器不看
fast 扫描write/k 写指针票号安检时间 O(n)空间 O(1)
边界测试 · 切换后整段动画会重算
帧 1/13Scene 1
当前步骤
Scene 1:不是删除,是原地压缩
发生了什么
LC26 不会真的把重复票号扔出队列。它要求把唯一票号一个接一个写到数组前面,最后返回这一段的长度 k;判题器只检查 nums[0..k-1],后面的格子写了什么不重要。
为什么这么做
数组在内存里是定长的,原地压缩比真删除省一倍空间;返回 k 就足够让调用方知道哪里是有效区。
结果区现在是什么
结果区还没开始:k=0,nums[0..k-1] = []。
面试该怎么说
我会强调:这题不是物理删除,是原地压缩;最终返回 k,约定前 k 个就是结果区。
k=0 · fast=· · n=10
指针 · 当前状态
fast
·
k (write)
0
nums[fast]
—
nums[k-1]
—
有效前缀 nums[0..k-1]
[]
数组完整快照(含忽略区)
[0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
fast 扫描光束write/k 落点框nums[k-1] 比较对象DUPLICATE 印章忽略区(不清理)
本步决策面板
还没开始扫描;先建立隐喻 + 不变量。
Go · 当前高亮行随帧切换
1func removeDuplicates(nums []int) int {2 k := 03 for _, x := range nums {4 if k == 0 || x != nums[k-1] {5 nums[k] = x6 k++7 }8 }9 return k10}
不变量 · 算法为什么对
- 下标区间 [0, k) 严格递增、无重复;判题器只看这一段。
- 有序输入 ⇒ 重复票号必相邻 ⇒ 只与 nums[k-1] 比较就够。
- k 是「下一个唯一票号要写入的格子」,不是「结果末尾」。
- fast 永远 ≥ k;写入只发生在 nums[k],永远不会破坏前缀。
时间线 · 点击跳转
常见误区
- 1. 误以为要真的删除数组元素数组在内存里是定长结构 —— 不可能在 O(1) 内删除中间元素。LC26 只要求把唯一票号写到前面、返回 k 即可。
- 2. 误以为要用 HashSetHashSet 是 O(n) 空间。利用有序性,重复值必相邻,只看 nums[k-1] 就能去重,空间 O(1)。
- 3. 误以为要和「原数组前一个元素」比较比较对象必须是结果区末尾 nums[k-1],而不是 nums[fast-1]。否则跨过多次重复后比较会失败。
- 4. 忘记返回 k判题器只接收一个整数,并据此切出 nums[0..k-1]。返回 k-1 或者 nums.length 都会被判错。
- 5. 忘记 k 后面的元素不重要不必把后面的位置清零或填空 —— 题目允许它们是任意值。试图清理只是浪费时间。
面试一句话答法
因为数组有序、重复值必相邻,我用双指针:写指针 k 指向「下一个唯一票号要写入的格子」,扫描指针 fast 从左到右;当 k == 0 或 nums[fast] ≠ nums[k-1] 时把 nums[fast] 写到 nums[k] 并 k++;最后返回 k。时间 O(n)、空间 O(1)。
关键澄清
这道题不是物理删除,是原地压缩;判题器只检查 nums[0..k-1],后面的元素允许任意值,因此空间复杂度 O(1)。