写在前面:
本文争取举一反三,让读者从这一题就能收获到做大多数mid及以下难度链表题应有的技巧、心理素质。
这题看起来容易,但代码实操起来还是有很多要注意的地方的,这就提醒我们要脚踏实地,不能眼高手低。思路方面的没问题了,还要继续缩短答题时间,优化简洁代码,模拟面试/笔试情境。本题第一次做是在今年的3月,翻看了一下当时的做题记录,感慨当时还是见的题型少了、经验不足、不会有目的地设置辅助指针变量、一味地分情况讨论,结果把自己成功地绕了进去,跳进了各种内存报错又肉眼无法轻易纠正的大坑中了,如下所示:
插一嘴。之前准备了很多的素材和笔记,手抄版一览:创作不易,你的支持将给我带来莫大的动力
接下来正文继续:
是不是眼熟得不能再熟的报错?额......
//典型的反面教材
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head)
{
ListNode *p=head,*prev= new ListNode(-1,head);
if(p)
{
while(p->next)
{
if(head&&head->next)
{
if((head->val==head->next->val)&&(p==head))
{
while(p->val==p->next->val)
p=p->next;
prev->next=p->next;
head=prev->next;
}
else if(p->val==p->next->val&&p!=head)
{
while(p->val==p->next->val)
p=p->next;
prev->next=p->next;
p=p->next;
}
else if(p->val!=p->next->val)
{
p=p->next;
prev=prev->next;
}
}
}
}
return head;
}
};
这是不是很多新同学初学初写链表题(以及各种算法题都喜欢在线和暴力)的常态鸭?心疼得不能再疼的if...else分类... 痛啊!
当时对照着题解,理顺了自己写本题时的逻辑,做了很长篇幅的笔记。但现实是,一段时间后,当你把笔记上写的注解啥的忘光时,助力你前行的是之前的踩坑和调试经验。
对以上代码的总结如下:
- 划分条件太多,易陷入未定义行为的坑,对于大多数案例可能不能发现程序的bug,但程序的健壮、安全性有待提升。
- “head->val==head->next->val&&p==head”,&&符号前半句本意是好的,只要head、head->next满足条件就触发继续向后寻找;后半句是为了控制相同元素出现在表头和表中位置的不同解决办法。
- 分了许多复杂的细节条件的根本原因是:选错了移动指针变量的初始位置和移动规则。以上述代码所设置的prev、p指针的形式来看,必须要分为 相同元素在 表头 表中 表为空三种情况。
问题描述:
给定一个已排序的链表的头 head
, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。(短短一行话,但难度为中等)
示例 1:
输入:head = [1,2,3,3,4,4,5] 输出:[1,2,5]
示例 2:
输入:head = [1,1,1,2,3] 输出:[2,3]
插嘴一句,是不是很多小伙伴看了第一个案例灵感就立马来了,一顿哐哧哐哧埋头捣鼓之后,一看哎呀,第二个案例所示情况没有考虑到!然后像打补丁似的修改代码......确实呀,初学的话,也没有啥神仙办法能让你写每一题都顾全大局、滴水不漏的,建议就是多刷题积累经验。
思路分析:
我会逐步分析自己的思路由萌芽到成熟完善的过程,以及破冰的历程。
浅浅一想:万一相同元素出现在表头怎么办?肯定要把head指向的元素删掉的。需要一个辅助的头结点。
其次,哑结点这基操之前应该是见过的。什么?布吉岛?那你现在知道了。总之,不管三七二十一new一个哑结点是不是心里踏实很多呢(笑)。
再者,想一想,我们肯定是需要一前一后两个节点的,前一个节点指向当前遍历到的元素,后一个节点即为前节点的前驱,若遇到了相同元素需要删除时,那么后节点所记录的前驱就派上用场了。这也表明了哑结点出现的必要性。
那么相信你能写出以下大致框架:
//错误演示
class Solution {
public:
//inf无穷大的作用就是保证哑结点和第一个节点值不同,取别的值也可以,此处随意。
const int inf = 0x7fffffff;
ListNode* deleteDuplicates(ListNode* head) {
/*注释掉的内容为写到后面时发现需要更多的前驱、前前驱、前前前驱来记录上、上上、上上上一步的节点(恨不得把链表改为双向链表哈哈)
基本上当你写到后面的else语句内,你就会发现采取这种形式是走不通的
ListNode *pppre = new ListNode(inf);
ListNode *ppre = new ListNode(inf);
*/
ListNode *pre = new ListNode(inf);
ListNode *p = head;
/*
pppre->next = ppre;
ppre->next = pre;
*/
pre->next = p;
while(p){
if(p->val!=pre->val){
/*
pppre = pppre->next;
ppre = ppre->next;
*/
pre = pre->next;
p = p->next;
}else{
......
关键的来了:p=p->next
但是pre、ppre、pppre全都要向后退!
(若pre向后退,则需要ppre记录pre前驱...循环往复)
是不是ppppre就要呼之欲出了?
stop打住!
}
}
......
}
};
升级一下思路:此时我们已经无形间达成了三个共识——
- 舍得用变量,千万别想着节省变量,否则容易被逻辑绕晕;
- head 有可能需要改动时,先增加一个 假head,返回的时候直接取 假head.next,这样就不需要为修改 head 增加一大堆逻辑了;
- 往往没有思路的时候,不妨试着走一步看一步;
刚刚我们不断地向后添加着前继节点,结果陷入了无底洞,那么不妨每次循环时向链表前面多探索一点?这样可以把后面的节点提前拿到这一步循环来处理,是不是更踏实许多!于是关键的节点出场了:p->next;
//错误演示
class Solution {
public:
const int inf = 0x7fffffff;
ListNode* deleteDuplicates(ListNode* head) {
ListNode *pre = new ListNode(inf);
ListNode *p = head;
pre->next = p;
//注意while循环里的条件,别忘了p也可能为空!
while(p&&p->next){
if(p->next->val!=p->val){
pre = pre->next;
p = p->next;
}else{
//能写出此框架就代表当前工作的重心已经转移到了
//为了写出更合理更普适的处理相同元素的算法上
...
详见代码外讲解
}
}
return head;
}
};
接下来就是处理相同元素的else函数体中该怎么写?
p = p->next->next; pre->next = p;
问题还是没有考虑周全,对于1->2->3->3->4->5凑巧合适,但是类似1->2->3->3->3->4->4->4->5这种结构(相邻的相同元素>=3时)中,每组相同元素的最后一个元素均不会被删去!再优化!
很快就能想到:上述代码中的p移动的程度不够!应该把移动的过程也写在一个while循环里!直到元素不再相同时p才停止移动,pre依然在原地保持不动。
//这个now设置地很有灵性,虽然没啥用,但已经有了标答的雏形
int now = p->val;
//注意while中的p->next也有可能为空!
while(p->next&&p->next->val==now){
p = p->next;
}
//还要再移动一步才指向第一个与前面不同的元素
p = p->next;
//pre还在原地没动,修改它的后继,把中间的相同元素节点遮盖住
//(节点释放的问题需跟面试官讲清楚)
pre->next = p;
基本上到这就已经成功了一半啦,最后AC,代码见下一个模块
官方题解:
我们用指针 cur 指向链表的哑节点,随后开始对链表进行遍历。如果当前 cur->next(相当于p)与cur->next->next(相当于p->next)对应的元素相同,那么我们就需要将 cur.next 以及所有后面拥有相同元素值的链表节点全部删除。我们记下这个元素值 now,随后不断将 cur.next 从链表中移除,直到 cur.next 为空节点或者其元素值不等于 x 为止。此时,我们将链表中所有元素值为 x 的节点全部删除。(到这,是不是和我的解法一模一样鸭)
如果当前 cur.next 与 cur.next.next 对应的元素不相同,那么说明链表中只有一个元素值为 cur.next 的节点,那么我们就可以将 cur 指向 cur.next。
当遍历完整个链表之后,我们返回链表的的哑节点的下一个节点 dummy.next (相当于ppre->next)即可。
参考代码:
自解(已通过可运行):
class Solution {
public:
const int inf = 0x7fffffff;
ListNode* deleteDuplicates(ListNode* head) {
ListNode *pre = new ListNode(inf);
ListNode *ppre = pre;
ListNode *p = head;
pre->next = p;
while(p&&p->next){
if(p->next->val!=p->val){
pre = pre->next;
p = p->next;
}else{
while(p->next&&p->next->val==p->val){
p = p->next;
}
p = p->next;
pre->next = p;
}
}
return ppre->next;
}
};
官解:条理逻辑非常清晰,值得学习
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (!head) {
return head;
}
ListNode* dummy = new ListNode(0, head);
ListNode* cur = dummy;
while (cur->next && cur->next->next) {
if (cur->next->val == cur->next->next->val) {
int x = cur->next->val;
while (cur->next && cur->next->val == x) {
cur->next = cur->next->next;
}
}
else {
cur = cur->next;
}
}
return dummy->next;
}
};
总结:
本文不仅是一篇题解,更是一篇心路历程的分享,我一直认为,数据结构专题,需要的是总结。题目是做不完的,剥丝抽茧得到的精髓却是历久弥香的。
这题很经典,掌握本题也许并不能如你所想乱杀链表题,因为和指针这类相关的题目,有些细节之处是自身难以把控的,但我们相信,你越是坚持刷题坚持总结,你做诸类题越是得心应手,信心十足。
以这篇心法,与诸君共勉!