算法:链表题核心技巧与解题框架

链表题核心技巧与解题框架

链表的数据结构不复杂:一个节点,一个 next 指针,最多再加一个 prevrandom。但很多人写链表题时,比写动态规划还容易崩。

原因不是链表难,而是链表题特别容易在边界处出错:

  • 删除头节点怎么办?
  • 插入第一个节点怎么办?
  • 反转后新头是谁?
  • 当前节点的 next 改掉以后,后面的链表还找不找得到?
  • 递归反转为什么明明只有几行,却总是想不明白?

链表题的本质不是记 API,而是维护指针不变量。

这篇文章只抓一条主线:链表操作,就是在不断回答两个问题:我现在握住了哪些节点?改完指针后,链表是否还能接回去?

本篇覆盖三个层次:

  • 基础改链技巧:哨兵节点、删除、合并
  • 反转链表:迭代、递归、局部反转
  • 经典模型:快慢指针、环入口、相交链表

其中有两个点必须真正吃透:

  • 哨兵节点:它不是"小技巧",而是统一头节点边界的工具
  • 递归反转:难点不是代码,而是你在脑子里同时追踪了太多层调用

一、链表题到底在考什么

数组题里,下标天然存在。你可以通过 i + 1 找到下一个元素,通过 nums[i] 随时访问当前位置。

链表不一样。链表只有指针。

复制代码
1 -> 2 -> 3 -> 4 -> null

如果你站在节点 2 上,只知道:

复制代码
cur = 2
cur.next = 3

你不知道 2 前面是谁,也不能随机访问 4。更麻烦的是,一旦你执行:

java 复制代码
cur.next = prev;

原本 cur.next 指向的后续链表就可能丢了。

所以链表题的第一原则是:改指针之前,先保存会丢的信息。

最典型的就是反转链表:

java 复制代码
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;

这四行代码背后的含义是:

  • next = cur.next:先握住后面的链表
  • cur.next = prev:把当前节点反过来接
  • prev = cur:反转后的链表头往前推进
  • cur = next:继续处理原链表剩余部分

你不需要把链表题想成"指针魔法"。只要每次改指针前问一句:我改掉这根线以后,还有没有人能找到后面的节点? 很多错误会立刻暴露出来。

二、哨兵节点:把"头节点特判"消灭掉

链表题里最常见的边界是什么?不是空链表,而是头节点变化

比如删除链表中等于 val 的节点:

复制代码
1 -> 2 -> 6 -> 3 -> 6
val = 6

如果要删的是中间节点,很简单:

java 复制代码
prev.next = cur.next;

但如果要删的是头节点呢?

复制代码
6 -> 1 -> 2 -> 3

这时没有 prev。你只能单独写:

java 复制代码
while (head != null && head.val == val) {
    head = head.next;
}

然后再处理后面的节点。

代码能写,但分叉出现了:头节点一套逻辑,非头节点一套逻辑。链表题最怕这种分叉,因为你会在每个题里重复处理它。

哨兵节点就是为了解决这个问题。

2.1 哨兵节点是什么

哨兵节点,也叫 dummy head,是一个人为创建的虚拟节点,放在真实头节点前面:

复制代码
dummy -> 6 -> 1 -> 2 -> 3

代码:

java 复制代码
ListNode dummy = new ListNode(-1);
dummy.next = head;

从此以后,真实头节点也有了前驱节点:dummy
#mermaid-svg-oS9Lxj7B1fdC1tBc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oS9Lxj7B1fdC1tBc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oS9Lxj7B1fdC1tBc .error-icon{fill:#552222;}#mermaid-svg-oS9Lxj7B1fdC1tBc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oS9Lxj7B1fdC1tBc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oS9Lxj7B1fdC1tBc .marker.cross{stroke:#333333;}#mermaid-svg-oS9Lxj7B1fdC1tBc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oS9Lxj7B1fdC1tBc p{margin:0;}#mermaid-svg-oS9Lxj7B1fdC1tBc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oS9Lxj7B1fdC1tBc .cluster-label text{fill:#333;}#mermaid-svg-oS9Lxj7B1fdC1tBc .cluster-label span{color:#333;}#mermaid-svg-oS9Lxj7B1fdC1tBc .cluster-label span p{background-color:transparent;}#mermaid-svg-oS9Lxj7B1fdC1tBc .label text,#mermaid-svg-oS9Lxj7B1fdC1tBc span{fill:#333;color:#333;}#mermaid-svg-oS9Lxj7B1fdC1tBc .node rect,#mermaid-svg-oS9Lxj7B1fdC1tBc .node circle,#mermaid-svg-oS9Lxj7B1fdC1tBc .node ellipse,#mermaid-svg-oS9Lxj7B1fdC1tBc .node polygon,#mermaid-svg-oS9Lxj7B1fdC1tBc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oS9Lxj7B1fdC1tBc .rough-node .label text,#mermaid-svg-oS9Lxj7B1fdC1tBc .node .label text,#mermaid-svg-oS9Lxj7B1fdC1tBc .image-shape .label,#mermaid-svg-oS9Lxj7B1fdC1tBc .icon-shape .label{text-anchor:middle;}#mermaid-svg-oS9Lxj7B1fdC1tBc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oS9Lxj7B1fdC1tBc .rough-node .label,#mermaid-svg-oS9Lxj7B1fdC1tBc .node .label,#mermaid-svg-oS9Lxj7B1fdC1tBc .image-shape .label,#mermaid-svg-oS9Lxj7B1fdC1tBc .icon-shape .label{text-align:center;}#mermaid-svg-oS9Lxj7B1fdC1tBc .node.clickable{cursor:pointer;}#mermaid-svg-oS9Lxj7B1fdC1tBc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oS9Lxj7B1fdC1tBc .arrowheadPath{fill:#333333;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oS9Lxj7B1fdC1tBc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oS9Lxj7B1fdC1tBc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oS9Lxj7B1fdC1tBc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oS9Lxj7B1fdC1tBc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oS9Lxj7B1fdC1tBc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oS9Lxj7B1fdC1tBc .cluster text{fill:#333;}#mermaid-svg-oS9Lxj7B1fdC1tBc .cluster span{color:#333;}#mermaid-svg-oS9Lxj7B1fdC1tBc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oS9Lxj7B1fdC1tBc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oS9Lxj7B1fdC1tBc rect.text{fill:none;stroke-width:0;}#mermaid-svg-oS9Lxj7B1fdC1tBc .icon-shape,#mermaid-svg-oS9Lxj7B1fdC1tBc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oS9Lxj7B1fdC1tBc .icon-shape p,#mermaid-svg-oS9Lxj7B1fdC1tBc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oS9Lxj7B1fdC1tBc .icon-shape .label rect,#mermaid-svg-oS9Lxj7B1fdC1tBc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oS9Lxj7B1fdC1tBc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oS9Lxj7B1fdC1tBc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oS9Lxj7B1fdC1tBc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有哨兵:统一删除逻辑
dummy
head
6

