上篇学习了链表比较基础的操作,踏进了链表的世界,这篇我们学习相对复杂的链表操作了:反转、指定位置的删除等等
处理链表的本质,是处理链表结点之间的指针关系。
快慢指针
删除链表的倒数第n个结点
真题描述:给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例: 给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个结点后,链表变为 1->2->3->5.
说明: 给定的 n 保证是有效的。
复习:dummy 结点——能够处理掉头结点为空的边界问题
const dummy = new ListNode()
// 这里的 head 是链表原有的第一个结点
dummy.next = head
思路分析:
难点:“倒数第 N 个”如何定位???因为遍历不可能从后往前走的,所以我们转换为“正数第 len - n + 1"个
噔噔磴~~~~
快慢指针登场
步骤:
① 快指针先走n步;
② 快慢指针一起前进;
③ 快指针到最后一个结点,两个指针就一起停下来;
④ 此时的满指针就是我们目标结点前一个(倒数第 n 个结点的前一个结点),然后做删除操作
const removeNthFromEnd = function (head, n) {
// 初始化 dummy 结点
const dummy = new ListNode()
// dummy指向头结点
dummy.next = head
// 初始化快慢指针,均指向dummy
let fast = dummy
let slow = dummy
// 快指针先走 n 步
while (n !== 0) {
fast = fast.next
n--
}
// 快慢指针一起走
while (fast.next) {
fast = fast.next
slow = slow.next
}
// 慢指针删除自己的后继结点
slow.next = slow.next.next
// 返回头结点
return dummy.next
};
多指针
完全反转一个链表
真题描述:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
思路分析:
把每个结点next指针反过来
怎么做呢???利用多指针
cur.next = pre
到这时候有个疑问就是:next有什么用呢?
当cur.next改变指针指向后,下一次遍历就没法定位下一个结点了,所以next是保存cur的后续结点,下次遍历从这开始
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
};
局部反转一个链表
真题描述:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明: 1 ≤ m ≤ n ≤ 链表长度。
示例:
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
思路分析:
问题来了:
如何让1指向4、让2指向5呢?
先来把m-n区域定为被逆序区间,解决这个问题关键就是对被逆序区间前后两个结点做额外处理!
// 入参是头结点、m、n
const reverseBetween = function (head, m, n) {
// 定义pre、cur,用leftHead来承接整个区间的前驱结点
let pre, cur, leftHead
// 别忘了用 dummy 嗷
const dummy = new ListNode()
// dummy后继结点是头结点
dummy.next = head
// p是一个游标,用于遍历,最初指向 dummy
let p = dummy
// p往前走 m-1 步,走到整个区间的前驱结点处
for (let i = 0; i < m - 1; i++) {
p = p.next
}
// 缓存这个前驱结点到 leftHead 里
leftHead = p
// start 是反转区间的第一个结点
let start = leftHead.next
// pre 指向start
pre = start
// cur 指向 start 的下一个结点
cur = pre.next
// 开始重复反转动作
for (let i = m; i < n; i++) {
let next = cur.next
cur.next = pre
pre = cur
cur = next
}
// leftHead 的后继结点此时为反转后的区间的第一个结点
leftHead.next = pre
// 将区间内反转后的最后一个结点 next 指向 cur
start.next = cur
// dummy.next 永远指向链表头结点
return dummy.next
};
环形链表
如何判断链表是否成环?
真题描述:给定一个链表,判断链表中是否有环。
示例 1:
输入:输入head=[3,2,0,-4],flag = 1
解释:链表中存在一个环
思路分析:
从 flag 出发,只要我能够再回到 flag 处,那么就意味着,我正在遍历一个环形链表
const hasCycle = function (head) {
// 定义快慢指针
let fast = slow = head
while (slow && fast && fast.next) {
// 慢指针走一步
slow = slow.next;
// 快指针走两步
fast = fast.next.next
// 如果存在环,快慢指针一定会相遇(相等)
if (slow == fast) {
return true
}
}
// 如果快指针next指向空,那么就是无环
if (fast.next == null) {
return null
}
}
衍生问题——定位环的起点
真题描述:给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 null。
示例 1:
输入:head = [3,2,0,-4](如下图)
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个结点。
示例 2:
输入:head = [1,2](如下图)
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个结点。
示例 3:
输入:head = [1](如下图)
输出:null
解释:链表中没有环。
// 快慢指针
const hasCycle = function (head) {
// 定义快慢指针
let fast = slow = head
while (slow && fast && fast.next) {
// 慢指针走一步
slow = slow.next;
// 快指针走两步
fast = fast.next.next
// 如果存在环,快慢指针一定会相遇(相等)
if (slow == fast) {
return true
}
}
// 如果快指针next指向空,那么就是无环
if (fast.next == null) {
return null
}
// 如果有环,找出环入口
// 定义新的快指针从头开始
fast = head;
while (slow != fast) {
// 慢指针从刚刚相遇地方开始,快指针从head开始
// 快慢指针同步前进
slow = slow.next
fast = fast.next
}
// 最后快慢指针相遇的地方就是环入口
return slow
}