链表操作技巧:从基础删除到反转与环检测
链表是一种常见的数据结构,其动态性和灵活性使其在很多场景中被广泛应用。但链表的操作也因其指针特性而具有一定挑战性,本文将结合具体代码,介绍链表操作中的几个关键技巧:dummy 节点的应用、链表反转、环检测以及倒数第 N 个节点的删除。
dummy 节点:简化边界条件的利器
在链表操作中,头节点的处理往往是一个难点,因为头节点没有前驱节点。dummy 节点(哨兵节点)正是为解决这一问题而生,它是一个不存储真实数据的假节点,通常放在链表的最前面,能有效简化边界条件。
基础节点删除
未使用 dummy 节点时,删除操作需要单独处理头节点的情况:
function remove(head, val) {
// 单独处理头节点为目标节点的情况
if (head && head.val === val) {
return head.next;
}
let cur = head;
// 遍历链表查找目标节点
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next; // 删除目标节点
break;
}
cur = cur.next;
}
return head;
}
使用 dummy 节点后,代码更加简洁,无需单独处理头节点:
function remove(head, val) {
const dummy = new ListNode(0); // 创建dummy节点
dummy.next = head; // 将dummy节点与原链表连接
let cur = dummy;
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next; // 统一的删除逻辑
break;
}
cur = cur.next;
}
return dummy.next; // 返回新的头节点(可能已改变)
}
链表反转:dummy 节点 + 头插法
链表反转是常见操作,利用 dummy 节点结合头插法可以高效实现。头插法的核心思想是将每个节点依次插入到已反转部分的头部。
function reverseList(head) {
// dummy节点的next始终指向当前已反转部分的头节点
const dummy = new ListNode(0);
let cur = head; // 当前要处理的节点
while (cur) {
const next = cur.next; // 1. 保存下一个节点,防止链表断裂
cur.next = dummy.next; // 2. 让当前节点指向已反转部分的头
dummy.next = cur; // 3. 将当前节点设为新的反转头
cur = next; // 处理原链表的下一个节点
}
return dummy.next; // 反转后的新头节点
}
头插法通过三步操作实现反转:保存下一个节点、连接已反转部分、更新反转头,循环处理所有节点即可完成整个链表的反转。
快慢指针:环检测与多指针技巧
快慢指针是链表操作中的另一个重要技巧,两个指针同向移动但速度不同,可用于解决环检测、寻找中间节点等问题。
链表环检测
判断链表是否有环的原理是:快指针每次走两步,慢指针每次走一步。如果链表有环,快指针最终会追上慢指针;否则快指针会先到达链表末尾。
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast && fast.next) { // 确保快指针能安全移动
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
if (slow === fast) { // 相遇则说明有环
return true;
}
}
// 快指针到达末尾,无环
return false;
}
删除链表的倒数第 N 个节点
结合 dummy 节点和快慢指针可以高效实现这一操作,无需先计算链表长度。
const removeNthFromEnd = function(head, n) {
const dummy = new ListNode(0);
dummy.next = head;
let fast = dummy;
let slow = dummy;
// 快指针先移动N步
for (let i = 0; i < n; i++) {
fast = fast.next;
}
// 快慢指针同时移动,直到快指针到达末尾
while (fast.next) {
fast = fast.next;
slow = slow.next;
}
// 此时慢指针指向倒数第N个节点的前一个
slow.next = slow.next.next; // 删除目标节点
return dummy.next;
};
通过上述技巧,我们可以优雅地解决链表操作中的多种问题。dummy 节点简化了边界条件处理,快慢指针提高了某些操作的效率,这些技巧在链表相关算法题中经常用到,掌握它们能极大提升解决链表问题的能力。