学习笔记七 —— 链表反转

95 阅读13分钟

题目:反转从位置 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 → 你(所有人转身反向拉手)

需要三个角色

  1. cur:当前准备转身的人(从队头的“你”开始)
  2. pre:当前人转身后要拉手的前一个人(初始无人,为null
  3. temp:临时助手,帮当前人记住身后的人(防止松手后找不到)

🔄 二、反转三步曲(迭代法核心)

每轮循环只做三件事,直到所有人转完身:

步骤动作代码图示变化
1. 存下一个cur先记住身后的人(防断联)temp = cur.next;👈记住→朋友A
2. 指向前一个cur转身拉pre的手cur.next = pre;nullpre初始为空)
3. 双指针前移precur向前移动一步pre = cur;
cur = temp;
pre=你, cur=朋友A 👉下一轮

完整流程图示

初始: [null] ← 你 👉 AB → C  
第1轮: null ← 你 👉 AB → C (你拉null,pre移到你,cur移到A)  
第2轮: 你 ← A 👉 B → C         (A拉你,pre移到A,cur移到B)  
第3轮: AB 👉 C              (BA,pre移到B,cur移到C)  
第4轮: B ← C 👉 null          (C拉B,cur为null结束)  
结果: C → BA → 你 → null  

⚠️ 三、边界与陷阱

  1. 空链表:直接返回null
  2. 单节点:无需操作,直接返回自身
  3. 易错点
    • 忘记存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]

💎 总结

  • 本质:通过curpre的拉手关系变更,逐步传递反转效果。
  • 口诀一存、二指、三前移(90%面试题靠此解决)。
  • 测试:用1→2→3手动模拟一遍,理解指针如何“跳交谊舞”💃。

下次写代码时默念口诀:存下一个 → 指向前一个 → pre/cur向前挪,三步行云流水,再也不怕指针绕晕!

链表部分反转(指定区间反转)是面试高频题,其核心在于精准定位反转区间边界并重组指针,同时确保一趟扫描完成。以下结合四指针法(prereversedHeadreversedTailpost)深度解析原理与实现,附完整代码及示意图。


🔧 一、问题本质与难点

  • 目标:反转链表中第 mn 的节点(位置从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],并记录 reversedHeadpost

    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

⚠️ 四、边界处理与注意事项

  1. m=1 的情况
    虚拟头节点(dummy)保证 pre 始终存在,避免头指针丢失。

  2. n 超出链表长度
    循环中需检查 cur 非空(若提前到末尾则终止反转)。

  3. 区间重叠(m=n
    直接返回原链表,无需操作。

  4. 指针断裂风险
    反转时需暂存 next 节点(next = cur.next),防止链表断裂。


📊 五、复杂度与面试考点

项目说明面试考点
时间复杂度O(n),一趟扫描单次遍历的实现能力
空间复杂度O(1),仅用指针空间优化意识
高频考察点四指针含义、边界处理、指针重组逻辑代码严谨性与思维清晰度
变种题型K个一组反转(LeetCode 25)递归/迭代实现区间反转的复用

🔍 六、拓展:其他链表反转变种题

  1. 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;
    }
    
  2. 重排链表(LeetCode 143)

    • 三步法:找中点 → 反转后半段 → 合并前后段:
    // 1. 快慢指针找中点
    // 2. 反转后半段(reverseList)
    // 3. 合并:l1: 1→2→3, l2: 5→4 → 合并为 1→5→2→4→3
    
  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]

以下是对头插法反转链表区间(从位置 mn)的逐步拆解。结合图示和分步代码分析,帮助你直观理解指针如何变化。核心思想:将区间内的节点逐个“插入”到反转区间的前驱节点(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

  1. start.next = then.next
    → 断开 232 直接指向 4
    dummy → 1 → 2 → 4 → 5  
              ↑  
             then → 3(游离)
    
  2. then.next = prev.next
    → 让游离的 3 指向 prev 的后继(即 2):
    dummy → 1 → 2 → 4 → 5  
              ↗  
             3
    
  3. prev.next = then
    → 让 prev(节点 1)指向 3,完成插入:
    dummy → 1 → 3 → 2 → 4 → 5  
    
  4. then = start.next
    → 更新 thenstart 的后继(即 4):
    then = 4
    

第2轮循环(i=1

  1. start.next = then.next
    → 断开 242 指向 5
    dummy → 1 → 3 → 2 → 5  
              ↑  
             then → 4(游离)
    
  2. then.next = prev.next
    → 让 4 指向 prev 的后继(即 3):
    dummy → 1 → 3 → 2 → 5  
              ↗  
             4
    
  3. prev.next = then
    prev(节点 1)指向 4
    dummy → 1 → 4 → 3 → 2 → 5
    
  4. then = start.next
    then = 5(循环结束)

📊 关键指针作用

指针角色初始值作用
prev反转区间的前驱锚点dummy(位置 m-1新节点始终插入到 prev 后方
start反转区间的原始头(反转后变尾)prev.next(位置 m固定不动,用于连接区间后的节点
then当前待移动节点start.next(位置 m+1被提取并插入到 prev 后方的节点

💡 形象比喻

  • prev 是“施工入口”,所有新砖(节点)必须经过此门;
  • start 是“固定桩”,位置不变但连接关系变化;
  • then 是“移动砖块”,逐个被搬到 prev 身后。

⚠️ 易错点与边界处理

  1. 循环次数为何是 n-m
    区间共 n-m+1 个节点,但只需移动 n-m 次(例如 m=2, n=4 时需移动 34 两个节点)。
  2. dummy 的作用是什么?
    处理 m=1(反转头部)的情况,避免头指针丢失。
  3. 为何 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)之后,实现局部反转
四步口诀
① 拆 thenstart.next = then.next)→ 防止断链;
② 连前方(then.next = prev.next)→ 让 then 指向 prev 的后继;
③ 接 prevprev.next = then)→ 完成插入;
④ 移 thenthen = 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→AB→C→D
    第1步:prev→BA→C→D  (B插入prev后)
    第2步:prev→C→BA→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→5prev=1, start=2, then=3
i=0dummy→1→3→2→4→5then=3插入1后 → then=4
i=1dummy→1→4→3→2→5then=4插入1后 → 结束

完整动图演示见。


💡 四、两种解法对比总结

对比维度四指针法头插法
时间复杂度O(2n)O(n)
空间复杂度O(1)O(1)
遍历次数2次(定位+反转)1次
指针数量4个3个
优势逻辑直观,易理解反转过程性能最优,满足单次遍历要求
推荐场景学习反转原理笔试/面试实战

⚠️ 若题目明确要求“一趟扫描”,必须使用头插法。


💎 给你的建议

  1. 掌握头插法即可:面试中90%的链表区间反转题要求一趟扫描,头插法是标准答案
  2. 理解指针含义
    • prev:反转区间的前驱节点(锚点)
    • start:反转区间的原始头(反转后变为尾)
    • then:当前被操作的“移动节点”
  3. 死记三步循环
    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=链表长度(如反转尾部)验证边界。