5000字链表心法:以典题LeetCode82.删除重元(二)为例

写在前面:

        本文争取举一反三,让读者从这一题就能收获到做大多数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分类... 痛啊!

当时对照着题解,理顺了自己写本题时的逻辑,做了很长篇幅的笔记。但现实是,一段时间后,当你把笔记上写的注解啥的忘光时,助力你前行的是之前的踩坑和调试经验。

对以上代码的总结如下:

  1. 划分条件太多,易陷入未定义行为的坑,对于大多数案例可能不能发现程序的bug,但程序的健壮、安全性有待提升。
  2. “head->val==head->next->val&&p==head”,&&符号前半句本意是好的,只要head、head->next满足条件就触发继续向后寻找;后半句是为了控制相同元素出现在表头和表中位置的不同解决办法。
  3. 分了许多复杂的细节条件的根本原因是:选错了移动指针变量的初始位置和移动规则。以上述代码所设置的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打住!
            }
        }
        ......
    }
};

升级一下思路:此时我们已经无形间达成了三个共识——

  1. 舍得用变量,千万别想着节省变量,否则容易被逻辑绕晕;
  2. head 有可能需要改动时,先增加一个 假head,返回的时候直接取 假head.next,这样就不需要为修改 head 增加一大堆逻辑了;
  3. 往往没有思路的时候,不妨试着走一步看一步;

刚刚我们不断地向后添加着前继节点,结果陷入了无底洞,那么不妨每次循环时向链表前面多探索一点?这样可以把后面的节点提前拿到这一步循环来处理,是不是更踏实许多!于是关键的节点出场了: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;
    }
};

总结:

本文不仅是一篇题解,更是一篇心路历程的分享,我一直认为,数据结构专题,需要的是总结。题目是做不完的,剥丝抽茧得到的精髓却是历久弥香的。

这题很经典,掌握本题也许并不能如你所想乱杀链表题,因为和指针这类相关的题目,有些细节之处是自身难以把控的,但我们相信,你越是坚持刷题坚持总结,你做诸类题越是得心应手,信心十足。

以这篇心法,与诸君共勉!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清风微浪又何妨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值