今天来看 Leetcode 上的一道颇有意思的题目 —— 80. 删除有序数组中的重复项 II。这道题可是被打上了 “中等” 的标签,看似平平无奇,实则暗藏玄机😏。
题目要求我们对一个有序数组 nums 进行操作,需要原地删除重复出现的元素,而且要保证出现次数超过两次的元素只出现两次 ,最后返回删除后数组的新长度。特别强调了不要使用额外的数组空间,必须在原地修改输入数组并且使用 O(1) 额外空间的条件下完成。这就像是游戏里给你设了一堆限制条件,让你在有限的资源里完成任务,是不是感觉挑战来了🧐?
举个例子,输入 nums = [1,1,1,2,2,3],输出应该是 5,并且原数组的前五个元素被修改为 [1,1,2,2,3] 。这里 1 出现了 3 次,我们得把它删减到只出现 2 次,其他元素保持不变 ,最终返回新数组的有效长度。 这种对有序数组的操作,在实际应用中也很常见,比如数据去重、数据整理等场景,所以掌握这个技能还是很有必要的~
代码亮相:见证奇迹的时刻 🚀
接下来,就是见证奇迹的时刻啦!看看这位大佬解决这个问题的代码👇
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int p = 0;
int prev = -10001;
int cnt = 1;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == prev) {
cnt++;
if (cnt > 2) {
continue;
} else {
nums[p++] = prev;
}
} else {
prev = nums[i];
nums[p++] = nums[i];
cnt = 1;
}
}
return p;
}
};
让我们逐行来 “拆解” 这段代码 ,看看它到底是怎么做到在有序数组里精准去重的。
- int p = 0; :这里的 p 就像是一个 “标记小卫士” ,它标记着当前可以放置新元素的位置,初始位置在数组的开头,也就是索引为 0 的地方 ,从这里开始,它将一步步见证数组的变化。
- int prev = -10001; :prev 可以说是 “上一个数字小助手” ,用来记录上一个处理过的数字 ,初始值设为 -10001,这个值是一个比较特殊的 “哨兵值” ,因为题目中没有说数组元素的范围,我们选一个大概率不会在数组中出现的值,这样在处理第一个元素时就不会因为比较而出错 ,主打一个未雨绸缪。
- int cnt = 1; :cnt 是一个计数器 ,用来统计当前数字出现的次数 ,初始化为 1 ,因为我们刚开始处理第一个元素 ,它自己就占了一次嘛,就像你新认识一个朋友,他出现的次数肯定是 1 啦 。
- for (int i = 0; i < nums.size(); i++) :这是一个循环,就像一个 “遍历小火车” ,从数组的开头(索引 0)出发,一直开到数组的末尾,每个元素都会被它 “拜访” 到 ,它的使命就是一个一个地处理数组里的元素 ,不放过任何一个角落。
- if (nums[i] == prev) :这是一个判断语句 ,当 “遍历小火车” 到达的当前元素 nums[i] 和 “上一个数字小助手” 记录的 prev 相等时 ,说明遇到重复元素啦 ,就好比你在排队,发现前面一个人和你穿一样的衣服,那就知道你们 “撞衫”(重复)了。
- cnt++; :既然发现重复元素 ,那计数器 cnt 就加 1 ,统计一下这个重复元素出现了几次 ,就像你发现和你 “撞衫” 的人越来越多,你就可以数一数到底有几个和你穿一样衣服的。
- if (cnt > 2) :如果这个元素出现的次数超过了 2 次 ,那就用 continue 跳过这次循环 ,意思就是这个元素太多了,我们不想要了 ,就好比你发现排队的人里面和你 “撞衫” 的已经超过两个了,你就不想和他们一起玩了,直接跳过他们 。
- else :如果出现次数没超过 2 次 ,那就把这个元素放到 “标记小卫士” p 标记的位置 ,然后 p 向前走一步(p++) ,意思是这个元素我们要留下 ,放在新数组的合适位置 ,就像你觉得和你 “撞衫” 的人还在你接受范围内,就把他们拉到你的小队伍里 。
- else :当 nums[i] 和 prev 不相等时 ,说明遇到了新的元素 ,那就更新 “上一个数字小助手” prev 为当前元素 nums[i] ,然后把这个新元素放到 p 标记的位置 ,p 再向前走一步 ,计数器 cnt 重置为 1 ,因为这是一个新元素,它出现的次数又从 1 开始啦 ,就像你在排队时发现前面一个人穿的衣服和你不一样,你就把他当作新朋友,记录下来,把他拉进你的队伍,重新开始统计他出现的次数 。
- return p; :最后 ,循环结束后 ,“标记小卫士” p 所在的位置就是新数组的长度 ,直接返回它就大功告成啦 ,就像你排完队,数一下队伍里有多少人,这个人数就是新队伍的长度 。
这段代码逻辑清晰,通过几个简单的变量和循环,就巧妙地完成了有序数组的去重任务 ,是不是很厉害 !
思路剖析:一步步揭开去重的神秘面纱
初始化变量:准备战斗!
在这场去重大战中,我们先派出三个 “小帮手”:p、prev 和 cnt 。
- p 这个 “标记小卫士”,它站在数组的起始位置 ,时刻准备标记新元素该去的地方 ,就像你在整理书架时,先把第一个空位标记好,准备放书一样 。
- prev 这个 “上一个数字小助手” ,初始值设为 -10001 ,这个特殊的值就像是一个 “陌生访客” ,确保在处理第一个元素时不会捣乱 ,等真正的元素来了,它就开始记录上一个数字,帮助我们判断当前数字是否重复 。
- cnt 作为 “重复次数小计数器” ,一开始就把自己设为 1 ,因为第一个元素自己就占了一次出现机会嘛 ,就好比你吃第一口蛋糕,这就算一次品尝啦 。
遍历数组:开启去重之旅
接着,“遍历小火车” for (int i = 0; i < nums.size(); i++) 出发了 ,它沿着数组轨道一路前进 ,每到一个站点(元素)就停下来检查一番 。
当它发现当前元素 nums[i] 和 “上一个数字小助手” prev 是 “双胞胎”(相等)时 ,就知道遇到重复元素了 。这时候 “重复次数小计数器” cnt 赶紧自增 1 ,统计一下这个重复元素出现的次数 。
如果这个重复元素太 “嚣张”,出现次数超过了 2 次 ,我们就直接让 “遍历小火车” 跳过这个站点(continue) ,把这个多余的重复元素 “扔” 掉 ,就像你在整理玩具时,发现某个玩具太多了,就把多出来的扔掉 。
要是重复次数还在我们能接受的范围内(不超过 2 次) ,我们就把这个元素 “收留” ,让它到 “标记小卫士” p 标记的位置去 ,然后 p 向前走一步(p++) ,继续标记下一个可以放置元素的位置 ,就像你整理书架时,把重复但还需要的书放在标记好的位置,然后准备标记下一个位置 。
处理非重复元素:新元素登场
当 “遍历小火车” 发现当前元素 nums[i] 和 prev 不是 “一家人”(不相等)时 ,就知道遇到新元素啦 。这时候 “上一个数字小助手” prev 马上更新为当前元素 nums[i] ,记住这个新面孔 。
然后把这个新元素送到 “标记小卫士” p 标记的位置 ,p 再向前走一步 ,同时 “重复次数小计数器” cnt 重置为 1 ,因为这是一个新元素,它的出现次数又要重新开始统计啦 ,就像你交了新朋友,要重新了解他出现的频率一样 。
复杂度分析:代码效率大揭秘
分析完代码的逻辑和思路,我们再来看看这段代码的复杂度,它决定了代码在不同规模数据下的运行效率。就好比你跑步,不同的速度(时间复杂度)和体力消耗(空间复杂度)会影响你能跑多快、跑多远。
时间复杂度:
这段代码的时间复杂度为 O(n) ,其中 n 是数组 nums 的长度。这是因为我们使用了一个 for 循环来遍历数组,循环的次数与数组的长度成正比 ,每一个元素都要被检查一次,所以时间复杂度是线性的 。就像你要数一排座位上有多少人,你得一个一个数过去,人数越多,你数的时间就越长 ,数人的次数和总人数就是线性关系 。
空间复杂度:
空间复杂度为 O(1) ,这意味着无论数组有多大,我们只使用了固定的额外空间 。代码中只定义了几个变量 p、prev 和 cnt ,它们占用的空间不会随着数组规模的增大而增加 ,就像你出门只带固定数量的东西,不管你要去的地方是大是小,你带的东西数量都不变 。
总的来说,这段代码在时间和空间复杂度上表现都很不错 ,在处理大规模数据时也能高效运行 ,是解决这个问题的一个很好的方案 !
总结升华:收获满满
通过这道题,我们掌握了一种巧妙的数组去重技巧 ,利用几个简单的变量和一次遍历就解决了看似复杂的问题 。这种解题思路和方法在很多类似的数组操作题目中都能派上用场 ,大家一定要好好消化吸收 。
如果觉得还不过瘾,不妨去试试 LeetCode 上的26. 删除有序数组中的重复项 ,这道题和我们今天讲的题目类似,不过它要求每个元素只出现一次 ,难度稍微降低了一些 ,就当是巩固练习啦 ,相信大家都能轻松拿下 !做完之后记得回来评论区分享你的解题思路和心得,我们一起交流进步 👀 。
好啦,今天的分享就到这里啦 ,希望大家在算法的世界里玩得开心 ,收获满满 !下次再见咯 👋 。