链表
定义
class ListNode {
public val: number;
public next: ListNode|null = null;
constructor(value?: number,next?:ListNode|null) {
this.val = value;
this.next = next;
}
}
![](/assets/listNode.851729c2.png)
链表的合并
真题描述:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的
示例: 输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4
做链表处理类问题,大家要把握住一个中心思想——处理链表的本质,是处理链表结点之间的指针关系。
![](/assets/链表1.d8d122a5.webp)
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
const mergeTwoLists = function(l1, l2) {
// 定义头结点,确保链表可以被访问到
let head = new ListNode()
// cur 这里就是咱们那根“针”
let cur = head
// “针”开始在 l1 和 l2 间穿梭了
while(l1 && l2) {
// 如果 l1 的结点值较小
if(l1.val<=l2.val) {
// 先串起 l1 的结点
cur.next = l1
// l1 指针向前一步
l1 = l1.next
} else {
// l2 较小时,串起 l2 结点
cur.next = l2
// l2 向前一步
l2 = l2.next
}
// “针”在串起一个结点后,也会往前一步
cur = cur.next
}
// 处理链表不等长的情况
cur.next = l1!==null?l1:l2
// 返回起始结点
return head.next
};
移除重复链表
要深刻理解指针的概念,head 和 cur 的区别 2023/12/22 10:37真题描述:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1: 输入: 1->1->2 输出: 1->2 示例 2: 输入: 1->1->2->3->3 输出: 1->2->3
![](/assets/链表2.dbf3938b.webp)
/**
* @param {ListNode} head
* @return {ListNode}
*/
const deleteDuplicates = function(head) {
// 设定 cur 指针,初始位置为链表第一个结点
let cur = head;
// 遍历链表
while(cur != null && cur.next != null) {
// 若当前结点和它后面一个结点值相等(重复)
if(cur.val === cur.next.val) {
// 删除靠后的那个结点(去重)
cur.next = cur.next.next;
} else {
// 若不重复,继续遍历
cur = cur.next;
}
}
return head;
};
移除重复链表2
真题描述:给定一个排序链表,删除所有含有重复数字的结点,只保留原始链表中 没有重复出现的数字。
示例 1: 输入: 1->2->3->3->4->4->5 输出: 1->2->5 示例 2: 输入: 1->1->1->2->3 输出: 2->3
题干要求我们只要一个元素发生了重复,就要把它彻底从链表中干掉,一个不留。
其实在链表题中,经常会遇到这样的问题:链表的第一个结点,因为没有前驱结点,导致我们面对它无从下手。这时我们就可以用一个
dummy
结点来解决这个问题。
所谓
dummy
结点,就是咱们人为制造出来的第一个结点的前驱结点,这样链表中所有的结点都能确保有一个前驱结点,也就都能够用同样的逻辑来处理了。
我们首先要做的就是定义一个 dummy 结点,指向链表的起始位置:
![](/assets/链表3.e63211fc.webp)
这样一来,如果想要删除两个连续重复的值为 1 的结点,我们只需要把 dummy 结点的 next 指针直接指向 2:
![](/assets/链表4.f6494ef2.webp)
/**
* @param {ListNode} head
* @return {ListNode}
*/
const deleteDuplicates = function(head) {
// 极端情况:0个或1个结点,则不会重复,直接返回
if(!head || !head.next) {
return head
}
// dummy 登场
let dummy = new ListNode()
// dummy 永远指向头结点
dummy.next = head
// cur 从 dummy 开始遍历
let cur = dummy
// 当 cur 的后面有至少两个结点时
while(cur.next && cur.next.next) {
// 对 cur 后面的两个结点进行比较
if(cur.next.val === cur.next.next.val) {
// 若值重复,则记下这个值
let val = cur.next.val
// 反复地排查后面的元素是否存在多次重复该值的情况
while(cur.next && cur.next.val===val) {
// 若有,则删除
cur.next = cur.next.next
}
} else {
// 若不重复,则正常遍历
cur = cur.next
}
}
// 返回链表的起始结点
return dummy.next;
};
dummy 结点
它可以帮我们处理掉头结点为空的边界问题,帮助我们简化解题过程。因此涉及链表操作、尤其是涉及结点删除的题目(对前驱结点的存在性要求比较高),我都建议大家写代码的时候直接把 dummy 给用起来,建立好的编程习惯:
移除链表元素
题意:删除链表中等于给定值 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 输出:[]
![](/assets/removeListNode.b9a50d45.png)
使用虚拟头结点,因为头部也可能是要删除的节点
TIP
let cur = ret
,是把 cur
当做了 ret
指针,其实还是一直在修改 ret
, 只是 ret
没有移动
只有当 cur.next.val != val
才可以继续移动 cur
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
const ret = new ListNode(0, head);
// 指针
let cur = ret;
while(cur.next) {
if(cur.next.val === val) {
cur.next = cur.next.next;
continue;
}
cur = cur.next;
}
return ret.next;
};
🔗反转链表
题意:反转一个单链表。 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
![](/assets/链表5.2f480295.webp)
接下来我们需要琢磨的是如何去反转指针的指向,这里我们需要用到三个指针,它们分别指向目标结点(cur)、目标结点的前驱结点(pre)、目标结点的后继结点(next)。这里咱们随便找个结点来开刀:
![](/assets/链表6.5fb3c256.webp)
![](/assets/链表7.ecfa3137.webp)
/**
* @param {ListNode} head
* @return {ListNode}
*/
const reverseList = function(head) {
// 初始化前驱结点为 null
let pre = null;
// 初始化目标结点为头结点
let cur = head;
// 只要目标结点不为 null,遍历就得继续
while (cur !== null) {
// 记录一下 next 结点
let next = cur.next;
// 反转指针
cur.next = pre;
// pre 往前走一步
pre = cur;
// cur往前走一步
cur = next;
}
// 反转结束后,pre 就会变成新链表的头结点
return pre
};
删除链表的倒数第N个节点
题意:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
![](/assets/删除链表倒数第N个节点.8404ae6e.png)
假设链表的长度是length,要删除倒数第n个节点,也就是删除正数第length-n+1个节点。
思路
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了(用空间换时间,本质上其实就是对关键信息进行提前记忆,这里咱们相当于用两个指针对差值实现了记忆)
![](/assets/删除倒数第N个节点.21d4e3e0.png)
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
let t = new ListNode(0,head);
let slow = t
let fast = t
// 先闷头走几步
while(n--){
fast = fast.next;
}
// 然后并排走
while(fast.next!=null){
fast = fast.next;
slow = slow.next
}
slow.next = slow.next.next;
return t.next
};
链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
例1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
例2:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
思路
使用 hash 记录
var getIntersectionNode = function(headA, headB) {
const visited = new Set();
let temp = headA;
while (temp !== null) {
visited.add(temp);
temp = temp.next;
}
temp = headB;
while (temp !== null) {
if (visited.has(temp)) {
return temp;
}
temp = temp.next;
}
return null;
};
使用 双指针
var getIntersectionNode = function(headA, headB) {
if (headA === null || headB === null) {
return null;
}
let pA = headA, pB = headB;
while (pA !== pB) {
pA = pA === null ? headB : pA.next;
pB = pB === null ? headA : pB.next;
}
return pA;
};
如何判断链表是否成环
真题描述:给定一个链表,判断链表中是否有环。
示例 1: 输入:[3,2,0,4](链表结构如下图) 输出:true 解释:链表中存在一个环
![](/assets/链表8.f34f4274.webp)
从 flag 出发,只要我能够再回到 flag 处,那么就意味着,我正在遍历一个环形链表。
/**
* @param {ListNode} head
* @return {boolean}
*/
// 入参是头结点
const hasCycle = function(head) {
// 只要结点存在,那么就继续遍历
while(head){
// 如果 flag 已经立过了,那么说明环存在
if(head.flag){
return true;
}else{
// 如果 flag 没立过,就立一个 flag 再往 下走
head.flag = true;
head = head.next;
}
}
return false;
};
衍生题:
给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 null。
如果一个结点是环形链表成环的起点,那么它一定是第一个被发现 flag 标志已存在的结点:
![](/assets/链表10.e5b9cc0d.webp)
这一点不难理解,我们试想如果从头开始遍历一个链表,假如途中进入了一个环,那么首先被打上 flag 标签的其实就是环的起点。待我们遍历完这个环时,即便环上所有的结点都已经被立了 flag,但起点处的 flag 一定最先被我们定位到。因此,我们只需要在第一次发现 flag 已存在时,将对应的结点返回即可:
/**
* @param {ListNode} head
* @return {ListNode}
*/
const detectCycle = function(head) {
while(head){
if(head.flag){
// 说明是环的起点
return head;
}else{
head.flag = true;
head = head.next;
}
}
return null;
};