代码随想录
https://www.programmercarl.com/0203.移除链表元素.html
题目
203. 移除链表元素
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入: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
输出:[]
代码模版如下:
/**
* Definition for singly-linked list.
* class ListNode {
* val: number
* next: ListNode | null
* constructor(val?: number, next?: ListNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
* }
*/
function removeElements(head: ListNode | null, val: number): ListNode | null {
// 你的代码
};
笔记
为什么要学链表
之前要存储一个序列时,一般是用数组存储。数组的好处是查找方便,每个元素都有索引,查找第i个元素只需要O(1)的时间复杂度,缺点是新增和删除比较困难。
比如在数组中删除一个元素,需要把该元素后面的所有元素都往前提一位,同时数组长度减一。所以删除数组元素的时间复杂度可达O(n)。
那如果题目中查找操作很少,新增或删除比较频繁,比如本题中需要我们删除序列中所有值为target的数,将频繁用到删除操作,再用数组就不太合适了,更适合用新的数据结构:链表。
这里是链表的基本介绍。
不设置虚拟头节点的解法
先来讨论对于一个不设置虚拟头节点,每个节点都存有数据的链表应如何操作。
首先,如何删除链表中的一个节点?
- 删除头节点
head往后移一位,将下一位作为新的头节点。
head = head.next;
- 删除其它节点
假设current是链表中的一个节点(非头节点),previous是current的上一个节点,现要删除current,代码是:
previous = current.next
current = current.next;
或假设current是链表中的一个节点,现要删除current.next,代码是:
current.next = current.next.next;
current = current.next;
其次,如何删除链表中所有value等于target的节点?
- 首先删除所有value等于target的头节点。
需要注意,删掉第一个头节点后,新的头节点也有可能需要被删除,所以要删完所有需要被删除的头节点再进入下一步。
删完后有两种可能,要么头节点等于null了,说明链表已经被删空了,那直接返回null就行;要么头节点不等于nul且不需要被删除,那就可以进入下一步删除其它节点了。 - 然后删除所有value等于target的非头节点。
从链表第二个节点开始遍历链表,被遍历到的节点被选为【被比较节点】,将它的value与目标数比较,若两数相等则删除该节点。
需要注意,在遍历中的每一次执行中,都要做一次【把下个节点选为被比较节点】,做多了会导致有节点还没被比较就被跳过了,做少了会导致已经被比较过的节点又被再次比较。如果删除动作中已经包含这个动作(比如下面这样),就要注意不要删除完又多选一次下个节点。
current.next = current.next.next;
current = current.next; // 这一行把下个节点current.next选为被比较节点current了
【把下个节点选为被比较节点】后,判断一下新的被比较节点是否等于null,若等于null就说明链表已经被遍历完了,可以结束并返回head了,否则继续遍历。
实现代码如下:
function removeElements(head: ListNode | null, val: number): ListNode | null {
// 进入删除头节点的流程
while (head && head.val === val) head = head.next;
if (!head) return null;
// 进入删除非头节点的流程
// 设置current和prev,保证pre.next一定是current。每次判断current要不要删除
let prev = head, current = head.next;
while (current) {
if (current.val === val) {
prev.next = current.next;
} else {
prev = prev.next;
}
current = current.next;
}
return head;
};
或
function removeElements(head: ListNode | null, val: number): ListNode | null {
// 进入删除头节点的流程
while (head && head.val === val) head = head.next;
if (!head) return null;
// 进入删除非头节点的流程
// 只设置current,每次判断current.next要不要删除
let current = head;
while (current && current.next) {
if (current.next.val === val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
};
设置虚拟头节点的解法
在上一个解法中,由于链表中第一个节点没有上一个节点,所以删除链表中第一个节点和删除其它节点不一样,要单独提出来特殊处理,有点麻烦。那有没有办法统一处理所有节点呢?
答案是有的,那就是设置虚拟头节点。设置一个虚拟头节点,它可以不存放数据,只要保证它的next指向链表的首元结点就行。它的作用是代替首元结点成为链表中第一个节点,使首元结点也可以作为普通节点被处理。由于现在有两个“头节点”了,接下来我将分别称它们为“虚拟头节点”和“首元节点”,以免混淆。
设置虚拟头节点前的链表:首元节点->普通节点->普通节点->…
设置虚拟头节点后的链表:虚拟头节点->首元节点->普通节点->普通节点->…
首元结点和头结点的区别
头结点:头结点是在单链表的第一个结点之前附设的一个结点,主要用于方便操作链表。头结点的数据域通常用来保存与链表有关的信息,如链表的长度。
首元结点:首元结点是链表中存储线性表的第一个数据元素的结点,即链表的开始结点。
现在再来讨论对于一个设置了虚拟头节点dummyHead的链表应如何操作。
首先,如何删除链表中的一个节点?
- 删除头节点
虚拟头节点自身不动,next往后移一位。且之后要求返回链表头节点时返回的是dummyHead.next。
const previous = dummyHead;
const current = dummyHead.next;
previous = current.next
current = current.next;
或
const current = dummyHead;
current.next = current.next.next;
current = current.next;
- 删除其它节点(与不设置虚拟头节点一样)
previous = current.next
current = current.next;
或
current.next = current.next.next;
current = current.next;
可以看到,现在首元节点可以用和普通节点一样的方式处理了,区别只是它的上一个节点是虚拟头节点而已。
其次,如何删除链表中所有value等于target的节点?
- 首先删除所有value等于target的链表中的第一个节点。
链表中的第一个节点固定是虚拟头节点,不需要被删除也不会为null,直接进入下一步。 - 然后删除所有value等于target的其它节点。
与不设置虚拟头节点的处理方式一样,依然是从链表第二个节点开始遍历链表,只是现在链表第二个节点是首元节点咯。
实现代码如下:
function removeElements(head: ListNode | null, val: number): ListNode | null {
const dummyHead = new ListNode(0, head);
// 设置current和prev,保证pre.next一定是current。每次判断current要不要删除
let previous = dummyHead;
let current = dummyHead.next;
while (current) {
if (current.val === val) {
previous.next = current.next;
} else {
previous = previous.next;
}
current = current.next;
}
return dummyHead.next;
};
或
function removeElements(head: ListNode | null, val: number): ListNode | null {
const dummyHead = new ListNode(0, head);
// 只设置current,每次判断current.next要不要删除
let current = dummyHead;
while (current && current.next) {
if (current.next.val === val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return dummyHead.next;
};