待删除
1
2
3
prev = dummy

统一走 prev.next = cur.next
无哨兵:头节点需特判
head
6

待删除
1
2
3
需要单独 while 循环

处理头节点删除

哨兵节点把头节点删除变成普通删除,于是删除节点的逻辑统一成:

java 复制代码
ListNode prev = dummy;
ListNode cur = head;

while (cur != null) {
    if (cur.val == val) {
        prev.next = cur.next;
    } else {
        prev = cur;
    }
    cur = cur.next;
}

return dummy.next;

注意最后返回的不是 head,而是 dummy.next。因为真实头节点可能已经被删掉,head 这个变量不一定还代表答案。

2.2 哨兵节点解决的不是空指针,而是统一操作位置

很多人把哨兵节点理解成"防止空指针"。这个说法不准确。

哨兵节点真正解决的是:让所有插入、删除、拼接,都发生在某个确定节点的后面。

比如合并两个有序链表:

java 复制代码
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(-1);
    ListNode tail = dummy;

    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            tail.next = l1;
            l1 = l1.next;
        } else {
            tail.next = l2;
            l2 = l2.next;
        }
        tail = tail.next;
    }

    tail.next = (l1 != null) ? l1 : l2;
    return dummy.next;
}

如果没有 dummy,你必须先决定新链表的第一个节点是谁。只要第一个节点单独处理,代码就会变成两段。

有了 dummy,tail 永远表示"新链表的最后一个节点",每次都往 tail.next 后面接。第一个节点和第 N 个节点没有区别。

2.3 哪些题应该第一反应想到哨兵

遇到下面这些信号,优先考虑哨兵节点:

题型信号 为什么适合哨兵
删除节点 被删节点可能是头节点
合并链表 新链表第一个节点不好单独处理
分隔链表 / 奇偶链表 要维护多个结果链表的尾指针
删除重复节点 一段重复节点可能从头开始

比如"删除排序链表中的重复节点,重复节点一个不保留":

复制代码
1 -> 1 -> 1 -> 2 -> 3
输出:2 -> 3

这里重复段一上来就在头部。没有哨兵,你会非常难受。

用哨兵后,prev 永远指向"已经确认保留的最后一个节点":

java 复制代码
public ListNode deleteDuplicates(ListNode head) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;

    ListNode prev = dummy;
    while (prev.next != null) {
        ListNode cur = prev.next;
        while (cur.next != null && cur.next.val == cur.val) {
            cur = cur.next;
        }

        if (prev.next == cur) {
            prev = cur;
        } else {
            prev.next = cur.next;
        }
    }

    return dummy.next;
}

这段代码里最关键的不变量是:prev.next 是当前待判断的一段的第一个节点;prev 前面的链表已经处理完。

只要这个不变量成立,头节点是不是重复段就不再特殊。

三、反转链表:先把迭代写成肌肉记忆

反转链表是所有链表题的地基。

题目:

复制代码
1 -> 2 -> 3 -> 4 -> 5 -> null

反转后:

复制代码
5 -> 4 -> 3 -> 2 -> 1 -> null

3.1 迭代写法

java 复制代码
public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode cur = head;

    while (cur != null) {
        ListNode next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }

    return prev;
}

这段代码建议直接背下来,但不要只背字面顺序,要背它的不变量:

  • prev:已经反转好的链表头
  • cur :还没处理的原链表头
  • next:临时保存 cur 后面的链表

