目录
删除元素的万能方法
构造虚拟头结点来应对删除链表头结点的情况
一、203.移除链表元素
题目
题解
二、19.删除链表中倒数第K个节点
题目
题解
三、
83.删除某个升序链表中的重复元素,使重复的元素都只出现一次
题目
题解
82.删除某个升序链表中的重复元素且不保留,即只含原始链表中没有重复出现过的元素
题目
题解
删除元素的万能方法
删除元素的万能方法,就是找到目标节点,保存目标节点的前驱节点的引用,让前驱结点的next,指向目标节点的后继节点。这样,没有目标节点的引用,它将被gc回收。
- 这里cur是前驱节点的引用,cur.next指向目标节点。删除元素步骤如下:首先我们在题目中通过各种判断,确定cur.next指向的,就是我们需要删除的目标节点 。然后,把cur.next.next赋值给cur.next。这也就是说,让cur.next也指向后继结点。
- 此时前驱结点的next已经指向了后继结点。那原来的目标节点呢?已经不连接在链表中了,并因其没有引用变量指向,将会被gc回收。
但这个时候有些细心的小伙伴可能会有疑问:你这删除是讲清楚了,但是我发现这样的删除需要有前驱节点和后继节点,那如果是删除链表头结点(没有前驱结点)或是删除链表尾结点(没有后继结点),那怎么办呢?哈哈,先告诉结论:链表尾结点的状况,上述做法可以包含进来。而链表头结点,要么分情况讨论,要么使用我们接下来使用的方法:建立虚拟链表头结点。
构造虚拟头结点来应对删除链表头结点的情况
在上文中我们得出尾结点是包含在cur.next = cur.next.next中的。但是对于头结点就不好使了。为什么呢?因为如果后继结点是null,那还可以把null赋给一个引用,但是如果前驱结点cur为null,那么cur.next就会直接抛出一个异常了!那我们怎么办呢?第一种方法是if-else判断,如果删除元素是头结点就直接返回head.next。但是这样写的话代码比较乱,这里介绍一种更好的办法:构造虚拟头结点。这是啥意思呢?所谓虚拟头结点,就是在原链表的头结点前方接上一个节点。这样,原链表的所有结点在新链表中都不是头结点了,自然就可以沿用上面的赋值式。
如图,先创建虚拟头结点temp(值设为-1),然后让它的next指向原链表头结点,然后让cur初始化为temp(cur = temp),这样cur就永不为null,删除节点永远有前驱节点,cur.next = cur.next.next就可以继续使用。
这样我们就成功用cur.next = cur.next.next完成了头结点的删除。
需要注意的是,此方法到最后,返回删除元素后的新链表的头结点时,应该返回的是temp.next,而不是temp。
一、203.移除链表元素
题目
给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点 。
题解
通过在头节点前增加虚拟头节点(哨兵),头结点则变为普通结点,则不需要判断头节点是否为待删除的节点,但是在返回的时候需要返回的是虚拟头节点的下一节点而不是虚拟头节点。
public class ListNode {
public int val;
public ListNode next;
}
public static ListNode deleteListNode(ListNode head,int val){
//删除所有val结点,删除节点的方式是找到前驱节点cur,让cur.next = cur.next.next
ListNode temp = new ListNode(-1);
temp.next = head;
ListNode cur = temp;//构造了带虚拟头结点的链表并初始化cur
//用cur.next.val判断是因为cur.next.val == val判断cur.next是否为目标节点,
//这样cur就是前驱结点的引用了。我们在删除节点时必须考虑到保留前驱结点引用。
while (cur.next != null){
if (cur.next.val == val){
cur.next = cur.next.next;//删除目标节点。cur不移动,继续与下一个节点比较
}else {
cur = cur.next;//如果当前cur.next不符合删除要求,cur 向链表后方移动一次再次比较
}
}
return temp.next;
}
二、19.删除链表中倒数第K个节点
题目
给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。提示: 链表中结点的数目为 sz 1 <= sz <= 30 0 <= Node.val <= 100 1 <= n <= sz
题解
这里我们使用双指针法,先让快慢指针步差为K+1,然后快慢指针同步移动,这样当快指针指向链表末尾的null时(你也可以理解成链表最后一个元素是倒数第1节点,最后一个元素的null指针域是倒数第0节点),即快指针指向倒数第0个节点时,因为步差K+1,所以这时慢指针指向倒数第K+1个节点,也就是前驱节点。不知道你发现没有,2)题这类问题的核心,就是在于寻找删除节点的前驱结点。
然后按部就班的删除即可。
public static ListNode deleteLastKByTwoPointers(ListNode head,int k){
ListNode temp = new ListNode(-1);
temp.next = head;
ListNode slow = temp;
ListNode fast = temp;
//初始化,虚拟头结点构造,快慢指针指向虚拟头结点
//fast先走k+1步
for (int i = 0; i < k+1; i++) {
fast = fast.next;
}
while (fast != null){
fast = fast.next;
slow = slow.next;
}//同步前进直到fast为null
slow.next = slow.next.next;//删除待删除的节点
return temp.next;
}
三、
这类问题的关键在于你的cur引用是指向第一个重复元素,还是指向第一个重复元素的前驱结点。
(1)如果cur指向第一个重复元素,那么把cur.val和cur.next.val比较,如果相等就删除cur.next,这样就删除了所有和cur.val相同值的节点,但还留下了一个cur节点。这样会留下一个重复节点,即LeetCode83。
- cur.val==cur.next.val ?删除cur.next:cur向后移动
- 最后重复元素留下一个节点0
(2)如果cur指向第一个重复元素的前驱节点,那么把cur.next.val和cur.next.next.val比较,如果相等就存下重复元素的值(6),然后cur.next.val逐个与存储值比较,是就删除cur.next,这样就删除了所有重复结点。这样重复值节点一个也不会留下,即LeetCode82。
- 如果cur.next.val==repeatData,循环删除直到没有repeatData节点为止
83.删除某个升序链表中的重复元素,使重复的元素都只出现一次
题目
给定一个已排序的链表的头
head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。提示: 链表中节点数目在范围 [0, 300] 内 -100 <= Node.val <= 100 题目数据保证链表已经按升序 排列
题解
由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,这个很关键。因此我们只需要对链表进行一次遍历,就可以删除重复的元素。
public static ListNode deleteRepeatAndSaveOne(ListNode head){
if (head == null){
return null;
}
//这题不用构建虚拟头结点
ListNode cur = head;
while (cur.next != null){
//逐个逐个比较直到后继结点为null
if (cur.val == cur.next.val){
//如果值相同,则删除cur.next, cur不移动,和下一个cur.next继续比较
cur.next = cur.next.next;
}else {
cur = cur.next;//如果值不同,则移动cur至后继结点,开始下一轮比较
}
}
return head;
}
82.删除某个升序链表中的重复元素且不保留,即只含原始链表中没有重复出现过的元素
题目
给定一个已排序的链表的头
head
, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。提示: 链表中节点数目在范围 [0, 300] 内 -100 <= Node.val <= 100 题目数据保证链表已经按升序 排列
题解
这道题和上一道题类似83. 删除排序链表中的重复元素,但是这里要求的是删除所有重复的链表节点,而上一题可以保留一个。
分析一下这道题,删除所有重复的节点,加入有两个节点被删除,例如[1,2,2,3],那么在进行对比的时候需要保留一个节点在1的位置,寻找重复的两个指针一个左边的在第一个2,右边的第一次在第二个2,第二次在3。这个时候我们需要把1->3连接起来。
流程如下:
- 一边遍历、一边统计相邻节点的值是否相等,如果值相等就继续后移找到值不等的位置,然后删除值相等的这个区间。
- 设置一个虚拟节点,将虚拟头节点和原链表头节点连接起来
- 从虚拟头节点位置开始访问
- 只要当前访问节点的下一个节点与下下个节点都存在,就继续访问下去
- 在访问过程中,如果下一个节点与下下个节点相同,那么说明与这个节点值相同的所有节点都应该被删除掉。
- 删除的方法是先记录这个值,利用 while 循环,不断的查找出那些相同的节点值来,每次找到了一个相同的值,那么当前访问的节点 cur 就越过这个节点。
- 在访问过程中,下一个节点与下下个节点不相同,说明 cur 可以加入到最终的结果链表中,那么继续访问后面的节点。
代码如下:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
// 一开始设置一个虚拟节点,它的值为 -1,它的值可以设置为任何的数,因为我们根本不需要使用它的值
ListNode dummy = new ListNode(-1);
// 将虚拟头节点和原链表头节点连接起来
// 添加虚拟头节点之后,原链表的每个节点的地位都是一样的
dummy.next = head;
// 从虚拟头节点位置开始访问
ListNode cur = dummy;
// 只要当前访问节点的下一个节点与下下个节点都存在,就继续访问下去
while (cur.next != null && cur.next.next != null) {
// 在访问过程中,会出现两种情况
// 1、下一个节点与下下个节点相同,那么说明与这个节点值相同的所有节点都应该被删除掉
if (cur.next.val == cur.next.next.val) {
// 删除的方法是先记录这个值
int value = cur.next.val;
// 利用 while 循环,不断的查找出那些相同的节点值来
while (cur.next != null && cur.next.val == value) {
// 每次找到了一个相同的值,那么当前访问的节点 cur 就越过这个节点
cur.next = cur.next.next;
}
// 2、下一个节点与下下个节点不相同,说明 cur 可以加入到最终的结果链表中
} else {
// 那么继续访问后面的节点
cur = cur.next;
}
}
// 最终返回虚拟头节点的下一个节点就行了
return dummy.next;
}
}
我们发现,其实单链表删除都是一样的套路。找好前驱节点,明晰删除条件,遍历小心谨慎,脑中不断构建图,其实单链表删除真的不难。