链表操作技巧:从基础删除到反转与环检测

33 阅读3分钟

链表操作技巧:从基础删除到反转与环检测

链表是一种常见的数据结构,其动态性和灵活性使其在很多场景中被广泛应用。但链表的操作也因其指针特性而具有一定挑战性,本文将结合具体代码,介绍链表操作中的几个关键技巧: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 节点简化了边界条件处理,快慢指针提高了某些操作的效率,这些技巧在链表相关算法题中经常用到,掌握它们能极大提升解决链表问题的能力。