#mermaid-svg-USqW69a9goCcOXvW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-USqW69a9goCcOXvW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-USqW69a9goCcOXvW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-USqW69a9goCcOXvW .error-icon{fill:#552222;}#mermaid-svg-USqW69a9goCcOXvW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-USqW69a9goCcOXvW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-USqW69a9goCcOXvW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-USqW69a9goCcOXvW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-USqW69a9goCcOXvW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-USqW69a9goCcOXvW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-USqW69a9goCcOXvW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-USqW69a9goCcOXvW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-USqW69a9goCcOXvW .marker.cross{stroke:#333333;}#mermaid-svg-USqW69a9goCcOXvW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-USqW69a9goCcOXvW p{margin:0;}#mermaid-svg-USqW69a9goCcOXvW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-USqW69a9goCcOXvW .cluster-label text{fill:#333;}#mermaid-svg-USqW69a9goCcOXvW .cluster-label span{color:#333;}#mermaid-svg-USqW69a9goCcOXvW .cluster-label span p{background-color:transparent;}#mermaid-svg-USqW69a9goCcOXvW .label text,#mermaid-svg-USqW69a9goCcOXvW span{fill:#333;color:#333;}#mermaid-svg-USqW69a9goCcOXvW .node rect,#mermaid-svg-USqW69a9goCcOXvW .node circle,#mermaid-svg-USqW69a9goCcOXvW .node ellipse,#mermaid-svg-USqW69a9goCcOXvW .node polygon,#mermaid-svg-USqW69a9goCcOXvW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-USqW69a9goCcOXvW .rough-node .label text,#mermaid-svg-USqW69a9goCcOXvW .node .label text,#mermaid-svg-USqW69a9goCcOXvW .image-shape .label,#mermaid-svg-USqW69a9goCcOXvW .icon-shape .label{text-anchor:middle;}#mermaid-svg-USqW69a9goCcOXvW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-USqW69a9goCcOXvW .rough-node .label,#mermaid-svg-USqW69a9goCcOXvW .node .label,#mermaid-svg-USqW69a9goCcOXvW .image-shape .label,#mermaid-svg-USqW69a9goCcOXvW .icon-shape .label{text-align:center;}#mermaid-svg-USqW69a9goCcOXvW .node.clickable{cursor:pointer;}#mermaid-svg-USqW69a9goCcOXvW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-USqW69a9goCcOXvW .arrowheadPath{fill:#333333;}#mermaid-svg-USqW69a9goCcOXvW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-USqW69a9goCcOXvW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-USqW69a9goCcOXvW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-USqW69a9goCcOXvW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-USqW69a9goCcOXvW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-USqW69a9goCcOXvW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-USqW69a9goCcOXvW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-USqW69a9goCcOXvW .cluster text{fill:#333;}#mermaid-svg-USqW69a9goCcOXvW .cluster span{color:#333;}#mermaid-svg-USqW69a9goCcOXvW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-USqW69a9goCcOXvW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-USqW69a9goCcOXvW rect.text{fill:none;stroke-width:0;}#mermaid-svg-USqW69a9goCcOXvW .icon-shape,#mermaid-svg-USqW69a9goCcOXvW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-USqW69a9goCcOXvW .icon-shape p,#mermaid-svg-USqW69a9goCcOXvW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-USqW69a9goCcOXvW .icon-shape .label rect,#mermaid-svg-USqW69a9goCcOXvW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-USqW69a9goCcOXvW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-USqW69a9goCcOXvW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-USqW69a9goCcOXvW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 完成
prev

5 -> 4 -> 3 -> 2 -> 1 -> null
cur

null
第2轮后
prev

2 -> 1 -> null
cur

3
4 -> 5
第1轮后
prev

1 -> null
cur

2
3 -> 4 -> 5
初始
prev

null
cur

1
2 -> 3 -> 4 -> 5

反转链表的三段不变量:

循环开始前:

复制代码
prev = null
cur  = 1 -> 2 -> 3 -> 4 -> 5

处理完节点 1:

复制代码
prev = 1 -> null
cur  = 2 -> 3 -> 4 -> 5

处理完节点 2:

复制代码
prev = 2 -> 1 -> null
cur  = 3 -> 4 -> 5

直到 cur == null,说明原链表都处理完了,此时 prev 就是新头。

这就是链表反转的全部逻辑。

四、递归反转为什么这么难

递归版反转链表只有几行:

java 复制代码
public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }

    ListNode newHead = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
}

代码短,但很多人越看越晕。

原因很简单:你脑子里同时做了三件事:

  • 想当前层 head 是谁
  • 想下一层递归会返回什么
  • 想整条链表每个节点的指针怎么变化

这三个视角混在一起,就会爆炸。

递归题最重要的心法是:不要跳进递归里面。相信子问题已经解决,只处理当前层和子问题的连接关系。

4.1 函数语义不是猜出来的

这里最容易卡住的问题是:为什么能想到 reverseList(head) 的语义是"反转以 head 开头的链表,并返回新头"?

它不是拍脑袋来的,而是从原问题倒推出来的。

原问题要你做的是:反转整条链表,并把反转后的头节点返回给调用方。递归要做的事情,就是把这个问题缩小一号。站在当前节点 head 上,你唯一能交给递归处理的更小链表,就是 head.next 开头的后半段链表。

所以最自然的子问题就是:请你帮我反转 head.next 开头的链表,并把反转后的头节点返回给我。

这句话一旦成立,函数语义就出来了:

复制代码
reverseList(x):反转以 x 开头的链表,返回反转后的新头。

为什么一定要"返回新头"? 因为反转后,整条链表的头节点会变。调用方如果拿不到新头,就不知道最终答案从哪里开始。对当前层来说,reverseList(head.next) 返回的新头,也是整条链表反转后的新头,所以它必须一路往上返回。

当然,这不是唯一可行的语义。比如你也可以把函数定义成 reverse(cur, prev):把 cur 开头的链表接到 prev 前面,返回最终新头。

