LeetCode 面试经典 150 题:删除有序数组中的重复项 II(最多保留 2 次 + 通用 k 次解法详解)
在 “删除有序数组中的重复项” 基础题之上,“最多保留指定次数重复元素” 是更贴近面试场景的延伸题。这道题不仅考察对数组 “原地操作” 的熟练度,更能体现对 “双指针思想” 的灵活应用 —— 从 “最多保留 1 次” 到 “最多保留 2 次”,再到 “最多保留 k 次”,核心逻辑可无缝迁移。本文将从题目解读、基础解法(最多保留 2 次)、通用解法(最多保留 k 次)三个维度展开,帮你彻底掌握这类问题的解题模板。
一、题目链接与题干解读
首先,你可以通过以下链接直接访问题目,先自行思考解题方向:
LeetCode 题目链接:80.删除有序数组中的重复项Ⅱ
题干核心信息
题目要求如下:
给你一个非严格递增排列的数组 nums,请你原地删除重复出现的元素,使每个元素最多出现两次,返回删除后数组的新长度。元素的相对顺序应该保持一致。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 len 个元素,那么 nums 的前 len 个元素应该保存最终结果,且这些元素的相对顺序和原数组一致。
不需要考虑数组中超出新长度后面的元素。
示例理解
通过两个典型示例,能更直观地理解题目要求:
- 示例 1:输入 nums = [1,1,1,2,2,3],输出 5,且删除后数组前 5 个元素为 [1,1,2,2,3]。解释:元素 “1” 出现 3 次,需删除 1 次,保留 2 次;“2” 出现 2 次,保留;“3” 出现 1 次,保留,最终新长度为 5。
- 示例 2:输入 nums = [0,0,1,1,1,1,2,3,3],输出 7,且删除后数组前 7 个元素为 [0,0,1,1,2,3,3]。解释:元素 “1” 出现 4 次,需删除 2 次,保留 2 次;其他元素均满足 “最多出现 2 次”,最终新长度为 7。
二、基础解法:最多保留 2 次重复元素
由于数组是有序的,重复元素必然相邻。要实现 “每个元素最多保留 2 次”,核心思路是:用变量k记录已处理的 “符合要求数组” 的长度,遍历数组时,判断当前元素是否与已处理数组的 “倒数第 2 个元素” 相同 —— 若不同,则保留;若相同,则跳过(因为已保留 2 次,再加入会超出限制)。
1. 变量k的核心作用
变量k依然扮演两个关键角色:
- 已处理数组的长度:k的值代表当前已筛选出的 “每个元素最多出现 2 次” 的有效元素个数;
- 有效元素的存放位置:数组前k个位置是已处理的符合要求区域,下一个有效元素需放到nums[k]的位置。
2. 关键判断条件:k < 2 或 x != nums[k-2]
为什么用这个条件?我们分两种情况拆解:
- 情况 1:k < 2(已处理数组长度小于 2):此时即使当前元素与已处理元素相同,也不会超出 “最多保留 2 次” 的限制(例如k=0时,加入第一个元素;k=1时,加入与第一个元素相同的元素,此时共 2 次,符合要求),因此直接保留当前元素。
- 情况 2:k >= 2(已处理数组长度大于等于 2):此时需判断当前元素x与已处理数组的 “倒数第 2 个元素”(即nums[k-2])是否相同 —— 若x != nums[k-2],说明当前元素加入后,该元素的出现次数不会超过 2 次(例如已处理元素为[1,1],k=2,当前元素x=2,2 != 1,加入后为[1,1,2],符合要求);若x == nums[k-2],说明已处理数组的最后 2 个元素都是x,再加入x会导致出现次数变为 3 次(例如已处理元素为[1,1],x=1,1 == 1,加入后为[1,1,1],超出限制),因此跳过。
3. 步骤拆解与示例演示
以示例 1(nums = [1,1,1,2,2,3])为例,一步步拆解整个过程:
步骤 1:初始化变量k
初始时,已处理的符合要求数组为空,因此k = 0(此时nums的前k=0个元素为空,符合要求区域尚未有元素)。
步骤 2:遍历数组,筛选有效元素
遍历数组的每个元素x(从索引 0 开始),判断是否满足k < 2或x != nums[k-2]:
- 索引 0:x = 1,k = 0 < 2 → 符合条件,将1放到nums[0],k自增 1 → k = 1;
- 索引 1:x = 1,k = 1 < 2 → 符合条件,将1放到nums[1],k自增 1 → k = 2;
- 索引 2:x = 1,k = 2 >= 2,nums[k-2] = nums[0] = 1 → x == nums[k-2](超出 2 次限制),跳过,k保持 2;
- 索引 3:x = 2,k = 2 >= 2,nums[k-2] = nums[0] = 1 → x != nums[k-2](符合要求),将2放到nums[2](此时nums变为[1,1,2,2,2,3]),k自增 1 → k = 3;
- 索引 4:x = 2,k = 3 >= 2,nums[k-2] = nums[1] = 1 → x != nums[k-2](符合要求),将2放到nums[3](此时nums变为[1,1,2,2,2,3]),k自增 1 → k = 4;
- 索引 5:x = 3,k = 4 >= 2,nums[k-2] = nums[2] = 2 → x != nums[k-2](符合要求),将3放到nums[4](此时nums变为[1,1,2,2,3,3]),k自增 1 → k = 5。
步骤 3:返回k的值
遍历结束后,k = 5,这就是删除重复项后数组的新长度。此时nums的前 5 个元素[1,1,2,2,3]完全符合 “每个元素最多出现 2 次” 的要求,与示例预期结果一致。
三、通用解法:最多保留 k 次重复元素
从 “最多保留 2 次” 扩展到 “最多保留 k 次”,核心逻辑完全可复用 —— 只需将判断条件中的 “倒数第 2 个元素” 改为 “倒数第 k 个元素” 即可。这是因为:
- 对于 “最多保留 k 次” 的需求,当已处理数组长度k_len >= k时,若当前元素与已处理数组的 “倒数第 k 个元素” 相同,说明该元素已保留 k 次,再加入会超出限制;若不同,则可保留。
1. 通用解题逻辑
假设需要 “每个元素最多保留 k 次”,解题步骤如下:
- 初始化变量k_len:k_len = 0,代表已处理的符合要求数组的长度;
- 遍历数组:对于每个元素x,判断是否满足k_len < k或x != nums[k_len - k]:
-
- 若满足:将x放到nums[k_len]的位置,k_len自增 1;
-
- 若不满足:跳过当前元素;
- 返回k_len:遍历结束后,k_len即为删除重复项后数组的新长度。
2. 示例验证(最多保留 3 次)
以数组nums = [1,1,1,1,2,2,2,3],k=3(最多保留 3 次)为例:
- 遍历过程关键步骤:
-
- k_len=0:x=1,k_len < 3 → 保留,k_len=1;
-
- k_len=1:x=1,k_len < 3 → 保留,k_len=2;
-
- k_len=2:x=1,k_len < 3 → 保留,k_len=3;
-
- k_len=3:x=1,k_len >=3,nums[3-3] = nums[0] =1 → x == nums[0](超出 3 次),跳过;
-
- k_len=3:x=2,nums[0] =1 → x !=1 → 保留,k_len=4;
-
- 后续元素以此类推,最终k_len=7,有效元素为[1,1,1,2,2,2,3],符合 “最多保留 3 次” 的要求。
四、复杂度分析
无论是 “最多保留 2 次” 还是 “最多保留 k 次”,复杂度均保持一致:
1. 时间复杂度:O (n)
- 我们只对数组nums进行了一次遍历,每个元素只被判断一次(是否符合保留条件),最多执行一次赋值操作(将有效元素放到nums[k_len]);
- 遍历的总次数为数组长度n,无嵌套循环,因此时间复杂度是线性的O(n)。
2. 空间复杂度:O (1)
- 整个过程只用到了一个额外变量k_len(或k),没有开辟新的数组、列表或其他数据结构;
- 所有操作都在原数组上完成,额外空间的使用与数组长度n、保留次数k均无关,因此空间复杂度是常数级的O(1)。
五、代码实现
以下以 Python,Java 为例(其他语言如 Java、C++ 逻辑一致,只需调整语法):
1,Python
class Solution:def removeDuplicates(self, nums: List[int]) -> int:k = 0for x in nums:if k < 2 or x != nums[k - 2]:nums[k] = xk += 1return k
2,Java
class Solution {public int removeDuplicates(int[] nums) {int k = 0;for (int x : nums) {if (k < 2 || x != nums[k - 2]) {nums[k++] = x;}}return k;}
}
你可以将上述代码复制到 LeetCode 编辑器中测试,例如输入nums = [1,1,1,2,2,3],代码会返回5,且nums前 5 个元素变为[1,1,2,2,3],完全符合题目要求。
六、总结与拓展
这道题的核心是 “利用数组有序性,通过判断当前元素与已处理数组‘倒数第 k 个元素’的关系,控制重复元素的保留次数”,本质仍是快慢双指针的应用 ——k_len(慢指针)维护符合要求的区域,遍历数组的元素(快指针)筛选有效元素,两者配合实现原地操作。
面试常见变形
在面试中,这类题目可能会有以下变形,但其核心逻辑不变:
- 要求 “每个元素最多保留 3 次”:直接使用通用解法,传入k=3;
- 要求 “删除所有重复元素,只保留不重复的元素”:本质是 “最多保留 1 次”,复用 LeetCode 26 题逻辑;
- 无序数组的 “最多保留 k 次”:需先对数组排序(时间复杂度变为O(n log n)),再套用通用解法(但需注意排序会改变元素相对顺序,需确认题目是否允许)。
掌握 “判断当前元素与已处理数组倒数第 k 个元素关系” 的核心逻辑,能帮你轻松应对所有 “原地控制重复元素保留次数” 的数组问题,形成解题模板,提升面试效率。
希望通过本文的讲解,你能不仅学会 “删除有序数组中的重复项 II” 的解法,更能掌握通用化的解题思路,做到举一反三,应对更多类似的算法题目。