力扣解题-26. 删除有序数组中的重复项
给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k。去重后,返回唯一元素的数量 k。 nums 的前 k 个元素应包含 排序后 的唯一数字。下标 k - 1 之后的剩余元素可以忽略。
判题标准: 系统会用下面的代码来测试你的题解:
int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案
int k = removeDuplicates(nums); // 调用
assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有断言都通过,那么您的题解将被 通过。
示例 1: 输入:nums = [1,1,2] 输出:2, nums = [1,2,_] 解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2: 输入:nums = [0,0,1,1,1,2,2,3,3,4] 输出:5, nums = [0,1,2,3,4,,,,,_] 解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
提示:
1 <= nums.length <= 3 * 104
-100 <= nums[i] <= 100
nums 已按 非递减 顺序排列。
第一次解答
解题思路
核心方法:哈希集合去重 + 原地覆盖,利用HashSet的“元素唯一性”特性筛选重复值,再将唯一元素依次写入数组前半部分,逻辑直观但存在额外内存开销,未利用数组“有序”的核心特性。
具体步骤:
- 初始化关键变量:
writeIndex:写指针,初始为0,记录下一个唯一元素的写入位置;sets:HashSet集合,用于存储已出现的元素,实现“去重”判断。
- 遍历数组筛选唯一元素:
- 遍历数组每个元素
value = nums[m]; - 若
sets中不包含value(说明是首次出现的唯一元素):- 将
value加入sets,标记为已出现; - 将
value写入nums[writeIndex],并将writeIndex右移一位;
- 将
- 若
sets中包含value(说明是重复元素):跳过该元素,不做任何操作。
- 遍历数组每个元素
- 返回结果:遍历完成后,
writeIndex的值即为数组中唯一元素的数量k,直接返回。
性能劣势说明
该解法耗时2ms仅击败9.05%用户,核心问题是未利用数组“有序”的特性,且引入额外内存开销:
- 时间复杂度的隐性开销:虽然遍历数组是O(n),但HashSet的
contains和add操作存在哈希冲突的可能(最坏情况O(n)),且集合的底层操作(如扩容、哈希计算)会增加额外耗时; - 空间复杂度非最优:HashSet需要存储所有唯一元素,空间复杂度为O(n),违背了“原地操作”的最优空间要求(O(1));
- 对有序数组的浪费:数组本身是“非递减排列”,重复元素必然相邻,无需通过HashSet判断,仅需对比相邻元素即可实现去重,该解法完全忽略了这一关键条件。
执行耗时:2 ms,击败了9.05% 的Java用户 内存消耗:46.03 MB,击败了56.48% 的Java用户
public int removeDuplicates(int[] nums) {
int wirteIndex=0;
Set<Integer> sets=new HashSet<>();
for(int m=0;m<nums.length;m++){
int value=nums[m];
if(!sets.contains(value)){
sets.add(value);
nums[wirteIndex]=value;
wirteIndex++;
}
}
return wirteIndex;
}
第二次解答
解题思路
核心方法:快慢双指针(相邻对比去重),充分利用数组“非递减排列,重复元素必相邻”的特性,通过快慢指针原地覆盖重复元素,时间复杂度O(n)、空间复杂度O(1),是本题的最优解法。
核心原理铺垫
数组非递减排列 → 所有重复元素必然连续出现(如[0,0,1,1]),因此只需对比当前元素与前一个元素是否相等,即可判断是否为重复值:
- 慢指针(slow):记录“下一个唯一元素的写入位置”,初始为1(数组第一个元素必然唯一,无需处理);
- 快指针(fast):遍历数组,检查每个元素是否为新的唯一值,初始为1(从第二个元素开始检查)。
具体步骤
- 边界处理:获取数组长度
n = nums.length(若n=0可直接返回0,代码中隐含处理); - 初始化双指针:
slow = 1:第一个元素(nums[0])天然唯一,慢指针从第二个位置开始写入;fast = 1:从第二个元素开始遍历检查。
- 快指针遍历数组(
while(fast < n)):- 核心判断:
nums[fast] != nums[fast-1](当前元素与前一个元素不同,说明是新的唯一元素); - 若满足条件:将
nums[fast]写入慢指针位置nums[slow],慢指针右移一位(slow++); - 无论是否满足条件,快指针均右移一位(
fast++),继续检查下一个元素。
- 核心判断:
- 返回结果:遍历完成后,
slow的值即为唯一元素的数量k(慢指针的位置等于已写入的唯一元素个数)。
核心优化逻辑说明
- 时间复杂度最优:仅一次遍历数组(O(n)),每个元素仅被快指针访问一次,无任何冗余计算,因此耗时0ms击败100%用户;
- 空间复杂度极致:仅使用两个指针变量,无额外集合/数组创建,空间复杂度O(1),完全符合“原地操作”的要求;
- 利用有序特性的关键:放弃HashSet的通用去重思路,针对性利用“重复元素相邻”的特性,仅通过相邻元素对比实现去重,消除了集合的额外开销;
- 内存表现优化:内存消耗45.91MB击败83.74%用户,核心原因是无HashSet的底层存储开销(如数组+链表/红黑树),仅使用基础变量,内存占用降至最低。
执行耗时:0 ms,击败了100.00% 的Java用户 内存消耗:45.91 MB,击败了83.74% 的Java用户
public int removeDuplicates(int[] nums) {
int n=nums.length;
int slow=1;
int fast=1;
while(fast<n){
if(nums[fast]!=nums[fast-1]){
nums[slow]=nums[fast];
slow++;
}
fast++;
}
return slow;
}
总结
- 第一次解答的“HashSet去重”是通用思路,但未利用数组“有序”的特性,引入额外内存和时间开销,性能较差;
- 第二次解答的“快慢双指针”是本题的最优解:针对性利用“重复元素相邻”的特性,通过相邻对比实现原地去重,时间/空间复杂度均达到理论最优;
- 本题的核心解题技巧:
- 有序数组的去重优先考虑“相邻对比”,而非通用的哈希去重;
- 慢指针记录写入位置,快指针负责检查,是数组原地操作的经典双指针模式;
- 无需处理k之后的元素,只需保证前k个元素为唯一值即可,简化逻辑。