今天只做了203一道题,但是做了好久好久好久。因为做的过程中反复琢磨链表这个独特的数据结构,产生了很多有厘头和无厘头的问题。好在最后还算是都把问题想明白想清楚了,看了视频和答案后又自己徒手写了三种解法。虽然看上去今天进度很低但是把这道题想明白也算是我这个小白的一大进步!慢慢来,加油!
理论基础
见代码随想录链表部分:代码随想录-链表理论基础
再补充一个链表的java代码定义:
//Definition for singly-linked list.
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
题目
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
![](https://img-blog.csdnimg.cn/img_convert/62ab2b49be64a2d0158380f69002ae1c.jpeg)
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
来源:Leetcode
初始思路
知道链表中删除一个节点其实就是将它前一个的节点的next指向它下一个节点。但是没有考虑head节点如何删除,也没有考虑何时终止迭代。
此外,一开始已知纠结于为什么本题输入的头节点head可以表示出整个链表的数值,而return head就可以返回整个链表的数值(就像head = [1,2,3,4,5]), head节点不应该是一个有data域和指针域的节点吗?后来通过咨询专业人士(我在大厂工作的学霸哥哥)后明白这只是一个输入和输出的形式。而我们这里编写的代码只是removeElements() 的部分。它通过一些其他部分的输出设置使它可以通过返回的head打印出示例中的输出结果。(光是这个问题我就想了好久呜呜呜,比较容易钻入死胡同但是明白了这些对自己今后理解题目也更有意义。)
解题方法
一个大要点:删除一个节点就必须知道它的上一个节点!!!(因为这是单链表)。只有改变上一个节点的next才能删。
方法1 迭代法(不用虚拟头)
这个方法写在虚拟头法前面,便于待会儿更好的理解虚拟头的意义。
方法1示意图如下:
![](https://img-blog.csdnimg.cn/img_convert/f61641760cf73154aed08e54468cb5a0.jpeg)
不用虚拟头法首先应对头节点单独判断,判断头节点是否需要删除。这是因为头节点没有上一个节点。
当head节点的值=val需要删除head:
while(head!= null && head.val == val){
head = head.next;
}
(用while不用if是要保证移除是持续的)
当head节点不需要删除时:
定义临时指针temp
定义临时指针temp是用来遍历链表。因为头节点head不能变,若变了到最后就无法返回head了。所以定义了一个temp指针来遍历。此时temp指向的是head节点,需要删除的点为temp指向的下一个节点。
ListNode temp = head; //临时指针temp
判断temp(此时的head)是否为null,判断需要删除的下一个节点是否为null,如果都不为null则可删除下一个节点。最后返回head。
while(temp!= null && temp.next!= null){
//第一遍写的时候没有判断temp!=null
if(temp.next.val == val){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return head;
head一直都没变,但head后面的链表变了,所以返回head就是返回的移除完元素后的链表
完整代码:
class Solution {
public ListNode removeElements(ListNode head, int val) {
while(head!= null && head.val == val){
head = head.next;
}
ListNode temp = head; //临时指针temp
while(temp!= null && temp.next!= null){
if(temp.next.val == val){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return head;
}
}
方法2 迭代法(用虚拟头)
通过方法1就会发现,由于head节点没有前一个节点,因此head需要删除的话需要单独讨论head。因此虚拟头法就是给head节点定义一个虚拟的前节点dum_head。这样就可以不用单独讨论head是否需要删除的情况,直接遍历整个链表即可。
方法2示意图如下:
![](https://img-blog.csdnimg.cn/img_convert/5db6da5911be78b83c12e5f003b87662.jpeg)
定义虚拟头dum_head
ListNode dum_head = new ListNode(); //虚拟头
dum_head.next = head;
定义临时指针temp
在本方法中,temp指向的就不再是head,而是dum_head。因为head节点是我们要判断是否移除的第一个节点,temp要在要删除的节点之前。
ListNode temp = dum_head;
开始遍历删除节点。只要删除的节点(temp.next)不为null即可持续循环。
while(temp.next!= null){
//temp指向节点的next不为null就持续遍历
if(temp.next.val == val){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return dum_head.next;
}
注意,这里最后return的是dum_head.next,而不是head。这是因为之前的head节点有可能被删除了。而dum_head.next无论何时指向的都是最终的head。
完整代码:
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dum_head = new ListNode(); //虚拟头
dum_head.next = head;
ListNode temp = dum_head; //临时指针temp从dum_head开始遍历
while(temp.next!= null){
//temp指向节点的next不为null就持续遍历
if(temp.next.val == val){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return dum_head.next;
}
}
方法3 递归法
这个方法也是让我思考了很久的一种方法,因为一直没太搞明白递归是怎么递归的。看完这一篇博文有了一些感悟:递归算法及经典例题详解。
递归:在运行的过程中调用自己。递归就是先将算法一层层递下去,再将值一层层归上来的过程。可以通过画一个递归图帮助了解。本题的递归图可以简要的画为下图所示:
![](https://img-blog.csdnimg.cn/img_convert/150de2357607c62af6031386120ec7d9.jpeg)
判断head(某个点)是否为null。若为null则直接返回head。
while(head == null){
return head;
}
若head(某个点)不为null,传递head.next。
一层层递,一层层归,最后归的是head.next。(也就是某个节点后面的节点已经全都遍历完成并把结果递上来了,下一步就判断这个节点是否需要删除就可以了)。
head.next = removeElements(head.next, val);
判断head(这个节点)是否需要删除
if(head.val == val){
//若这个点的值==val,则这个点需要删除
return head.next;
}else{
return head;
}
完整代码:
class Solution {
public ListNode removeElements(ListNode head, int val) {
//递归:在运行的过程中调用自己
while(head == null){
return head;
}
head.next = removeElements(head.next, val);
if(head.val == val){
return head.next;
}else{
return head;
}
}
}
后续任务
707.设计链表
206.反转链表