题目:反转从位置 m 到 n 的节点(一趟扫描完成)
示例:1→2→3→4→5,m=2, n=4 → 1→4→3→2→5
考点:四指针法(记录 pre、reversedHead、reversedTail、post)
链表反转的原理其实可以想象成“一群人排队转身”的过程,下面我用最生活化的方式拆解,配合图示和分步代码,帮你彻底理解其中的指针操作逻辑!
🧍♂️ 一、场景化理解:排队转身
想象一排人(链表节点)手拉手站成一队:
初始:你 → 朋友A → 朋友B → 朋友C(箭头代表拉手方向)
目标:朋友C → 朋友B → 朋友A → 你(所有人转身反向拉手)
需要三个角色:
cur:当前准备转身的人(从队头的“你”开始)pre:当前人转身后要拉手的前一个人(初始无人,为null)temp:临时助手,帮当前人记住身后的人(防止松手后找不到)
🔄 二、反转三步曲(迭代法核心)
每轮循环只做三件事,直到所有人转完身:
| 步骤 | 动作 | 代码 | 图示变化 |
|---|---|---|---|
| 1. 存下一个 | cur先记住身后的人(防断联) | temp = cur.next; | 你👈记住→朋友A |
| 2. 指向前一个 | cur转身拉pre的手 | cur.next = pre; | 你→null(pre初始为空) |
| 3. 双指针前移 | pre和cur向前移动一步 | pre = cur;cur = temp; | pre=你, cur=朋友A 👉下一轮 |
完整流程图示:
初始: [null] ← 你 👉 A → B → C
第1轮: null ← 你 👉 A → B → C (你拉null,pre移到你,cur移到A)
第2轮: 你 ← A 👉 B → C (A拉你,pre移到A,cur移到B)
第3轮: A ← B 👉 C (B拉A,pre移到B,cur移到C)
第4轮: B ← C 👉 null (C拉B,cur为null结束)
结果: C → B → A → 你 → null
⚠️ 三、边界与陷阱
- 空链表:直接返回
null - 单节点:无需操作,直接返回自身
- 易错点:
- 忘记存
temp导致断链 - 循环结束后未返回
pre(新队头) - 递归法未置
head.next=null(形成环)
- 忘记存
💻 四、代码实现(JavaScript)
function reverseList(head) {
// 边界:空或单节点直接返回
if (!head || !head.next) return head;
let pre = null; // 前面没人
let cur = head; // 从队头开始
let temp; // 临时助手
while (cur) {
temp = cur.next; // 1. 先记住身后人
cur.next = pre; // 2. 转身拉前人手
pre = cur; // 3. pre移到当前位置
cur = temp; // 4. cur移到下一个人
}
return pre; // 最后pre是新队头
}
🆚 五、三种方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 迭代法 | O(n) | O(1) | 面试首选,效率高 |
| 递归法 | O(n) | O(n)(栈空间) | 代码简洁但易栈溢出 |
| 头插法 | O(n) | O(1) | 需虚拟头节点[dummy] |
💎 总结
- 本质:通过
cur和pre的拉手关系变更,逐步传递反转效果。 - 口诀:一存、二指、三前移(90%面试题靠此解决)。
- 测试:用
1→2→3手动模拟一遍,理解指针如何“跳交谊舞”💃。
下次写代码时默念口诀:存下一个 → 指向前一个 → pre/cur向前挪,三步行云流水,再也不怕指针绕晕!
链表部分反转(指定区间反转)是面试高频题,其核心在于精准定位反转区间边界并重组指针,同时确保一趟扫描完成。以下结合四指针法(pre、reversedHead、reversedTail、post)深度解析原理与实现,附完整代码及示意图。
🔧 一、问题本质与难点
- 目标:反转链表中第
m到n的节点(位置从1计数),其他节点顺序不变。 - 难点:
- 边界处理:
m=1时需更新链表头;n为链表末尾时需正确处理尾指针。 - 指针重组:需在反转后,将反转区间的前驱(
pre)与新区间头连接,新区间尾(reversedTail)与后继(post)连接。 - 一趟扫描:禁止多次遍历链表,需在单次遍历中定位并反转。
- 边界处理:
⚙️ 二、核心原理:四指针法
四指针的作用与意义:
| 指针 | 作用 | 初始化位置 |
|---|---|---|
pre | 指向反转区间的前驱节点(位置 m-1) | 虚拟头节点(dummy) |
reversedTail | 指向反转区间的原始头节点(反转后变为尾) | pre.next(即位置 m 节点) |
reversedHead | 指向反转区间的新头节点(位置 n 节点) | 初始为 null,反转后更新 |
post | 指向反转区间的后继节点(位置 n+1) | 反转区间遍历结束时记录 |
形象比喻:
pre是施工队入口引导员,负责连接旧路与新路;reversedTail是旧路起点标记牌,反转后变成终点;reversedHead是新路起点,由施工队(反转操作)动态更新;post是旧路出口标记牌,防止施工后找不到原路。
🛠️ 三、实现步骤详解(一趟扫描)
步骤1:初始化虚拟头节点(处理 m=1 的边界)
function reverseBetween(head, m, n) {
const dummy = new ListNode(0); // 虚拟头节点
dummy.next = head;
let pre = dummy;
// 移动 pre 到反转区间前驱(位置 m-1)
for (let i = 0; i < m - 1; i++) {
pre = pre.next;
}
// reversedTail 是反转区间的原始头节点(位置 m)
let reversedTail = pre.next;
}
步骤2:反转区间 [m, n],并记录 reversedHead 和 post
let cur = reversedTail; // 当前待反转节点
let next = null;
let reversedHead = null; // 反转后的新区间头
for (let i = m; i <= n; i++) {
next = cur.next;
cur.next = reversedHead; // 将当前节点指向新区间头
reversedHead = cur; // 更新新区间头
cur = next; // 移动至下一节点
}
// 循环结束时:
// - reversedHead 指向位置 n 的节点(新区间头)
// - cur 指向位置 n+1 的节点(即 post)
let post = cur;
步骤3:重组链表
pre.next = reversedHead; // 前驱指向新区间头
reversedTail.next = post; // 新区间尾指向后继
return dummy.next; // 返回真实头节点
反转过程示意图(以 1→2→3→4→5, m=2, n=4 为例)
初始:dummy → 1 → 2 → 3 → 4 → 5
pre reversedTail (cur)
反转区间 [2,4]:
Step1: 2 → reversedHead(null) → 更新 reversedHead=2
Step2: 3 → reversedHead(2) → 更新 reversedHead=3
Step3: 4 → reversedHead(3) → 更新 reversedHead=4
结束:reversedHead=4, post=5
重组:
pre(1) → reversedHead(4) → ... → reversedTail(2) → post(5)
结果:dummy → 1 → 4 → 3 → 2 → 5
⚠️ 四、边界处理与注意事项
-
m=1的情况:
虚拟头节点(dummy)保证pre始终存在,避免头指针丢失。 -
n超出链表长度:
循环中需检查cur非空(若提前到末尾则终止反转)。 -
区间重叠(
m=n):
直接返回原链表,无需操作。 -
指针断裂风险:
反转时需暂存next节点(next = cur.next),防止链表断裂。
📊 五、复杂度与面试考点
| 项目 | 说明 | 面试考点 |
|---|---|---|
| 时间复杂度 | O(n),一趟扫描 | 单次遍历的实现能力 |
| 空间复杂度 | O(1),仅用指针 | 空间优化意识 |
| 高频考察点 | 四指针含义、边界处理、指针重组逻辑 | 代码严谨性与思维清晰度 |
| 变种题型 | K个一组反转(LeetCode 25) | 递归/迭代实现区间反转的复用 |
🔍 六、拓展:其他链表反转变种题
-
K个一组反转(LeetCode 25)
- 核心:递归反转每组,连接相邻组头尾:
function reverseKGroup(head, k) { let tail = head; for (let i = 0; i < k; i++) { if (!tail) return head; // 不足k个不反转 tail = tail.next; } const newHead = reverse(head, tail); // 反转 [head, tail) head.next = reverseKGroup(tail, k); // 连接下一组 return newHead; } -
重排链表(LeetCode 143)
- 三步法:找中点 → 反转后半段 → 合并前后段:
// 1. 快慢指针找中点 // 2. 反转后半段(reverseList) // 3. 合并:l1: 1→2→3, l2: 5→4 → 合并为 1→5→2→4→3 -
两两交换节点(LeetCode 24)
- K=2 的特例,可用迭代法:
while (temp.next && temp.next.next) { const node1 = temp.next; const node2 = node1.next; temp.next = node2; node1.next = node2.next; node2.next = node1; temp = node1; }
💎 总结
- 核心口诀:
虚头保边界,四针定乾坤;反转区间内,头尾再接回。 - 面试技巧:
主动解释四指针作用,手写时标注边界条件(如m=1),对比递归与迭代的优劣。 - 代码完整实现:
function reverseBetween(head, m, n) { const dummy = new ListNode(0, head); let pre = dummy; for (let i = 0; i < m - 1; i++) pre = pre.next; let reversedTail = pre.next; let reversedHead = null; let cur = reversedTail; for (let i = m; i <= n; i++) { const next = cur.next; cur.next = reversedHead; reversedHead = cur; cur = next; } pre.next = reversedHead; reversedTail.next = cur; // cur 即 post return dummy.next; }测试用例:
head = [1,2,3,4,5], m=2, n=4→ 输出[1,4,3,2,5]。
以下是对头插法反转链表区间(从位置 m 到 n)的逐步拆解。结合图示和分步代码分析,帮助你直观理解指针如何变化。核心思想:将区间内的节点逐个“插入”到反转区间的前驱节点(prev)之后,实现局部反转。
🔧 头插法四步拆解(以 1→2→3→4→5, m=2, n=4 为例)
初始状态
dummy → 1 → 2 → 3 → 4 → 5
↑ ↑
prev start (prev.next)
then = start.next = 3
第1轮循环(i=0)
start.next = then.next
→ 断开2和3,2直接指向4:dummy → 1 → 2 → 4 → 5 ↑ then → 3(游离)then.next = prev.next
→ 让游离的3指向prev的后继(即2):dummy → 1 → 2 → 4 → 5 ↗ 3prev.next = then
→ 让prev(节点1)指向3,完成插入:dummy → 1 → 3 → 2 → 4 → 5then = start.next
→ 更新then为start的后继(即4):then = 4
第2轮循环(i=1)
start.next = then.next
→ 断开2和4,2指向5:dummy → 1 → 3 → 2 → 5 ↑ then → 4(游离)then.next = prev.next
→ 让4指向prev的后继(即3):dummy → 1 → 3 → 2 → 5 ↗ 4prev.next = then
→prev(节点1)指向4:dummy → 1 → 4 → 3 → 2 → 5then = start.next
→then = 5(循环结束)
📊 关键指针作用
| 指针 | 角色 | 初始值 | 作用 |
|---|---|---|---|
prev | 反转区间的前驱锚点 | dummy(位置 m-1) | 新节点始终插入到 prev 后方 |
start | 反转区间的原始头(反转后变尾) | prev.next(位置 m) | 固定不动,用于连接区间后的节点 |
then | 当前待移动节点 | start.next(位置 m+1) | 被提取并插入到 prev 后方的节点 |
💡 形象比喻:
prev是“施工入口”,所有新砖(节点)必须经过此门;start是“固定桩”,位置不变但连接关系变化;then是“移动砖块”,逐个被搬到prev身后。
⚠️ 易错点与边界处理
- 循环次数为何是
n-m?
区间共n-m+1个节点,但只需移动n-m次(例如m=2, n=4时需移动3和4两个节点)。 dummy的作用是什么?
处理m=1(反转头部)的情况,避免头指针丢失。- 为何
start始终不变?
start是反转区间的原始头节点,反转后变成尾部。它像锚一样固定位置,用于连接剩余链表(start.next = then)。
💡 头插法 vs. 四指针法
| 方法 | 遍历次数 | 核心操作 | 适用场景 |
|---|---|---|---|
| 头插法 | 1次 | 边定位边反转(prev后插入) | 面试要求一趟扫描 |
| 四指针法 | 2次 | 先定位再整体反转子链 | 理解反转原理更直观 |
若题目明确要求一趟扫描,头插法是唯一选择。
🌰 测试用例验证
// 输入: 1→2→3→4→5, m=2, n=4
// 预期输出: 1→4→3→2→5
const head = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5))));
const result = reverseBetween(head, 2, 4);
// 输出: 1 → 4 → 3 → 2 → 5
💎 总结
头插法的本质是:通过反复将后续节点(then)插入前驱节点(prev)之后,实现局部反转。
四步口诀:
① 拆 then(start.next = then.next)→ 防止断链;
② 连前方(then.next = prev.next)→ 让 then 指向 prev 的后继;
③ 接 prev(prev.next = then)→ 完成插入;
④ 移 then(then = start.next)→ 准备下一块“砖”。
建议用纸笔模拟 m=2, n=4 的流程,画图后瞬间通透!
你在链表区间反转问题上遇到的理解差异,其实源于两种主流解法在实现思路上的本质区别——它们都是正确的,但采用了不同的指针操作逻辑。下面我从原理、步骤、代码三个维度为你透彻拆解:
🔧 一、两种解法的核心区别
| 解法类型 | 指针角色 | 反转逻辑 | 遍历次数 | 适用场景 |
|---|---|---|---|---|
| 四指针法 (你第一次看到的解法) | pre(前驱)reversedHead(新头)reversedTail(新尾)post(后继) | 先定位区间边界 → 整体反转子链 → 拼接首尾 | 2次遍历 (定位+反转) | 思路直观,适合理解反转原理 |
| 头插法 (你第二次看到的解法) | dummy(虚拟头)prev(前驱)start(区间头)then(当前节点) | 定位前驱后 → 逐个将节点插入前驱后方 | 1次遍历 (定位后原地反转) | 面试高频,空间最优 |
💡 关键结论:两种解法均正确,差异在于是否严格满足“一趟扫描”。头插法才是题目要求的单次遍历实现。
⚙️ 二、四指针法的原理与问题
1. 正确性分析
- 定位阶段:需遍历到位置
m-1(如m=2时找节点1) - 反转阶段:再遍历
n-m+1个节点(如反转2→3→4) - 拼接阶段:将反转后的子链头尾连接前后节点
2. 你代码中的问题
// 错误1:pre 定位过远 → 应停在 m-1 而非 m
for(let i=0; i<= m-1; i++) { // 循环 m 次 → pre 指向第 m 个节点
pre = pre.next;
}
// 正确应为:for (let i=0; i < m-1; i++)
// 错误2:反转逻辑成环
cur.next = reservedHead; // 导致 2→1 且 1→2 形成环
// 正确应为:cur.next = prev(暂存前节点)
四指针法完整实现可参考。
🚀 三、头插法(一趟扫描)详解
1. 核心思想
- 边定位边反转:在定位到
prev(位置m-1)后,通过不断将后续节点插入prev后方实现反转:初始:prev→A→B→C→D 第1步:prev→B→A→C→D (B插入prev后) 第2步:prev→C→B→A→D (C插入prev后)
2. 正确代码实现
function reverseBetween(head, m, n) {
const dummy = new ListNode(0, head); // 虚拟头节点
let prev = dummy;
// Step1:移动prev到位置 m-1
for (let i = 0; i < m - 1; i++) {
prev = prev.next;
}
// Step2:初始化关键指针
let start = prev.next; // 反转区间头节点(反转后变为尾)
let then = start.next; // 当前待操作节点
// Step3:头插法反转(操作 n-m 次)
for (let i = 0; i < n - m; i++) {
start.next = then.next; // 移除then节点
then.next = prev.next; // then插入prev后方
prev.next = then;
then = start.next; // 更新then指向下一节点
}
return dummy.next;
}
3. 流程图示(以 m=2, n=4 为例)
| 步骤 | 链表状态 | 指针变化 |
|---|---|---|
| 初始 | dummy→1→2→3→4→5 | prev=1, start=2, then=3 |
| i=0 | dummy→1→3→2→4→5 | then=3插入1后 → then=4 |
| i=1 | dummy→1→4→3→2→5 | then=4插入1后 → 结束 |
完整动图演示见。
💡 四、两种解法对比总结
| 对比维度 | 四指针法 | 头插法 |
|---|---|---|
| 时间复杂度 | O(2n) | O(n) |
| 空间复杂度 | O(1) | O(1) |
| 遍历次数 | 2次(定位+反转) | 1次 |
| 指针数量 | 4个 | 3个 |
| 优势 | 逻辑直观,易理解反转过程 | 性能最优,满足单次遍历要求 |
| 推荐场景 | 学习反转原理 | 笔试/面试实战 |
⚠️ 若题目明确要求“一趟扫描”,必须使用头插法。
💎 给你的建议
- 掌握头插法即可:面试中90%的链表区间反转题要求一趟扫描,头插法是标准答案
- 理解指针含义:
prev:反转区间的前驱节点(锚点)start:反转区间的原始头(反转后变为尾)then:当前被操作的“移动节点”
- 死记三步循环:
start.next = then.next; // 1. 移除then then.next = prev.next; // 2. then指向prev的下一个 prev.next = then; // 3. prev连接then then = start.next; // 4. 更新then
测试时用
m=1(如反转头部)和n=链表长度(如反转尾部)验证边界。