链表题核心技巧与解题框架
链表的数据结构不复杂:一个节点,一个 next 指针,最多再加一个 prev 或 random。但很多人写链表题时,比写动态规划还容易崩。
原因不是链表难,而是链表题特别容易在边界处出错:
- 删除头节点怎么办?
- 插入第一个节点怎么办?
- 反转后新头是谁?
- 当前节点的
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 表示还没处理的部分。
所以递归函数不是只有一种定义方式。关键不在于"必须定义成哪一句",而在于:你定义的函数语义,必须能让子问题变小,并且能让当前层把结果接回来。
更具体一点,一个递归语义只要满足三个条件,就有机会写对:
- 子问题变小:每次递归处理的范围,比当前问题少一点
- 返回值清楚:当前层知道递归调用会还给自己什么
- 当前层能收尾:拿到子问题结果后,当前层能完成自己的连接逻辑
普通递归版选择"反转链表并返回新头",是因为它刚好贴合题目的返回值,也让当前层只剩两件事:把 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 递归反转的记忆口诀
递归反转只记三句话:
- 子链表先反转好
- 让后一个节点指回当前节点
- 当前节点断开原来的
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
思路:
fast先走 k 步fast和slow一起走- 当
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;
}
这里的不变量是:fast 和 slow 之间始终相差 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 + bfast走了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,而是你能不能始终知道:哪一段已经处理完?哪一段还没处理?我改完这根指针后,整条链还能不能接回去?
把这个问题问清楚,链表题就不再是一团乱线。