链表类题型刷了大概20道左右的样子,总结出一些规律,分享给大家
解题思路
最主要的思路是递归和迭代两种,其中又包含一些技巧性的操作,例如快慢指针这种,总结出来的这些解题思路,应对大多数的链表类型题应该问题不大了
案例1: 倒数第k个节点
参考题:
- 剑指offer22 链表中倒数第k个节点 简单
- LC19 删除链表中的倒数第N个节点 中等
主要思路是快慢指针,让快指针先走k步,再一起前进,快指针走完的时候,慢指针此时所在位置就是倒数第k个节点
const getKthFromEnd: ListNode | null = (head: ListNode | null, k: number) => {
let slow: ListNode = head, fast: ListNode = head
for (let i = 0; i < k; i++){
fast = fast.next
}
while (fast){
fast = fast.next
slow = slow.next
}
return slow
};
这个思路可以紧接着套用到删除倒数第n个节点,只要找到倒数第n+1个节点的引用即可
const removeNthFromEnd: ListNode | null = (head: ListNode | null, n: number) => {
let dummy: ListNode = new ListNode(-1)
dummy.next = head
const p: ListNode = getKthFromEnd(dummy, n+1)
p.next = p.next.next
return dummy.next
}
案例2: 环形链表/寻找链表中点
参考题:
- LC141 环形链表
- LC142 环形链表II
- LC876 链表的中间节点
快慢指针的第二种常见应用场景,这个类型的题中,快指针走两步,慢指针走一步。由于快慢指针的方法趋同,所以把这两个类型的题型放到一起讲
对于环形链表1,可以这么理解,每次让快指针走两步,慢指针走一步,一旦进入环,慢指针和快指针终究会相遇,从头到尾没相遇就是没有环
代码如下
const hasCycle: boolean = (head: ListNode | null) => {
let slow: ListNode = head
let fast: ListNode = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
if (slow === fast){
return true
}
}
return false
};
对于环形链表II,题目要求返回环的起点索引,这个可以先判断是否有环,如果有环则等待下一步操作,无环返回null
若有环,在快慢指针重合的那个节点开始,让慢指针回到head头节点,快慢指针以同样的速度前进,若快慢指针再次相遇时这个点就是环的起点。
这句话怎么理解呢?首先要承认,快指针走2k步时,慢指针正好走了k步,恰巧此时快慢指针相遇,那么快指针比慢指针多走的k步实际上就是环长度的整数倍。假设第一次相遇的节点距离环的起始位置为m个节点,且一圈环的长度正好是k步,那么慢指针距离环起点的距离为k-m,快指针从环里出发到达起点的距离也为k-m。这里的思路参考labuladong的算法小抄。
const detectCycle: ListNode | null = (head: ListNode | null) => {
let slow: ListNode = head
let fast: ListNode = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
if (fast === slow){
break;
}
}
if (fast === null || fast.next === null) {
return null
}
slow = head
while (slow !== fast) {
slow = slow.next
fast = fast.next
}
return slow
};
寻找链表中点也非常相似,也是慢指针1步,快指针两步,快指针走到的时候,慢指针正好走到中间,这里需要考虑链表长度的奇偶问题,可能会产生一些影响
案例3: 反转链表
参考题:
- LC206 反转链表 简单
- LC234 回文链表 简单
- LC92 反转链表II 中等
- LC25 K个一组反转链表 困难
以最简单的反转链表为例子,给定一个头节点,返回一个反转的链表,typescript的模版如下, 使用迭代的方法,先保存cur.next到一个新变量,再每次修改cur的指针指向pre,向前移动pre和cur。
const reverse: ListNode | null = (head: ListNode | null) => {
let pre: ListNode = null, cur: ListNode = head
while (cur){
let temp = cur.next
cur.next = pre
pre = cur
cur = temp
}
return pre
}
回文链表基于上面的例子,就很简单了,就是一个寻找链表中点+反转链表的例子,代码如下:
const isPalindrome: boolean = (head: ListNode | null) => {
let slow: ListNode = head
let fast: ListNode = head
while (slow){
if (fast === null) { //偶数
break
} else if (fast.next === null){ //奇数
slow = slow.next
break
}
slow = slow.next
fast = fast.next.next
}
let p: ListNode = head // 比较p和slow
slow = reverse(slow)
while (slow) {
if (slow.val !== p.val){
return false
}
slow = slow.next
p = p.next
}
return true
};
其他类型的题目
代表性的有合并链表,删除链表重复元素,相交链表,两两交换链表节点,K个一组反转链表,这些大多数都可以去用到递归去做,这里不多赘述,这里主要以几个比较经典的案例来分析一些常见的链表类题型,如果有写错的地方请多多包涵~