D

当前:LC26 · 删除有序数组中的重复项 · 首次出现于 Day 1 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

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]
原始队伍 · CONVEYOR BELT(fast 在这条带上从左到右扫)0[0]0[1]1[2]1[3]1[4]2[5]2[6]3[7]3[8]4[9]k=0唯一结果区 · UNIQUE ZONE (判题器只看这里)(结果区还为空:k = 0)
fast 扫描光束write/k 落点框nums[k-1] 比较对象DUPLICATE 印章忽略区(不清理)

本步决策面板

还没开始扫描;先建立隐喻 + 不变量。

Go · 当前高亮行随帧切换

1func removeDuplicates(nums []int) int {
2 k := 0
3 for _, x := range nums {
4 if k == 0 || x != nums[k-1] {
5 nums[k] = x
6 k++
7 }
8 }
9 return k
10}

不变量 · 算法为什么对

  • 下标区间 [0, k) 严格递增、无重复;判题器只看这一段。
  • 有序输入 ⇒ 重复票号必相邻 ⇒ 只与 nums[k-1] 比较就够。
  • k 是「下一个唯一票号要写入的格子」,不是「结果末尾」。
  • fast 永远 ≥ k;写入只发生在 nums[k],永远不会破坏前缀。

时间线 · 点击跳转

常见误区

  • 1. 误以为要真的删除数组元素
    数组在内存里是定长结构 —— 不可能在 O(1) 内删除中间元素。LC26 只要求把唯一票号写到前面、返回 k 即可。
  • 2. 误以为要用 HashSet
    HashSet 是 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)。