对应代码就是另一种递归写法:

java 复制代码
private ListNode reverse(ListNode cur, ListNode prev) {
    if (cur == null) {
        return prev;
    }

    ListNode next = cur.next;
    cur.next = prev;
    return reverse(next, cur);
}

public ListNode reverseList(ListNode head) {
    return reverse(head, null);
}

这段代码也完全正确。它的语义更接近迭代版:prev 表示已经反转好的部分,cur 表示还没处理的部分。

所以递归函数不是只有一种定义方式。关键不在于"必须定义成哪一句",而在于:你定义的函数语义,必须能让子问题变小,并且能让当前层把结果接回来。

更具体一点,一个递归语义只要满足三个条件,就有机会写对:

  1. 子问题变小:每次递归处理的范围,比当前问题少一点
  2. 返回值清楚:当前层知道递归调用会还给自己什么
  3. 当前层能收尾:拿到子问题结果后,当前层能完成自己的连接逻辑

普通递归版选择"反转链表并返回新头",是因为它刚好贴合题目的返回值,也让当前层只剩两件事:把 head 接到尾巴后面,再断开原来的 next

4.2 递归反转的正确打开方式

假设当前链表是:

复制代码
1 -> 2 -> 3 -> 4 -> 5 -> null

当前层 head = 1

递归调用:

java 复制代码
ListNode newHead = reverseList(head.next);

你不要继续往下想 2、3、4、5 每一层怎么执行。你只需要相信 reverseList(2) 已经把 2 -> 3 -> 4 -> 5 反转好了。

也就是说,它返回后,局面变成:

复制代码
5 -> 4 -> 3 -> 2 -> null
1 -> 2

#mermaid-svg-DcOK8kmn2cRxpX9n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DcOK8kmn2cRxpX9n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DcOK8kmn2cRxpX9n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DcOK8kmn2cRxpX9n .error-icon{fill:#552222;}#mermaid-svg-DcOK8kmn2cRxpX9n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DcOK8kmn2cRxpX9n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DcOK8kmn2cRxpX9n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DcOK8kmn2cRxpX9n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DcOK8kmn2cRxpX9n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DcOK8kmn2cRxpX9n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DcOK8kmn2cRxpX9n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DcOK8kmn2cRxpX9n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DcOK8kmn2cRxpX9n .marker.cross{stroke:#333333;}#mermaid-svg-DcOK8kmn2cRxpX9n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DcOK8kmn2cRxpX9n p{margin:0;}#mermaid-svg-DcOK8kmn2cRxpX9n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DcOK8kmn2cRxpX9n .cluster-label text{fill:#333;}#mermaid-svg-DcOK8kmn2cRxpX9n .cluster-label span{color:#333;}#mermaid-svg-DcOK8kmn2cRxpX9n .cluster-label span p{background-color:transparent;}#mermaid-svg-DcOK8kmn2cRxpX9n .label text,#mermaid-svg-DcOK8kmn2cRxpX9n span{fill:#333;color:#333;}#mermaid-svg-DcOK8kmn2cRxpX9n .node rect,#mermaid-svg-DcOK8kmn2cRxpX9n .node circle,#mermaid-svg-DcOK8kmn2cRxpX9n .node ellipse,#mermaid-svg-DcOK8kmn2cRxpX9n .node polygon,#mermaid-svg-DcOK8kmn2cRxpX9n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DcOK8kmn2cRxpX9n .rough-node .label text,#mermaid-svg-DcOK8kmn2cRxpX9n .node .label text,#mermaid-svg-DcOK8kmn2cRxpX9n .image-shape .label,#mermaid-svg-DcOK8kmn2cRxpX9n .icon-shape .label{text-anchor:middle;}#mermaid-svg-DcOK8kmn2cRxpX9n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DcOK8kmn2cRxpX9n .rough-node .label,#mermaid-svg-DcOK8kmn2cRxpX9n .node .label,#mermaid-svg-DcOK8kmn2cRxpX9n .image-shape .label,#mermaid-svg-DcOK8kmn2cRxpX9n .icon-shape .label{text-align:center;}#mermaid-svg-DcOK8kmn2cRxpX9n .node.clickable{cursor:pointer;}#mermaid-svg-DcOK8kmn2cRxpX9n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DcOK8kmn2cRxpX9n .arrowheadPath{fill:#333333;}#mermaid-svg-DcOK8kmn2cRxpX9n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DcOK8kmn2cRxpX9n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DcOK8kmn2cRxpX9n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DcOK8kmn2cRxpX9n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DcOK8kmn2cRxpX9n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DcOK8kmn2cRxpX9n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DcOK8kmn2cRxpX9n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DcOK8kmn2cRxpX9n .cluster text{fill:#333;}#mermaid-svg-DcOK8kmn2cRxpX9n .cluster span{color:#333;}#mermaid-svg-DcOK8kmn2cRxpX9n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DcOK8kmn2cRxpX9n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DcOK8kmn2cRxpX9n rect.text{fill:none;stroke-width:0;}#mermaid-svg-DcOK8kmn2cRxpX9n .icon-shape,#mermaid-svg-DcOK8kmn2cRxpX9n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DcOK8kmn2cRxpX9n .icon-shape p,#mermaid-svg-DcOK8kmn2cRxpX9n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DcOK8kmn2cRxpX9n .icon-shape .label rect,#mermaid-svg-DcOK8kmn2cRxpX9n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DcOK8kmn2cRxpX9n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DcOK8kmn2cRxpX9n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DcOK8kmn2cRxpX9n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 递归返回后
head.next.next = head
head.next = null
newHead

