26. 删除有序数组中的重复项
难度:简单 | 主题:数组 + 双指针(快慢指针)
题目
给你一个非严格递增排列的数组 nums,请原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应保持一致。不要使用额外的数组空间,必须原地修改输入数组。
示例
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4,…]
解释:函数返回新长度 5,前 5 个元素为 0,1,2,3,4
思路
先讲个故事:整理书架
你有一排书架,上面插着按顺序排列的书。有些书名重复了——同一本书有好几本。
你想把重复的书抽走,只保留一本,然后把剩下的书往左推紧,让书架前半段变得紧凑有序。
你的做法:左手放在第一本书上(慢指针),右手从第二本开始一本一本往后扫(快指针)。
-
右手发现一本新书(和左手的不同)→ 左手往前移一位,把新书插过来
-
右手发现一本重复书(和左手的相同)→ 跳过不看
整理完,左手指到哪,哪就是最后一本不重复的书。
这就是快慢指针——数组版”有序去重”的完美模型。
引导式推导:快慢指针的发明
前提:数组已排序
排序意味着重复元素必然相邻出现。这是 O(n) 快慢指针能工作的前提。
如果数组未排序呢? 必须用哈希表记录已出现的数,空间 O(n)。
慢指针 slow:指向已去重区域的最后一个位置(初始为 0)
快指针 fast:探路,从 1 开始扫描
graph TD subgraph 示例: [0,0,1,1,1,2,2,3,3,4] A["初始: slow=0, fast=1"] B["nums[1]==nums[0] → fast++"] C["nums[2]!=nums[0] → slow=1, 写入 nums[1]=nums[2]=1"] D["nums[3]==nums[1] → fast++"] E["nums[4]==nums[1] → fast++"] F["nums[5]!=nums[1] → slow=2, 写入 nums[2]=nums[5]=2"] G["继续...直到 fast 扫完"] H["返回 slow+1 = 5"] end A --> B --> C --> D --> E --> F --> G --> H
核心规则
nums[fast] == nums[slow] → 重复,跳过
nums[fast] != nums[slow] → 新元素,slow++ 后写入
代码
def removeDuplicates(self, nums):
if not nums:
return 0
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]: # 发现新元素
slow += 1 # 去重区域扩展一位
nums[slow] = nums[fast] # 写入新元素
return slow + 1 # slow 是下标,长度 = slow + 1
复杂度
| 指标 | 值 | 解释 |
|---|---|---|
| 时间 | O(n) | 一次线性遍历 |
| 空间 | O(1) | 原地修改,无额外空间 |
| 哈希表法 | O(n) | 空间 O(n),未排序时使用 |
实战考量
频率分析
出现在:字节/阿里 一面,快慢指针入门题。这道题用来考察原地修改数组的基本功——能不能在 O(1) 空间内完成去重。
延伸思考
Q:如果最多允许重复两次呢?
A:比较 nums[fast] 与 nums[slow-1](而不是 nums[slow])。因为最多保留两个,新元素只要和倒数第二个不同就可以写入。详见 80 题”删除有序数组中的重复项 II”。
Q:如果数组未排序呢?
A:必须用哈希表记录已出现的数,空间 O(n),时间 O(n)。
Q:删除所有值为 val 的元素呢?
A:同款快慢指针,if nums[fast] != val 才写入。详见 283移动零(把 0 移到末尾本质相同)。
Q:返回 slow 还是 slow+1?
A:slow 是下标,去重区域长度 = 最后一个下标 + 1 = slow + 1。fast=1 时 slow=0,如果只有一个元素,循环不执行但返回 1,正确。
易错点
-
返回
slow + 1不是slow -
fast 从 1 开始(第一个元素天然在去重区域)
-
别忘了判空
-
前提是数组已排序,否则快慢指针不成立
生活类比
有序去重 → 快慢指针 → 原地修改
像你在超市清点货架。你从第一个商品开始往后扫(快指针),手里拿着”当前商品”的样本(慢指针)。
看到一样的→跳过(没必要多放一瓶)。
看到不一样的→把样本往前推一位,把新商品放过来。
扫完整个货架,前几个位置就是每种商品各一个的完美陈列。
多用”慢指针标记位置”,少用”额外空间存数据”。
相关题目
| 题目 | 关系 |
|---|---|
| 283移动零 | 同族原地操作,快慢指针模式,把 0 移到末尾 |
| 80. 删除有序数组中的重复项 II | 进阶版,最多保留两个重复元素(wikilink 待建) |
| 26删除有序数组中的重复项 | 本题,快慢指针入门 |
→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)
速记卡(面试闪卡)
Q1:一句话讲清「26. 删除有序数组中的重复项」到底是什么?
A:用快慢指针原地删掉有序数组里的重复元素,只留一个,返回新长度。
Q2:题目 —— 怎么理解?
A:像清点货架:给你一排按顺序排列的书,要把重复书名抽走只留一本,再把剩下的往左推紧。要求原地修改、不用额外数组,返回去重后的新长度。前提数组已排序,重复元素必然相邻。
Q3:思路 —— 怎么理解?
A:像整理书架:左手放第一本书(慢指针 slow),右手从第二本往后扫(快指针 fast)。右手看到新书(和左手不同)→ 左手前移一位、把新书插过来;看到重复书→跳过。扫完左手指到的就是最后一本不重复的,去重区域长度 = slow+1。
Q4:代码 —— 怎么理解?
A:慢指针 slow 从 0 起,fast 从 1 遍历:nums[fast]!=nums[slow] 时 slow++ 再把 nums[fast] 写入 nums[slow];否则跳过。最后返回 slow+1。核心就一句:发现新元素就扩展去重区并写入。
Q5:复杂度 —— 怎么理解?
A:像走一趟长廊:时间 O(n) 一次线性遍历;空间 O(1) 原地修改无额外空间。若数组没排序,就得用哈希表记录已出现的数的,时间仍 O(n) 但空间变 O(n)。
Q6:核心速记主线有哪些?
-
前提:数组已排序,重复必相邻,快慢指针才成立
-
慢指针 slow 标记去重区末尾,快指针 fast 探路
-
返回 slow+1(slow 是下标,长度要 +1),fast 从 1 起
-
易错:忘了判空、忘了 slow+1、未排序不能用此法
口诀
A:有序去重慢快指,
新书写入旧跳过;
返回长度 slow 加一,
数组已排是前提。