一、链表操作:从入门到实战

链表这玩意儿,说实话是面试里的常客。我当年刚入行时,总觉得链表操作就是背模板,直到在一次线上排查内存泄漏时,才发现自己对链表的理解有多浅。今天咱们就把链表操作的核心套路捋一遍。

1.1 链表反转——最基础的“翻盘”操作

链表反转,说白了就是把箭头方向全部调个头。我习惯用迭代法,因为它空间复杂度是 O(1),而且逻辑清晰。

// 反转单链表(迭代法)
public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode nextTemp = curr.next; // 先保存下一个节点
        curr.next = prev;               // 反转指针
        prev = curr;                    // 前驱后移
        curr = nextTemp;                // 当前后移
    }
    return prev;
}
小技巧: 反转链表时,记得先保存 next 节点。我曾经因为忘记这步,导致链表断链,debug 了半小时。

1.2 快慢指针——链表里的“双人舞”

快慢指针,一个走两步,一个走一步。你想想看,如果链表有环,快指针迟早会追上慢指针。这个套路在环形链表检测、找中点、找倒数第K个节点时特别好用。

// 判断链表是否有环
public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) return false;
    ListNode slow = head;
    ListNode fast = head.next;
    while (slow != fast) {
        if (fast == null || fast.next == null) return false;
        slow = slow.next;
        fast = fast.next.next;
    }
    return true;
}
注意: 快指针移动时一定要判空,否则会抛出 NullPointerException。我见过太多人在这里翻车了。

1.3 合并有序链表——归并思想在链表上的体现

合并两个有序链表,其实就是归并排序的链表版本。我个人习惯用递归,代码简洁,但要注意递归深度。

// 合并两个有序链表(递归版)
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;
    if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
}

1.4 经典例题精讲

环形链表 II——找到环的入口

这道题比判断是否有环多一步:找到环的入口。我记得第一次做这题时,被数学推导绕晕了。其实核心就一句话:相遇后,让一个指针从头开始,另一个从相遇点开始,都走一步,再次相遇就是入口。

public ListNode detectCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) { // 相遇了
            ListNode ptr = head;
            while (ptr != slow) {
                ptr = ptr.next;
                slow = slow.next;
            }
            return ptr; // 环的入口
        }
    }
    return null;
}

相交链表——找两个链表的交点

这道题的经典解法是:两个指针分别从两个链表头出发,走到末尾后跳到另一个链表头继续走。如果相交,它们会在交点相遇。为什么?因为走过的路程相同。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) return null;
    ListNode pA = headA, pB = headB;
    while (pA != pB) {
        pA = (pA == null) ? headB : pA.next;
        pB = (pB == null) ? headA : pB.next;
    }
    return pA;
}
核心总结: 链表操作的核心就是 指针操作 + 边界处理。反转看指针方向,快慢看步长差异,合并看递归/迭代选择。把这些模板吃透,面试时就能信手拈来。

1.5 知识体系总览

下面这张图,是我自己梳理的链表操作知识体系。你可以把它当作复习地图。

链表操作核心 链表反转 快慢指针 合并有序链表 迭代法 递归法 局部反转 环形检测 找中点 倒数第K个 递归合并 迭代合并 K路合并

1.6 避坑指南

  • 空指针检查: 操作链表前,一定要检查 head 是否为 null。我曾经在线上环境因为没判空,导致整个服务挂了。
  • 边界条件: 反转时注意头尾节点,合并时注意其中一个链表先走完的情况。
  • 循环引用: 反转链表时,如果忘记断开原引用,可能会造成循环引用,导致内存泄漏。
  • 快慢指针初始化: 有些题目要求 slow 和 fast 都从 head 开始,有些要求 fast 从 head.next 开始。具体看题目要求,别搞混了。
我的习惯: 每次写完链表代码,我都会手动模拟一遍流程,尤其是反转和快慢指针。纸上画一遍,胜过 debug 两小时。

公众号:蓝海资料掘金营,微信deep3321