5
4
3
2
head

1
null
递归前
head

1
head.next 子链表

2 -> 3 -> 4 -> 5

当前层只剩一件事:把 1 接到 2 后面。

java 复制代码
head.next.next = head;

这句话翻译成人话就是:原来 head 后面的那个节点,现在是反转后半段的尾巴;让它指回 head

于是:

复制代码
5 -> 4 -> 3 -> 2 -> 1

但此时 1 的 next 还指向 2,如果不切断,会形成环:

java 复制代码
head.next = null;

最后返回 newHead,也就是整条反转后链表的头节点 5。

4.3 递归反转的记忆口诀

递归反转只记三句话:

  1. 子链表先反转好
  2. 让后一个节点指回当前节点
  3. 当前节点断开原来的 next

对应代码:

java 复制代码
ListNode newHead = reverseList(head.next); // 1
head.next.next = head;                     // 2
head.next = null;                          // 3
return newHead;

这里最容易错的是 head.next.next = head。如果你觉得这句话别扭,就把它念成:我后面的节点,反过来指向我。 这比"某某的 next 的 next"更好记。

4.4 写递归链表题的四步法

建议以后所有链表递归题都按这四步写:

步骤 内容
1. 定义函数语义 这个函数替当前层解决哪一段子问题,返回什么?
2. 写 base case 空链表、单节点、反转 N 个节点到哪里停?
3. 假设子问题已经正确 不要展开递归过程
4. 处理当前节点和子问题结果之间的连接 当前层连接逻辑

以反转链表为例:

步骤 内容
函数语义 反转以 head 开头的链表,返回新头
base case 空链表或单节点直接返回
子问题 reverseList(head.next) 反转后面的链表
当前层连接 head.next.next = head; head.next = null

递归好不好写,几乎完全取决于第一步:函数语义是否清楚、是否稳定。如果函数语义都没想清楚,一边写一边猜返回值,后面一定乱。

五、局部反转:用哨兵把边界接回去

链表反转真正进入面试区,是从局部反转开始的。

题目:反转第 m 到第 n 个节点。

复制代码
1 -> 2 -> 3 -> 4 -> 5
m = 2, n = 4

输出:1 -> 4 -> 3 -> 2 -> 5

这道题最重要的不是反转,而是找到四个位置:

  • before:反转区间前一个节点
  • start :反转区间第一个节点
  • end :反转区间最后一个节点
  • after :反转区间后一个节点

图上是:

复制代码
1 -> 2 -> 3 -> 4 -> 5
^    ^         ^    ^
|    |         |    |
before start   end  after

反转 [start, end] 之后,要接成:

复制代码
before -> end -> ... -> start -> after

m = 1 时,before 不存在。怎么办?还是哨兵节点。
#mermaid-svg-8GiNbUxE04BDRhr5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8GiNbUxE04BDRhr5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8GiNbUxE04BDRhr5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8GiNbUxE04BDRhr5 .error-icon{fill:#552222;}#mermaid-svg-8GiNbUxE04BDRhr5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8GiNbUxE04BDRhr5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8GiNbUxE04BDRhr5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8GiNbUxE04BDRhr5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8GiNbUxE04BDRhr5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8GiNbUxE04BDRhr5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8GiNbUxE04BDRhr5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8GiNbUxE04BDRhr5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8GiNbUxE04BDRhr5 .marker.cross{stroke:#333333;}#mermaid-svg-8GiNbUxE04BDRhr5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8GiNbUxE04BDRhr5 p{margin:0;}#mermaid-svg-8GiNbUxE04BDRhr5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8GiNbUxE04BDRhr5 .cluster-label text{fill:#333;}#mermaid-svg-8GiNbUxE04BDRhr5 .cluster-label span{color:#333;}#mermaid-svg-8GiNbUxE04BDRhr5 .cluster-label span p{background-color:transparent;}#mermaid-svg-8GiNbUxE04BDRhr5 .label text,#mermaid-svg-8GiNbUxE04BDRhr5 span{fill:#333;color:#333;}#mermaid-svg-8GiNbUxE04BDRhr5 .node rect,#mermaid-svg-8GiNbUxE04BDRhr5 .node circle,#mermaid-svg-8GiNbUxE04BDRhr5 .node ellipse,#mermaid-svg-8GiNbUxE04BDRhr5 .node polygon,#mermaid-svg-8GiNbUxE04BDRhr5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8GiNbUxE04BDRhr5 .rough-node .label text,#mermaid-svg-8GiNbUxE04BDRhr5 .node .label text,#mermaid-svg-8GiNbUxE04BDRhr5 .image-shape .label,#mermaid-svg-8GiNbUxE04BDRhr5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-8GiNbUxE04BDRhr5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8GiNbUxE04BDRhr5 .rough-node .label,#mermaid-svg-8GiNbUxE04BDRhr5 .node .label,#mermaid-svg-8GiNbUxE04BDRhr5 .image-shape .label,#mermaid-svg-8GiNbUxE04BDRhr5 .icon-shape .label{text-align:center;}#mermaid-svg-8GiNbUxE04BDRhr5 .node.clickable{cursor:pointer;}#mermaid-svg-8GiNbUxE04BDRhr5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8GiNbUxE04BDRhr5 .arrowheadPath{fill:#333333;}#mermaid-svg-8GiNbUxE04BDRhr5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8GiNbUxE04BDRhr5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8GiNbUxE04BDRhr5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8GiNbUxE04BDRhr5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8GiNbUxE04BDRhr5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8GiNbUxE04BDRhr5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8GiNbUxE04BDRhr5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8GiNbUxE04BDRhr5 .cluster text{fill:#333;}#mermaid-svg-8GiNbUxE04BDRhr5 .cluster span{color:#333;}#mermaid-svg-8GiNbUxE04BDRhr5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8GiNbUxE04BDRhr5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8GiNbUxE04BDRhr5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-8GiNbUxE04BDRhr5 .icon-shape,#mermaid-svg-8GiNbUxE04BDRhr5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8GiNbUxE04BDRhr5 .icon-shape p,#mermaid-svg-8GiNbUxE04BDRhr5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8GiNbUxE04BDRhr5 .icon-shape .label rect,#mermaid-svg-8GiNbUxE04BDRhr5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8GiNbUxE04BDRhr5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8GiNbUxE04BDRhr5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8GiNbUxE04BDRhr5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 反转后接回
before
prev

