链表
「链表 linked list」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过"引用"相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。
链表由于其结构的不同可以分为:单链表、双向链表、循环链表等等,其中最基础的就是单链表了。
本章主要列举一些有关单链表的LeetCode题目,从而掌握单链表题目中的一些常用知识点和操作。
单链表
单链表中每个节点形如:
ts
type ListNode={
val:object,
next:ListNode,
}
节点与节点之间使用next
连接,由于其单向性以及存储地址的不连续性,我们在访问第i
个节点时都需要从头遍历,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),但是其增加和删除操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。
由于单链表是一个比较简单的数据结构,你可以在这里了解更多。
基础题目
LeetCode-203.移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
这道题很简单,就是删除目标结点。首先分析一下如何删除结点,如图:
我们需要删除红色节点,那么我们需要知道哪些信息? 答:要知道被删除节点的前一个节点。
需要怎么做? 答:假设要删除节点的前一个节点为cur
,只需要cur.next = cur.next.next
即可;
那如果头节点就是我们要删除的节点呢? 答:可以在删除其他节点前,用head = head.next
来删除前面连续的目标节点。
这些问题都解决了,就可以开始写代码了:
js
var removeElements = function (head, val) {
//删除前面连续的目标节点
while (head) {
if (head.val === val) {
head = head.next;
} else {
break;
}
}
//链表是否为空
if (!head) return head;
//删除后续节点
let cur = head;
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next;
continue;
}
cur = cur.next;
}
return head;
};
这样虽然AC了,但是还不够优雅,因为你需要特殊去处理头节点为目标节点的情况。
为此,我们可以统一操作,引入哨兵(sentinel)节点,如图:
这样链表中的所有节点都有了指向它的节点,这样操作更加统一,也减少了我们需要考虑的边界情况:
js
var removeElements = function (head, val) {
const sentinel = new ListNode(0, head);
let cur = sentinel;
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next;
continue;
}
cur = cur.next;
}
return sentinel.next;
};
LeetCode-206.反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
对于链表1-->2-->3-->4
经过翻转变为4-->3-->2-->1
;
对于链表题目,我十分推荐画图模拟分析:
如图所示,我们反转链表时一定需要两个节点的信息,从而让cur
指向pre
,但是你会发现,节点2和节点3断开了,我们再也没办法找到下一个节点了,所以cur
的下一个节点也是我们需要的,那么重新梳理一下流程:
如上图所示:
- 将
cur.next
存为temp
cur.next = pre
pre.next = null
- 移动到下一个位置:
pre = cur; cur = temp;
emmmm.... 步骤3中为什么指向null呢?后面的操作也需要如此嘛,梳理一下发现只有对于第一遍反转时才需要指向null,对于这种因为头节点而产生的副作用,我们都去考虑引入哨兵节点 ,如果引入了我们就不再需要步骤3了,最后只要令哨兵节点为null
即可。
但是,这里令哨兵节点为null不可行,说是链表中存在环,希望大家能够帮我解答一下👍。
因此,由于代码中不存在修改pre.next
的操作,因此直接令pre
为null
即可。
js
var reverseList = function (head) {
let pre = null;
let cur = head;
//对于只有0个或一个节点时
if (!cur || !cur.next) { return head; }
while (cur) {
let temp = cur.next;
cur.next = pre;
//更新位置
pre = cur;
cur = temp;
}
return pre;
};
LeetCode-24.两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
对于链表1-->2-->3-->4
经过翻转变为2-->1-->4-->3
; 对于链表1-->2-->3
经过翻转变为2-->1-->3
;
不说了,看图:
我们需要的节点是需要交换的两个节点的前一个节点(需要交换3,4,则需要1,3),那对于头节点又会出现边界情况,那就引入哨兵节点,代码如下:
js
var swapPairs = function (head) {
let sentinel = new ListNode(0, head);
let pre = sentinel;
let cur = pre.next;
//头节点为空
if (!cur) return head;
//根据节点数量在奇数和偶数时,最后cur指针位置不同来决定while条件
while (cur && cur.next) {
pre.next = cur.next;
cur.next = cur.next.next;
pre.next.next = cur;
//更新位置
pre = pre.next.next;
cur = pre.next;
}
return sentinel.next;
};
LeetCode-19.删除链表的倒数第 N 个结点
这道题又是一个删除链表节点的题,这道题的技巧在于如何找到链表倒数第N个节点的前驱节点。由于链表是单向的,因此我们无法回退。
但是,我们只需要两个指针,两个指针之间相距N步,那么当快指针移动到尾部时,慢指针移动到了倒数第N-1个节点了。
js
var removeNthFromEnd = function (head, n) {
let fast = head;
let slow = head;
for (let i = 0; i < n; i++) {
fast = fast.next;
}
//如果删除的是第一个元素,即倒数第size个,那么fast前移size步会指向null
if (!fast) return head.next;
while (fast.next) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return head;
};
考虑特殊情况:如果链表长度为3,要删除倒数第三个,那么我们fast
指针向前3步就会指向null
了,那么后续while循环就会报错,所以需要进行特判,又是头节点。上哨兵试试:
js
//哨兵
var removeNthFromEnd = function (head, n) {
const sentinel = new ListNode(0, head);
let fast = sentinel;
let slow = sentinel;
for (let i = 0; i < n; i++) {
fast = fast.next;
}
while (fast.next) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
//因为头节点可能被删除了,因为返回哨兵节点的next
return sentinel.next;
};
LeetCode-160.相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
假设链表A长度为 5 ,链表B长度为 6 ,那么相交点一定后五个节点中,我们只要得到两个链表的长度差offset,然后再让两个指针分别遍历短链表A的头部开始,长链表B的第offset个开始逐一比较即可。(注意此处比较的是节点是否相同,而不是节点的val
)
js
var getIntersectionNode = function (headA, headB) {
let lengthA = 0, lengthB = 0;
let cur = headA;
while (cur) {
lengthA++;
cur = cur.next;
}
cur = headB;
while (cur) {
lengthB++;
cur = cur.next;
}
let offset = Math.abs(lengthA - lengthB);
let longList, shorList;
if (lengthA > lengthB) {
longList = headA;
shorList = headB
} else {
longList = headB;
shorList = headA;
}
for (let i = 0; i < offset; i++) {
longList = longList.next;
}
while (longList && shorList) {
if (longList === shorList) return longList;
longList = longList.next;
shorList = shorList.next;
}
return null;
};
LeetCode-142.环形链表 II
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始 )。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
这个题中有两个问题:
- 判断是否有环?
- 快指针每次前移两步,慢指针每次移动一步,那么快指针一定先入环,当慢指针入环时快指针此时在环中的某个节点处,此时,快指针相对于慢指针每次前移一格,因此一定会追上慢指针,那么当快慢指针相遇时就可以判断是否有环了。
- 入口节点如何求?
- 如下图所示:
其中慢指针走了x+y
为什么不是 x+y+n(y+z)
?
因为当慢指针入环后,一定在他未走完一圈时被快指针碰到,试想,如果慢指针走了一圈,那么快指针就走了两圈,那么快指针一定在某处超过慢指针,又因为快指针相对于慢指针每次向前一步,因此不可能越过慢指针,只能是相遇后超过,因此证明了慢指针一定在入环的第一圈被快指针相遇。
然后如图中公式,我们就能用一个指针指向head
,另一个指向相遇节点,共同前进,当相遇时即为环的入口了。
js
var detectCycle = function (head) {
let fast = head;
let slow = head;
//快慢指针判断是否有环
while (fast && fast.next && fast.next.next) {
fast = fast.next.next;
slow = slow.next;
if (fast === slow) break;
}
if (!(fast && fast.next && fast.next.next))
return null;
//有环
let res = head;
while (res !== slow) {
res = res.next;
slow = slow.next;
}
return res;
};
总结
- 使用哨兵节点(虚拟头节点)统一链表操作,减少判断头节点可能会出现的情况。
- 大多数情况下,我们要操作某个节点都需要他的前驱节点,例如24题中,我们需要的不是3,4而是1,3。
- 对于相交链表、环形链表这种题目,还是要多做多积累。