题目概述(简单难度)
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
示例1
输入:{1,2,3,3,4,4,5} 返回值:{1,2,5}
附上牛客网链接:
点击此处进入牛客网
思路与代码
思路展现
1:既然是删除链表中的所有重复节点,留下不重复的节点,那么就肯定要遍历链表中的所有节点了,则必须使用我们的老朋友cur变量来遍历链表了.
2:那么其余不重复的节点我们该如何存储呢?
可以建立一个新的链表来存储我们不重复的节点,最终返回我们新链表头节点的下一个节点即可,也就是我们第一个插入新链表的节点.
3:我们的思路是这样的:使用cur开始遍历我们的新的链表,同时比较我们的cur指针所指向的节点的值域与其下一个节点的的值域的大小,如果相等的话,cur就需要继续往下遍历,直至找到不相等的节点,存入我们的新的链表.
4:当然代码中包含了很多种特殊情况和细节点,在下面的代码解析中我会挨个来给大家分析,现在就先来看下我们的代码把.
代码示例
public class Solution { public ListNode deleteDuplication(ListNode pHead) { if(pHead==null){ return null; } //创建新链表的头节点 ListNode newHead=new ListNode(-1); //cur指针指向原链表的头节点pHead ListNode cur=pHead; //tmp指针指向新链表的头节点newHead ListNode tmp=newHead; while(cur!=null){ //要加cur.next!=null if((cur.next!=null)&&(cur.val==cur.next.val)){ //要加cur.next!=null while(cur.next!=null&&cur.val==cur.next.val){ cur=cur.next; } //要多走一步 cur=cur.next; }else{ tmp.next=cur; tmp=tmp.next; cur=cur.next; } } //特殊情况 tmp.next=null; return newHead.next; } }
代码解析
正常情况
如下的代码也是我们会经常写的代码,也是不考虑特殊情况的代码,所以运行起来会报错,但是很多人却不知道哪里出了问题,下面我带大家来一一剖析:
先来看一组代码,这组代码可以对应如下的图一块看:
public class Solution { public ListNode deleteDuplication(ListNode pHead) { if(pHead==null){ return null; } //创建新链表的头节点 ListNode newHead=new ListNode(-1); //cur指针指向原链表的头节点pHead ListNode cur=pHead; //tmp指针指向新链表的头节点newHead ListNode tmp=newHead; while(cur!=null){ if(cur.val==cur.next.val){ while(cur.val==cur.next.val){ cur=cur.next; } //要多走一步,这里需要注意 cur=cur.next; }else{ tmp.next=cur; tmp=tmp.next; cur=cur.next; } } return newHead.next; } }
对于图片中这种情况来说,这组代码看起来非常完美,我们先来分析下代码
1:首先判断链表是否为空,如果为空,则返回null
2:开始遍历我们的链表,然后开始判断cur指针所指向的节点的值域与其下一个节点的的值域是否相等,如果相等,cur就继续往下遍历,直至找到不相等的节点,但是这里也有需要注意的点:
就是为什么if中还要加一个while循环?
假如我们不加这个while循环,我们来看代码是怎么执行的,当cur这个指针变量走到了第三个15的时候,此时15不等于89,此时就会执行else中的语句,else中的语句是为了将不重复的节点插入到我们用于存储这些节点的新链表当中,此时就会把第三个值域为15的节点加入到我们存储不重复节点的新链表中,可以根据题意,值域为15的节点都应该算是重复节点,重复节点是应该被删掉的,怎么能存入到我们的新链表当中去呢?
所以为了防止这种情况的发生,我们在if中加入了while循环,用while循环代替if语句将重复的节点走过,当重复的这组节点的最后一个节点的值域与下一个节点的值域不相同时,就直接执行while循环外,if循环内的cur=cur.next这条语句,此时就直接跳到了下一个节点,完美避免了上述情况的发生.这个思路比较巧妙,希望大家好好思考.
3:else语句中执行的就是我们将不重复的节点插入新链表的语句,每插入一个节点,tmp和cur就继续往下遍历即可.
4:最终返回我们新链表的头节点的下一个节点,即我们插入新链表的第一个不重复的节点,返回它的原因是从这个节点以及它往后的节点组成的链表就是我们要的存储不重复节点的新链表。
经过我们的分析,感觉这个代码很完美,一运行,直接空指针异常,这是为什么?下面经过讨论几种特殊情况以及代码的完善,相信大家就会了解为什么代码运行不过了.
特殊情况1(完善第一步)
我们先来改变下原先链表节点的值域,改变后如下图所示:
这种情况的链表按照刚才的代码一定会发生空指针异常,这是为什么呢?
原因是经过上述代码执行后,我们的cur最终会来到91这个节点,此时回到while循环判断cur非空,非空后执行我们的if判断条件cur.val==cur.next.val,也就是这个判断条件发生了空指针异常,原因是cur为此时链表最后一个节点.cur.next=null.那么cur.next.val就会发生空指针异常,相当于null.val了.所以此处的if条件判断应该加上cur.next!=null这一条件,与之前的判断条件用逻辑与(&&)连接.完善后的代码如下:
public class Solution { public ListNode deleteDuplication(ListNode pHead) { if(pHead==null){ return null; } //创建新链表的头节点 ListNode newHead=new ListNode(-1); //cur指针指向原链表的头节点pHead ListNode cur=pHead; //tmp指针指向新链表的头节点newHead ListNode tmp=newHead; while(cur!=null){ //特殊情况1所加的判断条件cur.next!=null if(cur.next!=null&&cur.val==cur.next.val){ while(cur.val==cur.next.val){ cur=cur.next; } cur=cur.next; }else{ tmp.next=cur; tmp=tmp.next; cur=cur.next; } } return newHead.next; } }
你以为这就完了?当然不,经过运行,卧槽?还报空指针异常,有些小伙伴就纳闷了,看来还是有特殊情况没有考虑不到,别慌,我们继续来完善.
特殊情况2(完善第二步)
下面我们再来看第二种特殊情况,还是修改下链表中的节点的值域,如下图所示:
与特殊情况1中不同的是,我们会发现除了首节点,我们的其他节点都是重复节点,所以最终我们不重复的节点只有首节点一个
执行完特殊情况2中完善的代码后我们会发现,最终cur会一直停留在if中的while循环内,那么cur此时会走到最后一个89,回到while循环中执行cur.val==cur.next.val这条语句,也就是这个while循环条件发生了空指针异常,原因是cur为此时链表最后一个节点.cur.next=null.那么cur.next.val就会发生空指针异常,相当于null.val了,所以同样我们也要为if内的while循环加上cur.next!=null这一条件,与之前的判断条件用逻辑与(&&)连接`.完善后的代码如下:
public class Solution { public ListNode deleteDuplication(ListNode pHead) { if(pHead==null){ return null; } //创建新链表的头节点 ListNode newHead=new ListNode(-1); //cur指针指向原链表的头节点pHead ListNode cur=pHead; //tmp指针指向新链表的头节点newHead ListNode tmp=newHead; while(cur!=null){ //特殊情况1所加的判断条件cur.next!=null if(cur.next!=null&&cur.val==cur.next.val){ //特殊情况2所加的判断条件cur.next!=null while(cur.next!=null&&cur.val==cur.next.val){ cur=cur.next; } cur=cur.next; }else{ tmp.next=cur; tmp=tmp.next; cur=cur.next; } } return newHead.next; } }
完善过后总应该没问题了把,我们会发现报了如下错误:
这到底是个啥意思?来看下面的分析
特殊情况3(完善最终曲)
在特殊情况2中,有一个用例没有通过,原因是什么?
答案很简单,在特殊情况2中我们最后只有首节点插入到了新的链表中,如下图所示:
但是我们知道的是,我们首节点的next域并不是null,其保存着下一个节点的地址,所以这个首节点作为新链表的尾节点,其next域会因为不是null而报错,根据链表的标准,链表的尾节点,也就是链表的最后一个节点必须为null,这个链表才成立,所以在最后返回新链表的头节点前,还需要手动将新链表的尾节点的next域置空一次,不管它之前是否为null,都有必要执行一次tmp.next=null语句将next域置空.
再来看之前报错的截图:最终输出[2,4,5,5]的原因就是新链表中值域为4的这个节点作为链表的尾节点,其next域依然保存着原链表中值域为5的节点的地址,不为null,所以最终输出就成了[2,4,5,5],当我们最后再将新链表尾节点next域置空后,此时就成了[2,4]了.
最终代码正如之前给出的示例一样:
public class Solution { public ListNode deleteDuplication(ListNode pHead) { if(pHead==null){ return null; } //创建新链表的头节点 ListNode newHead=new ListNode(-1); //cur指针指向原链表的头节点pHead ListNode cur=pHead; //tmp指针指向新链表的头节点newHead ListNode tmp=newHead; while(cur!=null){ //特殊情况1所加的判断条件cur.next!=null if(cur.next!=null&&cur.val==cur.next.val){ //特殊情况2所加的判断条件cur.next!=null while(cur.next!=null&&cur.val==cur.next.val){ cur=cur.next; } cur=cur.next; }else{ tmp.next=cur; tmp=tmp.next; cur=cur.next; } } //特殊情况3所加代码 tmp.next=null; return newHead.next; } }
总结
这个题目需要考虑的特殊情况非常之多,需要小伙伴们下来好好研究下这个题目,只有将所有情况考虑进去,我们的代码才不会出错.