4
3
2 -->
after

5
before.next = prev
start.next = after
反转前定位
before
start

2
3
end

4
after

5
before: 区间前一个

start: 区间第一个

end: 区间最后一个

after: 区间后一个

局部反转只需要接回两根线:

java 复制代码
public ListNode reverseBetween(ListNode head, int left, int right) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;

    ListNode before = dummy;
    for (int i = 1; i < left; i++) {
        before = before.next;
    }

    ListNode start = before.next;
    ListNode prev = null;
    ListNode cur = start;

    for (int i = 0; i <= right - left; i++) {
        ListNode next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }

    before.next = prev;
    start.next = cur;

    return dummy.next;
}

这段代码里的变量含义:

  • before:反转区间前一个节点
  • start :反转前的区间头,反转后会变成区间尾
  • prev :反转后的区间头
  • cur :反转区间后一个节点,也就是 after

最后两句是整题关键:

java 复制代码
before.next = prev;
start.next = cur;

翻译一下:反转区间前面接新头,反转区间旧头接后面。

六、快慢指针:让距离成为信息

链表不能随机访问,所以很多数组里通过下标计算的位置关系,在链表里要通过指针速度来制造。

快慢指针的核心是:两个指针以不同速度移动,它们之间的距离会表达某种结构信息。

6.1 倒数第 K 个节点

题目:

复制代码
1 -> 2 -> 3 -> 4 -> 5
k = 2
输出:4

思路:

  1. fast 先走 k 步
  2. fastslow 一起走
  3. fast 到达 null 时,slow 正好在倒数第 k 个节点
java 复制代码
public ListNode findKthFromEnd(ListNode head, int k) {
    ListNode fast = head;
    ListNode slow = head;

    while (k > 0 && fast != null) {
        fast = fast.next;
        k--;
    }

    if (k > 0) {
        return null;
    }

    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }

    return slow;
}

这里的不变量是:fastslow 之间始终相差 k 个节点。所以当 fast 走到链表末尾,slow 离末尾也正好差 k。

6.2 环检测:为什么快慢指针一定会相遇

判断链表是否有环:

java 复制代码
public boolean hasCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            return true;
        }
    }

    return false;
}

为什么有环就一定会相遇?

进入环之后,fast 每次比 slow 多走一步。你可以把它们看成在环形跑道上追赶:每轮距离缩短 1,环长有限,距离迟早变成 0。所以一定会相遇。

6.3 环入口:为什么一个指针回到头节点后再走会相遇在入口

环入口是链表题里最常见的追问。

设:

  • 头节点到环入口距离 = a
  • 环入口到相遇点距离 = b
  • 相遇点再走回入口距离 = c

链表结构:

复制代码
head --a--> entry --b--> meet --c--> entry

相遇时:

  • slow 走了 a + b
  • fast 走了 a + b + 若干圈

因为 fast 速度是 slow 的两倍:

复制代码
2(a + b) = a + b + k(b + c)

整理:

复制代码
a + b = k(b + c)
a = (k - 1)(b + c) + c

这句话的含义是:从头节点走到入口的距离 a,等价于从相遇点走 c,再绕若干整圈。

所以当一个指针从 head 出发,另一个指针从 meet 出发,每次都走一步,它们会在入口相遇。

java 复制代码
public ListNode detectCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            ListNode p = head;
            while (p != slow) {
                p = p.next;
                slow = slow.next;
            }
            return p;
        }
    }

    return null;
}

环检测与环入口算法流程

