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

LC26 · Remove Duplicates · 数组 / 双指针

删除有序数组中的重复项:读写双指针原地压缩

要求原地、O(1) 空间——不是新建数组,而是让有效前缀自己生长出来。

升序数组中的相同值聚成连续分组,每组只留第一个。维护一个「写指针」指向有效前缀的末尾、一个「读指针」扫描全部元素:读指针遇到新值就把它写到写指针处并前移写指针。返回写指针就是答案 k。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

给你一个升序数组,比如 [0,0,1,1,1,2,2,3,3,4]。请原地删除重复出现的元素,使每个元素只出现一次,并返回删除后数组的新长度。

注意两个关键词:原地,意味着不能用额外数组;返回新长度,意味着不要求真的把数组缩短——多余的元素留在后面完全合法。

这篇文章带你走一遍:为什么「复制一份」不行,双指针是怎么出现的,以及那个容易让人困惑的契约——返回 k 就够了。

先明确问题

输入升序数组 nums,要求原地去重,返回去重后的长度 k。前 k 个元素必须是不重复的,且相对顺序不变。

示例:nums = [0,0,1,1,1,2,2,3,3,4],返回 5,前 5 个元素是 [0,1,2,3,4]。

数组后面的部分可以是任何值——题目只验证 nums[0..k) 。这是很多初学者卡住的地方:以为要物理删除。

先看最显然的做法:复制一份

不要求原地的话,太简单了:新建一个数组,遍历一遍,第一次见到的值就 append 进去。

但它用掉了 O(n) 的额外空间。题目明确要求 O(1) 空间,这条路被堵死。

那么问题变成:能不能在同一个数组上,把「该保留的值」搬到前面,同时不丢失「还没看的值」?

复制一份去重:直观但用了 O(n) 额外空间
0
[0]
0
[1]
1
[2]
1
[3]
1
[4]
2
[5]
2
[6]
3
[7]
3
[8]
4
[9]

双指针:一个读,一个写

关键观察:数组升序,所以相同的值一定连在一起。这意味着「新值」只会在连续分组的开头出现。

我们用两个指针:write 指向「下一个该写入的位置」(有效前缀的末尾),read 指针从头扫到尾。

初始化:write = 1(下标 0 的值一定保留),read = 1。然后读指针每走一步,问一句:nums[read] 和 nums[write-1](有效前缀的最后一个值)相等吗?

不相等:说明遇到了新值,把它写到 nums[write],write 前移。相等:说明是重复副本,跳过。

关键write 指向「有效前缀的末尾」——它是写入点,也是比较的基准。
双指针就位:write=1 指向写入点,read=1 开始扫描
0
[0]
0
[1]
1
[2]
1
[3]
1
[4]
2
[5]
2
[6]
3
[7]
3
[8]
4
[9]
write
read

一路压缩:遇见新值就写入

手推一遍 [0,0,1,1,1,2,2,3,3,4]:

read=1:nums[1]=0 与 nums[write-1]=nums[0]=0 相等 → 跳过,read 前进。

read=2:nums[2]=1 与 nums[0]=0 不等 → 新值!nums[1]=1,write 变 2。

read=3、4:都是 1,与 nums[1]=1 相等 → 跳过。

read=5:nums[5]=2 ≠ nums[1]=1 → 新值!nums[2]=2,write 变 3。

…… 如此反复,读到结尾时 write=5,前 5 位恰好是 [0,1,2,3,4]。

注意 write 永远 ≤ read:写的位置要么是自己,要么是已经被读过的位置,所以覆盖是安全的。

read 扫到新值就写到 write,重复副本直接跳过
0
[0]
0
[1]
1
[2]
1
[3]
1
[4]
2
[5]
2
[6]
3
[7]
3
[8]
4
[9]
write
read

理解契约:返回 k,而不是真的缩短

扫描结束时 nums 变成 [0,1,2,3,4,2,2,3,3,4],后面还有一堆旧值。

但题目只要求返回 k=5,并保证 nums[0..5) 是不重复的。后 5 位是什么不重要。

这正是「原地」题型的通用套路:不删除,只压缩有效前缀,用返回值告知有效长度。

契约「删除」在这里的意思不是物理移除,而是让 k 个有效元素占据数组头部。

七行 Go:双指针原地压缩

solution.goGo
func removeDuplicates(nums []int) int {
write := 1
for read := 1; read < len(nums); read++ {
if nums[read] != nums[write-1] {
nums[write] = nums[read]
write++
}
}
return write
}

1write 指向「下一个写入点」,同时它就是有效前缀的长度。

2读指针从 1 开始扫描(下标 0 一定保留)。

3遇到和有效前缀最后一个值不同的新值,就写入并推进 write。

4相等则是重复副本,什么都不做。

5返回 write 就是去重后的长度。

总结

读指针负责发现新值,写指针负责安放新值,返回写指针就是 k。

  • 升序使相同值连续成组,「新值」只出现在分组开头。
  • 双指针里 write ≤ read 恒成立,所以原地覆盖安全。
  • 返回值 k 定义有效前缀,数组尾部残留无关紧要。
同族题目
LC27移除元素(同款双指针)LC283移动零(把非零搬前面)LC80删除有序数组中的重复项 II(允许两次)