#mermaid-svg-PqlONXp7XXz67uD9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PqlONXp7XXz67uD9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PqlONXp7XXz67uD9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PqlONXp7XXz67uD9 .error-icon{fill:#552222;}#mermaid-svg-PqlONXp7XXz67uD9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PqlONXp7XXz67uD9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PqlONXp7XXz67uD9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PqlONXp7XXz67uD9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PqlONXp7XXz67uD9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PqlONXp7XXz67uD9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PqlONXp7XXz67uD9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PqlONXp7XXz67uD9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PqlONXp7XXz67uD9 .marker.cross{stroke:#333333;}#mermaid-svg-PqlONXp7XXz67uD9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PqlONXp7XXz67uD9 p{margin:0;}#mermaid-svg-PqlONXp7XXz67uD9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PqlONXp7XXz67uD9 .cluster-label text{fill:#333;}#mermaid-svg-PqlONXp7XXz67uD9 .cluster-label span{color:#333;}#mermaid-svg-PqlONXp7XXz67uD9 .cluster-label span p{background-color:transparent;}#mermaid-svg-PqlONXp7XXz67uD9 .label text,#mermaid-svg-PqlONXp7XXz67uD9 span{fill:#333;color:#333;}#mermaid-svg-PqlONXp7XXz67uD9 .node rect,#mermaid-svg-PqlONXp7XXz67uD9 .node circle,#mermaid-svg-PqlONXp7XXz67uD9 .node ellipse,#mermaid-svg-PqlONXp7XXz67uD9 .node polygon,#mermaid-svg-PqlONXp7XXz67uD9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PqlONXp7XXz67uD9 .rough-node .label text,#mermaid-svg-PqlONXp7XXz67uD9 .node .label text,#mermaid-svg-PqlONXp7XXz67uD9 .image-shape .label,#mermaid-svg-PqlONXp7XXz67uD9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PqlONXp7XXz67uD9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PqlONXp7XXz67uD9 .rough-node .label,#mermaid-svg-PqlONXp7XXz67uD9 .node .label,#mermaid-svg-PqlONXp7XXz67uD9 .image-shape .label,#mermaid-svg-PqlONXp7XXz67uD9 .icon-shape .label{text-align:center;}#mermaid-svg-PqlONXp7XXz67uD9 .node.clickable{cursor:pointer;}#mermaid-svg-PqlONXp7XXz67uD9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PqlONXp7XXz67uD9 .arrowheadPath{fill:#333333;}#mermaid-svg-PqlONXp7XXz67uD9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PqlONXp7XXz67uD9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PqlONXp7XXz67uD9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PqlONXp7XXz67uD9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PqlONXp7XXz67uD9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PqlONXp7XXz67uD9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PqlONXp7XXz67uD9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PqlONXp7XXz67uD9 .cluster text{fill:#333;}#mermaid-svg-PqlONXp7XXz67uD9 .cluster span{color:#333;}#mermaid-svg-PqlONXp7XXz67uD9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PqlONXp7XXz67uD9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PqlONXp7XXz67uD9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PqlONXp7XXz67uD9 .icon-shape,#mermaid-svg-PqlONXp7XXz67uD9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PqlONXp7XXz67uD9 .icon-shape p,#mermaid-svg-PqlONXp7XXz67uD9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PqlONXp7XXz67uD9 .icon-shape .label rect,#mermaid-svg-PqlONXp7XXz67uD9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PqlONXp7XXz67uD9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PqlONXp7XXz67uD9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PqlONXp7XXz67uD9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否





开始
slow = fast = head
fast != null &&

fast.next != null ?
无环

return null
slow = slow.next

fast = fast.next.next
slow == fast ?
有环!
p = head
p != slow ?
p = p.next

slow = slow.next
p 即环入口

return p

七、相交链表:让两个指针走完相同总路程

题目:两个单链表可能在某个节点相交,找出第一个公共节点。

注意:这里的相交不是值相等,而是节点对象相同。

最优雅的写法是双指针换路:

java 复制代码
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    ListNode p = headA;
    ListNode q = headB;

    while (p != q) {
        p = (p == null) ? headB : p.next;
        q = (q == null) ? headA : q.next;
    }

    return p;
}

为什么这样能对齐?

假设:

  • A 独有长度 = a
  • B 独有长度 = b
  • 公共部分长度 = c

指针 p 走:a + c + b

指针 q 走:b + c + a

总路程相同。如果两个链表相交,它们会在公共节点相遇;如果不相交,它们会一起走到 null。

这道题的本质不是快慢,而是:通过换路,让两个指针走过相同长度,消除链表长度差。
#mermaid-svg-IgXgRy8N7drpG5CH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IgXgRy8N7drpG5CH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IgXgRy8N7drpG5CH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IgXgRy8N7drpG5CH .error-icon{fill:#552222;}#mermaid-svg-IgXgRy8N7drpG5CH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IgXgRy8N7drpG5CH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IgXgRy8N7drpG5CH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IgXgRy8N7drpG5CH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IgXgRy8N7drpG5CH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IgXgRy8N7drpG5CH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IgXgRy8N7drpG5CH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IgXgRy8N7drpG5CH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IgXgRy8N7drpG5CH .marker.cross{stroke:#333333;}#mermaid-svg-IgXgRy8N7drpG5CH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IgXgRy8N7drpG5CH p{margin:0;}#mermaid-svg-IgXgRy8N7drpG5CH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IgXgRy8N7drpG5CH .cluster-label text{fill:#333;}#mermaid-svg-IgXgRy8N7drpG5CH .cluster-label span{color:#333;}#mermaid-svg-IgXgRy8N7drpG5CH .cluster-label span p{background-color:transparent;}#mermaid-svg-IgXgRy8N7drpG5CH .label text,#mermaid-svg-IgXgRy8N7drpG5CH span{fill:#333;color:#333;}#mermaid-svg-IgXgRy8N7drpG5CH .node rect,#mermaid-svg-IgXgRy8N7drpG5CH .node circle,#mermaid-svg-IgXgRy8N7drpG5CH .node ellipse,#mermaid-svg-IgXgRy8N7drpG5CH .node polygon,#mermaid-svg-IgXgRy8N7drpG5CH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IgXgRy8N7drpG5CH .rough-node .label text,#mermaid-svg-IgXgRy8N7drpG5CH .node .label text,#mermaid-svg-IgXgRy8N7drpG5CH .image-shape .label,#mermaid-svg-IgXgRy8N7drpG5CH .icon-shape .label{text-anchor:middle;}#mermaid-svg-IgXgRy8N7drpG5CH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IgXgRy8N7drpG5CH .rough-node .label,#mermaid-svg-IgXgRy8N7drpG5CH .node .label,#mermaid-svg-IgXgRy8N7drpG5CH .image-shape .label,#mermaid-svg-IgXgRy8N7drpG5CH .icon-shape .label{text-align:center;}#mermaid-svg-IgXgRy8N7drpG5CH .node.clickable{cursor:pointer;}#mermaid-svg-IgXgRy8N7drpG5CH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IgXgRy8N7drpG5CH .arrowheadPath{fill:#333333;}#mermaid-svg-IgXgRy8N7drpG5CH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IgXgRy8N7drpG5CH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IgXgRy8N7drpG5CH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IgXgRy8N7drpG5CH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IgXgRy8N7drpG5CH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IgXgRy8N7drpG5CH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IgXgRy8N7drpG5CH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IgXgRy8N7drpG5CH .cluster text{fill:#333;}#mermaid-svg-IgXgRy8N7drpG5CH .cluster span{color:#333;}#mermaid-svg-IgXgRy8N7drpG5CH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IgXgRy8N7drpG5CH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IgXgRy8N7drpG5CH rect.text{fill:none;stroke-width:0;}#mermaid-svg-IgXgRy8N7drpG5CH .icon-shape,#mermaid-svg-IgXgRy8N7drpG5CH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IgXgRy8N7drpG5CH .icon-shape p,#mermaid-svg-IgXgRy8N7drpG5CH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IgXgRy8N7drpG5CH .icon-shape .label rect,#mermaid-svg-IgXgRy8N7drpG5CH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IgXgRy8N7drpG5CH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IgXgRy8N7drpG5CH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IgXgRy8N7drpG5CH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} q 换路走:b + c + a
q 走 B 链
走完 B 独有 b
走完公共 c
切到 A 链
走 A 独有 a
相遇点
p 换路走:a + c + b
p 走 A 链
走完 A 独有 a
走完公共 c
切到 B 链
走 B 独有 b
相遇点
B 链:b + c
headB
b

B 独有
c

公共
null
A 链:a + c
headA
a

A 独有
c

公共
null

八、面试实战:链表题的通用检查表

链表代码写完后,不要急着交。按下面这张表过一遍,能挡掉大部分 bug。

8.1 变量语义是否清楚

每个指针变量必须能用一句话解释:

变量 常见含义
dummy 虚拟头节点,统一头节点变化
prev 当前节点的前驱,或已处理部分的尾巴
cur 当前正在处理的节点
next 改指针前保存的后续链表
tail 结果链表的尾节点
fast/slow 制造距离差或速度差

如果你解释不清一个变量,它大概率是 bug 来源。

8.2 返回值是不是正确的新头

链表题最常见错误之一:返回旧的 head

下面这些场景,答案头节点可能改变:

  • 删除头节点
  • 反转链表
  • 合并链表
  • 局部反转从第一个节点开始
  • 分隔链表后重新拼接

用了哨兵,通常返回:

java 复制代码
return dummy.next;

反转整条链表,通常返回:

java 复制代码
return prev;

递归反转,通常返回:

java 复制代码
return newHead;

8.3 尾巴有没有断开

链表拆分、奇偶重排、局部拼接后,一定检查尾巴。

比如奇偶链表:

复制代码
1 -> 2 -> 3 -> 4 -> 5
输出:1 -> 3 -> 5 -> 2 -> 4

如果偶数链表的尾巴不置空,可能残留旧指针,形成错误连接甚至环。

常见收尾:

java 复制代码
tail.next = null;

8.4 空链表、单节点、两节点是否能过

链表题最适合用小样例验证:

复制代码
[]
[1]
[1,2]
[1,2,3]

如果是删除/反转类题,再额外测:

  • 删除头节点
  • 删除尾节点
  • 删除所有节点
  • 反转从第一个节点开始的区间

不要只测一个中间场景。链表 bug 基本都藏在边界上。

九、链表题识别信号

题目描述 优先想到
删除、插入、合并、拆分 哨兵节点 + 尾指针
反转整条链表 三指针迭代 / 递归
反转一段链表 哨兵 + 保存 before/start/after
倒数第 K 个、中点 快慢指针 / 前后指针
是否有环、环入口 快慢指针
两链表相交 双指针换路

结尾

链表题看起来变化很多,但主线其实很少。

第一,用哨兵节点统一边界。只要头节点可能变化,就先想 dummy。它能把"头节点特判"变成普通节点操作。

第二,反转链表抓住三指针不变量:prev 是已经反转好的部分,cur 是还没处理的部分,next 是改指针前保存的后路。

第三,递归不要展开调用栈。先定义函数语义,然后相信子问题已经解决,只处理当前节点和子问题结果之间的连接。递归反转难出来,不是因为代码长,而是因为你试图同时模拟所有层。

第四,快慢指针让距离变成信息。倒数第 K 个靠固定距离,环检测靠速度差,环入口靠相遇后的距离关系。

链表题最终考的不是你会不会写 node.next,而是你能不能始终知道:哪一段已经处理完?哪一段还没处理?我改完这根指针后,整条链还能不能接回去?

把这个问题问清楚,链表题就不再是一团乱线。