欢迎光临
我们一直在努力

【LeetCode刷题日志】链表删除与交换双经典:19.删除链表的倒数第N个结点 & 24.两两交换链表中的节点(C++详解)

【LeetCode刷题日志】链表删除与交换双经典:19.删除链表的倒数第N个结点 & 24.两两交换链表中的节点(C++详解)

文章包含完整的题目描述、前置知识、解题思路、C++ 代码逐行解释、复杂度分析、核心技巧、踩坑记录、可复用模板和学习感悟。


📑 目录

  • 📋 本篇日志包含的题目
  • 📖 开始前必须搞懂的 5 个概念
    • 概念1:什么是"倒数第N个结点"?
    • 概念2:为什么fast要先走n步?(快慢指针找倒数第N个)
    • 概念3:什么是递归回溯计数?
    • 概念4:两两交换的三步指针操作
    • 概念5:虚拟头节点(dummy)的作用
  • 一、力扣 19. 删除链表的倒数第N个结点
    • 1.1 题目描述
    • 1.2 解题思路
    • 1.3 C++ 代码实现(逐行解释)
    • 1.4 复杂度分析
    • 1.5 核心技巧与踩坑记录
  • 二、力扣 24. 两两交换链表中的节点
    • 2.1 题目描述
    • 2.2 解题思路
    • 2.3 C++ 代码实现(逐行解释)
    • 2.4 复杂度分析
    • 2.5 核心技巧与踩坑记录
  • 三、两题关联与进阶思考
    • 3.1 从 19 到 24 的链表指针操作思维跃迁
    • 3.2 可复用模板汇总(其他题目直接套用)
  • 四、刷题心得与学习感悟
    • 4.1 这两题能学到什么?
    • 4.2 高频算法总结
    • 4.3 新手常见误区
    • 4.4 我的刷题方法分享
  • 五、总结

📋 本篇日志包含的题目

序号题号题目名称难度核心考点核心方法
1 19 删除链表的倒数第N个结点 中等 找倒数第N个 / 快慢指针 / 递归回溯 快慢指针(fast先走n步)+ 递归回溯计数
2 24 两两交换链表中的节点 中等 两两交换 / 虚拟头节点 / 递归 迭代法(虚拟头节点+三步交换)+ 递归法

💡 为什么把这两题放在一起? 第19题教你"如何找到并删除倒数第N个节点",核心是快慢指针的"速度差"思想;第24题教你"如何两两交换相邻节点",核心是虚拟头节点+三步指针操作。两题都涉及链表的指针修改,而且都能用迭代和递归两种方法解决,对比学习效果更好。


📖 开始前必须搞懂的 5 个概念

这部分是给零基础同学准备的,如果你已经熟悉这些概念,可以直接跳到正文。

概念1:什么是"倒数第N个结点"?

链表是单向的,只能从头往后走,不能从尾往前走。那"倒数第N个"怎么找?

链表:1 → 2 → 3 → 4 → 5
倒数第1个 = 5(最后一个)
倒数第2个 = 4
倒数第3个 = 3

  • 倒数第N个,就是从尾巴往回数第N个节点
  • 因为链表不能往回走,所以需要用技巧来找

概念2:为什么fast要先走n步?(快慢指针找倒数第N个)

这是19题快慢指针法的核心思想:

设链表长度为 L,要找倒数第 n 个
那么倒数第 n 个的位置是第 L-n+1 个(从1开始数)

做法:

  • 让fast先走 n 步,此时fast在第 n+1 个位置
  • 然后slow和fast同时走,每次都走一步
  • 当fast走到最后一个节点(fast->next == nullptr)时,fast一共走了 L 步
  • 因为fast先走了n步,所以同时走的阶段走了 L-n 步
  • slow也走了 L-n 步,此时slow在第 L-n+1 个位置——正好是倒数第n个的前驱!
  • 💡 关键:slow和fast都从dummy(虚拟头节点)开始,这样slow最终停在待删节点的前驱,方便删除。

    概念3:什么是递归回溯计数?

    19题的递归法用了一个很巧妙的思路:

    • 先一路递归到链表末尾(head == nullptr时返回)
    • 然后在回溯(从末尾往回走)的时候,用count计数
    • count从0开始,每回溯一层count++
    • 当count == n时,说明当前节点就是倒数第n个,返回head->next跳过它

    递归深入:1 → 2 → 3 → 4 → 5 → nullptr
    回溯计数:← count=1 ← count=2 ← count=3 ← count=4 ← count=5
    (5) (4) (3) (2) (1)

    回溯时count=1对应节点5(倒数第1个),count=2对应节点4(倒数第2个),以此类推。

    概念4:两两交换的三步指针操作

    24题迭代法中,交换node1和node2需要三步:

    交换前:cur → node1 → node2 → node3

    第一步:cur->next = node2(cur连到node2)

    cur → node2 → node3
    node1 ↗

    第二步:node1->next = node2->next(node1连到node3)

    cur → node2 → node3
    node1 ↗ ↑
    node1->next

    第三步:node2->next = node1(node2连到node1)

    cur → node2 → node1 → node3

    交换完成!然后cur = node1,继续处理下一对。

    概念5:虚拟头节点(dummy)的作用

    19题和24题的迭代法都用了虚拟头节点:

    ListNode* dummy = new ListNode(0, head);

    为什么需要虚拟头节点?

    • 19题:如果要删除的是头节点(倒数第n个正好是head),没有dummy的话删除头节点需要特殊处理。有了dummy,头节点和其他节点一样处理,统一返回dummy->next。
    • 24题:交换第一对节点时,头节点会变(原来的第二个节点变成新的头节点)。有了dummy,cur从dummy开始,第一对和后面的对一样处理,统一返回dummy->next。

    💡 一句话总结:虚拟头节点让"头节点的操作"和"其他节点的操作"统一起来,不需要写特殊的边界处理代码。


    一、力扣 19. 删除链表的倒数第N个结点

    1.1 题目描述

    给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

    示例 1:

    输入:head = [1,2,3,4,5], n = 2
    输出:[1,2,3,5]
    解释:删除倒数第2个节点(值为4的节点)

    示例 2:

    输入:head = [1], n = 1
    输出:[]
    解释:删除唯一的节点

    示例 3:

    输入:head = [1,2], n = 1
    输出:[1]
    解释:删除最后一个节点(值为2的节点)

    提示:

    • 链表中结点的数目为 sz
    • 1 <= sz <= 30
    • 0 <= Node.val <= 100
    • 1 <= n <= sz

    进阶: 你能尝试使用一趟扫描实现吗?


    1.2 解题思路

    思路一:快慢指针法(fast先走n步)—— 推荐

    核心思想: 用两个指针slow和fast,都从虚拟头节点dummy出发。先让fast先走n步,然后slow和fast同时走(每次都走一步),直到fast走到最后一个节点(fast->next == nullptr)。此时slow正好停在倒数第n个节点的前驱,删除slow->next即可。

    为什么fast先走n步就能找到倒数第n个?

    • 设链表长度为 L,fast先走n步后,fast在第n+1个位置(从dummy开始算)
    • 然后slow和fast同时走,当fast->next == nullptr时,fast在最后一个节点
    • fast从第n+1个位置走到最后一个节点,走了 L-n 步
    • slow也走了 L-n 步,从dummy出发走 L-n 步,正好停在第 L-n 个位置
    • 倒数第n个节点在第 L-n+1 个位置,所以slow是它的前驱!

    手动模拟:以 [1→2→3→4→5], n=2 为例

    链表:dummy → 1 → 2 → 3 → 4 → 5
    要删除倒数第2个(节点4)

    第一步:fast先走n=2步

    步骤fast位置
    初始 dummy
    走1步 1
    走2步 2

    fast现在在节点2。

    第二步:slow和fast同时走,直到fast->next == nullptr

    轮次slow位置fast位置fast->next==nullptr?
    初始 dummy 2 否(fast->next=3)
    第1轮 1 3 否(fast->next=4)
    第2轮 2 4 否(fast->next=5)
    第3轮 3 5 是!(fast->next=nullptr)

    slow现在在节点3,正好是待删节点4的前驱!

    第三步:删除slow->next(节点4)

    slow->next = slow->next->next // 3 → 5,跳过4
    delete delNode // 释放节点4

    结果:dummy → 1 → 2 → 3 → 5 ✅

    思路二:递归法(回溯计数)

    核心思想: 先递归深入到链表末尾,然后在回溯的时候用count计数。count从0开始,每回溯一层count++。当count == n时,说明当前节点就是倒数第n个,返回head->next跳过它(相当于删除);否则返回head。

    为什么回溯时count==n就是倒数第n个?

    • 递归深入到nullptr时开始回溯
    • 回溯第一层(最后一个节点)count=1,对应倒数第1个
    • 回溯第二层count=2,对应倒数第2个
    • …
    • 回溯第n层count=n,对应倒数第n个

    手动模拟:以 [1→2→3→4→5], n=2 为例

    递归深入阶段:

    removeNthFromEnd(1,2)
    → removeNthFromEnd(2,2)
    → removeNthFromEnd(3,2)
    → removeNthFromEnd(4,2)
    → removeNthFromEnd(5,2)
    → removeNthFromEnd(nullptr,2) → 返回nullptr

    回溯阶段(从下往上):

    回溯层当前节点countcount==n?操作返回
    第1层 5 1 否 5->next=nullptr 返回5
    第2层 4 2 是! 4->next=5,返回4->next=5 返回5(跳过4)
    第3层 3 3 否 3->next=5(上一层返回的) 返回3
    第4层 2 4 否 2->next=3 返回2
    第5层 1 5 否 1->next=2 返回1

    最终返回节点1,链表变为 1→2→3→5 ✅

    ⚠️ 注意:count是类的成员变量,在递归过程中共享。每次调用函数count都会累加。


    1.3 C++ 代码实现(逐行解释)

    方法一:快慢指针法(fast先走n步)

    class Solution {
    public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
    // 创建虚拟头节点,next指向head
    ListNode* dummy = new ListNode(0, head);
    // slow和fast都从dummy开始
    ListNode* slow = dummy;
    ListNode* fast = dummy;

    // fast先走n步
    for (int i = 0; i < n; i++) {
    fast = fast->next;
    }

    // slow和fast同时走,直到fast走到最后一个节点
    while (fast->next != nullptr) {
    slow = slow->next;
    fast = fast->next;
    }

    // 此时slow是待删节点的前驱,删除slow->next
    ListNode* delNode = slow->next; // 保存待删节点
    slow->next = slow->next->next; // 跳过待删节点
    delete delNode; // 释放内存

    // 返回新的头节点
    return dummy->next;
    }
    };

    关键代码逐行解释:

  • ListNode* dummy = new ListNode(0, head);

    • 创建虚拟头节点,val=0,next指向head
    • 有了dummy,删除头节点也不需要特殊处理
  • ListNode* slow = dummy; ListNode* fast = dummy;

    • slow和fast都从dummy开始
    • 都从dummy开始,slow最终会停在待删节点的前驱
  • for (int i = 0; i < n; i++) { fast = fast->next; }

    • fast先走n步
    • 这是快慢指针找倒数第n个的关键:制造n步的"距离差"
  • while (fast->next != nullptr)

    • 只要fast还没走到最后一个节点,就继续走
    • 注意条件是 fast->next != nullptr,不是 fast != nullptr
    • 因为我们要让fast停在最后一个节点,这样slow才停在待删节点的前驱
  • slow = slow->next; fast = fast->next;

    • slow和fast同时走一步
    • 保持n步的距离差
  • ListNode* delNode = slow->next;

    • 保存待删节点,后面要delete释放内存
  • slow->next = slow->next->next;

    • 核心删除操作:让slow的next跳过待删节点,直接连到待删节点的下一个
    • 这样待删节点就从链表中移除了
  • delete delNode;

    • 释放待删节点的内存
    • 好习惯:删除节点后要释放内存,避免内存泄漏
  • return dummy->next;

    • 返回新的头节点
    • 用dummy->next而不是head,因为head可能被删除了
  • 方法二:递归法(回溯计数)

    class Solution {
    public:
    // count是成员变量,在递归过程中共享
    int count = 0;

    ListNode* removeNthFromEnd(ListNode* head, int n) {
    // 递归终止条件:走到末尾了
    if (head == nullptr) {
    return nullptr;
    }

    // 递归深入:先处理后面的节点
    head->next = removeNthFromEnd(head->next, n);

    // 回溯阶段:count++,从末尾往回数
    count++;

    // 如果count==n,说明当前节点是倒数第n个,返回head->next跳过它
    if (count == n) {
    return head->next;
    }

    // 否则正常返回当前节点
    return head;
    }
    };

    关键代码逐行解释:

  • int count = 0;

    • count是类的成员变量,在所有递归调用中共享
    • 初始为0,回溯时每经过一个节点count++
  • if (head == nullptr) { return nullptr; }

    • 递归终止条件:走到链表末尾了
    • 返回nullptr,开始回溯
  • head->next = removeNthFromEnd(head->next, n);

    • 递归深入:先递归处理head后面的节点
    • 把返回值赋给head->next(如果后面的节点被删除了,head->next会更新)
    • 这一行执行完后,递归已经回溯到当前层了
  • count++;

    • 回溯阶段:count加1
    • 因为是从末尾往回回溯,count=1对应最后一个节点(倒数第1个)
  • if (count == n) { return head->next; }

    • 如果count等于n,说明当前节点就是倒数第n个
    • 返回head->next,相当于跳过当前节点(删除它)
    • 上一层的head->next会接收到这个返回值,从而跳过被删节点
  • return head;

    • 否则正常返回当前节点
    • 上一层的head->next会接收到当前节点

  • 1.4 复杂度分析

    方法时间复杂度空间复杂度
    方法一:快慢指针法 O(L) O(1)
    方法二:递归法 O(L) O(L)(递归栈)

    说明:

    • L 是链表的长度
    • 快慢指针法只遍历链表一次(fast先走n步,再和slow一起走L-n步,总共L步),满足题目"进阶:一趟扫描"的要求
    • 递归法需要递归栈空间,空间O(L)

    1.5 核心技巧与踩坑记录

    核心技巧:

  • 快慢指针的"距离差"思想:fast先走n步制造距离差,然后同速走,fast到末尾时slow就在目标位置。这个思想不仅能找倒数第n个,还能找链表中点(fast走两步slow走一步)、判断环(fast走两步slow走一步)等。
  • 虚拟头节点统一处理:删除头节点是链表题的常见边界情况,用虚拟头节点可以让头节点和其他节点一样处理,不需要写特殊代码。
  • 递归回溯计数:递归法的巧妙之处在于"深入时不做事,回溯时计数"。因为回溯是从末尾往回走的,所以count自然就是"倒数第几个"。
  • 踩坑记录:

  • fast先走n步后,while条件写错:应该是 while (fast->next != nullptr),不是 while (fast != nullptr)。如果用后者,fast会走到nullptr,slow会停在待删节点而不是前驱,删除就会出错。
  • slow和fast不从dummy开始:如果从head开始,删除头节点时slow可能停在head本身,无法删除。必须从dummy开始,slow才能停在待删节点的前驱。
  • 忘记delete释放内存:删除节点后要 delete delNode 释放内存,否则会内存泄漏。虽然力扣不检查内存泄漏,但这是好习惯。
  • 递归法count不是成员变量:count必须是类的成员变量,在所有递归调用中共享。如果写成局部变量,每次递归调用都会重新初始化为0,计数就错了。
  • 递归法返回head->next后忘记上一层会接收:当count==n时返回head->next,上一层的 head->next = removeNthFromEnd(…) 会接收到这个值,从而跳过被删节点。这是递归删除的关键。
  • n等于链表长度时(删除头节点):这是最容易出错的边界情况。快慢指针法中,fast先走n步后fast正好在最后一个节点,while循环不执行,slow在dummy,删除dummy->next(即head),正确。递归法中,回溯到第一层(head)时count==n,返回head->next,正确。两种方法都能正确处理,这就是虚拟头节点和递归的优势。

  • 二、力扣 24. 两两交换链表中的节点

    2.1 题目描述

    给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

    示例 1:

    输入:head = [1,2,3,4]
    输出:[2,1,4,3]
    解释:第1对(1,2)交换→(2,1),第2对(3,4)交换→(4,3)

    示例 2:

    输入:head = []
    输出:[]
    解释:空链表

    示例 3:

    输入:head = [1]
    输出:[1]
    解释:只有一个节点,无法交换

    提示:

    • 链表中节点的数目在范围 [0, 100] 内
    • 0 <= Node.val <= 100

    2.2 解题思路

    思路一:迭代法(虚拟头节点+三步交换)—— 推荐

    核心思想: 用虚拟头节点dummy,cur从dummy开始。每次处理cur后面的两个节点node1和node2,通过三步指针操作交换它们,然后cur移动到node1(交换后的第二个节点),继续处理下一对。

    三步交换指针操作:

    交换前:cur → node1 → node2 → node3

  • cur->next = node2(cur连到node2)
  • node1->next = node2->next(node1连到node3)
  • node2->next = node1(node2连到node1)
  • 交换后:cur → node2 → node1 → node3

    然后 cur = node1,继续处理下一对。

    手动模拟:以 [1→2→3→4] 为例

    初始:dummy → 1 → 2 → 3 → 4
    cur = dummy

    第一轮:交换node1=1和node2=2

    步骤操作链表状态
    初始 cur=dummy, node1=1, node2=2 dummy→1→2→3→4
    第1步 cur->next=node2 dummy→2→3→4,1→2
    第2步 node1->next=node2->next dummy→2→3→4,1→3
    第3步 node2->next=node1 dummy→2→1→3→4
    前进 cur=node1=1 cur在1,准备处理下一对

    第二轮:交换node1=3和node2=4

    步骤操作链表状态
    初始 cur=1, node1=3, node2=4 dummy→2→1→3→4
    第1步 cur->next=node2 dummy→2→1→4,3→4
    第2步 node1->next=node2->next dummy→2→1→4,3→nullptr
    第3步 node2->next=node1 dummy→2→1→4→3
    前进 cur=node1=3 cur在3,cur->next=nullptr,循环结束

    最终:dummy→2→1→4→3,返回dummy->next即节点2 ✅

    思路二:递归法

    核心思想: 递归处理每一对节点。对于当前的head和head->next(即node1和node2):

  • 先保存newHead = head->next(交换后的第一个节点,也是这一对的新头)
  • 递归交换newHead后面的节点(head->next = swapPairs(newHead->next))
  • 让newHead->next = head(交换后的第二个节点连到head)
  • 返回newHead
  • 递归终止条件: head为空或head->next为空(没有两个节点可以交换),直接返回head。

    手动模拟:以 [1→2→3→4] 为例

    递归深入阶段:

    swapPairs(1)
    → newHead=2, head->next=swapPairs(3)
    → newHead=4, head->next=swapPairs(nullptr)
    → 返回nullptr

    回溯阶段(从下往上):

    回溯层当前headnewHead操作返回
    第1层 3 4 3->next=nullptr, 4->next=3 返回4
    第2层 1 2 1->next=4(上一层返回的), 2->next=1 返回2

    最终返回节点2,链表变为 2→1→4→3 ✅


    2.3 C++ 代码实现(逐行解释)

    方法一:迭代法(虚拟头节点+三步交换)

    class Solution {
    public:
    ListNode* swapPairs(ListNode* head) {
    // 创建虚拟头节点,next指向head
    ListNode* dummy = new ListNode(0, head);
    // cur从dummy开始,每次处理cur后面的两个节点
    ListNode* cur = dummy;

    // 只要cur后面还有两个节点,就继续交换
    while (cur->next != nullptr && cur->next->next != nullptr) {
    // 保存要交换的两个节点
    ListNode* node1 = cur->next;
    ListNode* node2 = cur->next->next;

    // 三步交换指针
    cur->next = node2; // 第一步:cur连到node2
    node1->next = node2->next; // 第二步:node1连到node2后面的节点
    node2->next = node1; // 第三步:node2连到node1

    // cur移动到node1(交换后的第二个节点),准备处理下一对
    cur = node1;
    }

    // 返回新的头节点
    return dummy->next;
    }
    };

    关键代码逐行解释:

  • ListNode* dummy = new ListNode(0, head);

    • 创建虚拟头节点,val=0,next指向head
    • 有了dummy,第一对节点的交换和后面的对一样处理,不需要特殊处理头节点变化
  • ListNode* cur = dummy;

    • cur从dummy开始
    • cur始终指向"已处理部分的最后一个节点",每次处理cur后面的两个节点
  • while (cur->next != nullptr && cur->next->next != nullptr)

    • 只要cur后面还有至少两个节点,就继续交换
    • 两个条件都要检查:cur->next不为空(有第一个节点),cur->next->next不为空(有第二个节点)
    • 如果只有一个节点或没有节点,就不需要交换了
  • ListNode* node1 = cur->next; ListNode* node2 = cur->next->next;

    • 保存要交换的两个节点
    • node1是cur后面第一个,node2是cur后面第二个
  • cur->next = node2;

    • 第一步:cur连到node2
    • 交换前cur→node1,交换后cur→node2
  • node1->next = node2->next;

    • 第二步:node1连到node2后面的节点
    • 这样node1就"跳"到了node2后面
  • node2->next = node1;

    • 第三步:node2连到node1
    • 完成交换:cur→node2→node1→…
  • cur = node1;

    • cur移动到node1(交换后的第二个节点)
    • 下一轮处理node1后面的两个节点
    • 注意:不是cur=node2,因为node2已经在node1前面了,cur要停在已处理部分的最后
  • return dummy->next;

    • 返回新的头节点
    • 用dummy->next而不是head,因为头节点已经变成原来的第二个节点了
  • 方法二:递归法

    class Solution {
    public:
    ListNode* swapPairs(ListNode* head) {
    // 递归终止条件:没有节点或只有一个节点,无法交换
    if (head == nullptr || head->next == nullptr) {
    return head;
    }

    // 保存交换后的第一个节点(原来的第二个节点)
    ListNode* newHead = head->next;

    // 递归交换newHead后面的节点,把结果连到head后面
    head->next = swapPairs(newHead->next);

    // newHead连到head,完成当前这一对的交换
    newHead->next = head;

    // 返回交换后的新头
    return newHead;
    }
    };

    关键代码逐行解释:

  • if (head == nullptr || head->next == nullptr) { return head; }

    • 递归终止条件:没有节点(head为空)或只有一个节点(head->next为空)
    • 无法交换,直接返回head
  • ListNode* newHead = head->next;

    • 保存交换后的第一个节点
    • 原来的顺序是head→newHead,交换后是newHead→head
    • 所以newHead是这一对交换后的新头
  • head->next = swapPairs(newHead->next);

    • 递归交换newHead后面的节点
    • 把递归返回的结果(后面部分交换后的头)连到head后面
    • 这样head就成了交换后的第二个节点,后面接着已经交换好的部分
  • newHead->next = head;

    • newHead连到head
    • 完成当前这一对的交换:newHead→head→(后面交换好的部分)
  • return newHead;

    • 返回交换后的新头
    • 上一层递归会把这个newHead连到它的head后面

  • 2.4 复杂度分析

    方法时间复杂度空间复杂度
    方法一:迭代法 O(n) O(1)
    方法二:递归法 O(n) O(n)(递归栈)

    说明:

    • n 是链表的长度
    • 两种方法都只遍历链表一次,时间O(n)
    • 迭代法只用了几个指针,空间O(1)
    • 递归法需要递归栈空间,空间O(n)

    2.5 核心技巧与踩坑记录

    核心技巧:

  • 三步交换指针:交换两个相邻节点,记住三步——cur连到node2、node1连到node2后面、node2连到node1。顺序不能乱,否则会丢失指针。
  • 虚拟头节点统一处理:第一对节点交换后头节点会变,用虚拟头节点可以让第一对和后面的对一样处理,统一返回dummy->next。
  • cur=node1前进:交换后cur要移动到node1(交换后的第二个节点),不是node2。因为node2已经在前面了,cur要停在已处理部分的最后,才能处理下一对。
  • 踩坑记录:

  • 三步交换顺序写错:必须先 cur->next=node2,再 node1->next=node2->next,最后 node2->next=node1。如果先改node2->next,会丢失node2后面的节点指针。
  • cur前进到node2而不是node1:交换后cur应该等于node1(交换后的第二个节点),如果等于node2,下一轮处理的节点就错了,会导致死循环或漏处理。
  • while条件只检查cur->next:必须同时检查 cur->next != nullptr && cur->next->next != nullptr,如果只检查第一个,当只剩一个节点时访问cur->next->next会越界。
  • 递归法忘记先保存newHead:必须先 ListNode* newHead = head->next 保存新头,然后再递归。如果先递归再保存,head->next已经被修改了,newHead就不对了。
  • 递归法head->next赋值顺序:必须先 head->next = swapPairs(newHead->next)(递归处理后面),再 newHead->next = head(连当前这一对)。如果顺序反了,newHead->next被改成head后,newHead->next就不是原来的后面节点了,递归就错了。
  • 奇数个节点时最后一个不交换:链表长度为奇数时,最后一个节点没有配对,不交换。while条件 cur->next->next != nullptr 会自动处理这种情况,最后一个节点保持不动。

  • 三、两题关联与进阶思考

    3.1 从 19 到 24 的链表指针操作思维跃迁

    维度19. 删除倒数第N个结点24. 两两交换链表中的节点
    操作类型 删除节点 交换相邻节点
    迭代法核心 快慢指针(fast先走n步) 虚拟头节点+三步交换
    递归法核心 回溯计数(count==n时跳过) 递归交换每一对(newHead技巧)
    虚拟头节点 必须用(删除头节点) 必须用(头节点会变)
    关键指针位置 slow停在待删节点前驱 cur停在已处理部分最后
    时间复杂度 O(L) O(n)
    空间复杂度 快慢O(1)/递归O(L) 迭代O(1)/递归O(n)

    思维提炼: 这两题都是链表指针操作的经典题,而且都能用迭代和递归两种方法解决:

    • 迭代法:19题用快慢指针的"距离差"找位置,24题用虚拟头节点+三步交换改指针。两题都必须用虚拟头节点来统一处理头节点的边界情况。
    • 递归法:19题用"回溯计数"(深入时不做事,回溯时计数),24题用"newHead技巧"(先保存新头,递归处理后面,再连当前这一对)。两题的递归都利用了"回溯时从后往前处理"的特点。

    对比学习这两题,能深刻理解"快慢指针距离差"和"三步交换指针"这两个链表操作的核心思想,以及迭代和递归两种思维方式的转换。


    3.2 可复用模板汇总(其他题目直接套用)

    从这两道题中,可以提炼出链表操作的 2 个固定模板。

    模板一:快慢指针找倒数第N个(源自 19 题)

    核心思想: fast先走n步制造距离差,然后slow和fast同速走,fast到末尾时slow就在倒数第n个的前驱。

    // 模板:快慢指针找倒数第N个节点的前驱
    // 适用:删除倒数第N个节点、找倒数第K个节点等
    ListNode* findNthFromEnd(ListNode* head, int n) {
    ListNode* dummy = new ListNode(0, head);
    ListNode* slow = dummy;
    ListNode* fast = dummy;
    // fast先走n步
    for (int i = 0; i < n; i++) {
    fast = fast->next;
    }
    // 同速走,fast到末尾时slow在目标前驱
    while (fast->next != nullptr) {
    slow = slow->next;
    fast = fast->next;
    }
    return slow; // slow是倒数第n个节点的前驱
    }

    可直接套用的题目:

    题号题目名称套用方式
    19 删除链表的倒数第N个结点 直接套用,删除slow->next
    面试题02.02 返回倒数第k个节点 变体:返回slow->next
    1721 交换链表中的节点 变体:找正数第k个和倒数第k个
    剑指Offer22 链表中倒数第k个节点 直接套用

    模板二:虚拟头节点+三步交换(源自 24 题)

    核心思想: cur从dummy开始,每次处理cur后面的两个节点node1和node2,三步交换指针,cur=node1前进。

    // 模板:虚拟头节点+三步交换相邻节点
    // 适用:两两交换链表节点、K个一组反转链表等
    ListNode* swapPairs(ListNode* head) {
    ListNode* dummy = new ListNode(0, head);
    ListNode* cur = dummy;
    while (cur->next != nullptr && cur->next->next != nullptr) {
    ListNode* node1 = cur->next;
    ListNode* node2 = cur->next->next;
    // 三步交换
    cur->next = node2;
    node1->next = node2->next;
    node2->next = node1;
    // 前进
    cur = node1;
    }
    return dummy->next;
    }

    可直接套用的题目:

    题号题目名称套用方式
    24 两两交换链表中的节点 直接套用
    25 K个一组翻转链表 变体:每K个一组反转,三步变多步
    206 反转链表 变体:双指针迭代反转
    92 反转链表II 变体:反转指定区间

    链表操作模板选择速查表
    场景推荐模板时间复杂度空间复杂度
    找/删倒数第N个 模板一:快慢指针 O(L) O(1)
    两两交换节点 模板二:虚拟头节点+三步交换 O(n) O(1)
    删除头节点 虚拟头节点统一处理 O(1) O(1)
    头节点会变的操作 虚拟头节点+返回dummy->next – O(1)

    💡 小白建议:链表操作题,先想"要不要用虚拟头节点"(头节点会变或可能删头节点就一定要用),再想"用迭代还是递归"(迭代空间O(1),递归代码简洁但空间O(n))。虚拟头节点是链表题的万能神器,记住它!


    四、刷题心得与学习感悟

    4.1 这两题能学到什么?

  • 快慢指针的"距离差"思想是万能的:19题中fast先走n步,然后同速走,就能找到倒数第n个。这个"距离差"思想不仅能找倒数第n个,还能找中点(fast走两步slow走一步)、判环(fast走两步slow走一步)、找环入口(Floyd算法)等。掌握了距离差思想,一大类链表题就通了。
  • 三步交换指针是链表操作的基本功:24题的三步交换(cur连node2、node1连node2后面、node2连node1)看似简单,但顺序不能乱。这是链表指针操作的基本功,掌握了它,K个一组反转、反转链表II等更复杂的题也能应对。
  • 虚拟头节点是链表题的万能神器:19题和24题的迭代法都用了虚拟头节点。只要头节点可能变(删除头节点、交换头节点、反转链表等),就用虚拟头节点,让头节点和其他节点一样处理,不需要写特殊的边界代码。
  • 递归法的"回溯处理"思想很巧妙:19题的递归法在回溯时计数,24题的递归法在回溯时连接当前这一对。递归的特点是"先深入到末尾,再从后往前处理",利用这个特点可以优雅地解决"从后往前数"、"从后往前交换"等问题。
  • 4.2 高频算法总结

    题型分类代表题目核心方法
    链表删除 19删除倒数第N个、83删除排序链表重复元素、82删除排序链表重复元素II 快慢指针/虚拟头节点
    链表交换/反转 24两两交换、25 K个一组反转、206反转链表、92反转链表II 三步交换/双指针迭代/递归
    链表查找 141环形链表、142环形链表II、160相交链表、876中间结点 快慢指针/哈希表
    链表合并 21合并两个有序链表、23合并K个升序链表 虚拟头节点+比较
    链表排序 148排序链表、147对链表进行插入排序 归并排序/插入排序

    本篇的删除倒数第N个和两两交换,分别是"链表删除"和"链表交换/反转"两大基础题型。

    4.3 新手常见误区

  • 19题while条件写成fast != nullptr:应该是fast->next != nullptr,否则slow会停在待删节点而不是前驱。
  • 19题slow和fast不从dummy开始:必须从dummy开始,否则删除头节点时slow无法停在head的前驱。
  • 24题三步交换顺序写错:必须先cur->next=node2,再node1->next=node2->next,最后node2->next=node1。顺序错了会丢失指针。
  • 24题cur前进到node2而不是node1:交换后cur应该等于node1(交换后的第二个节点),等于node2会导致下一轮处理错误。
  • 递归法count不是成员变量(19题):count必须是类的成员变量,在所有递归调用中共享。
  • 递归法忘记先保存newHead(24题):必须先保存newHead=head->next,再递归处理后面。
  • 不用虚拟头节点:只要头节点可能变,就一定要用虚拟头节点。不用的话需要写大量边界处理代码,还容易出错。
  • 链表题不画图:指针的变化、交换的过程,用文字描述很抽象,一定要在纸上画图。画两遍就理解了。
  • 4.4 我的刷题方法分享

  • 先理解核心思想,再写代码:19题先搞懂"fast先走n步制造距离差",24题先搞懂"三步交换指针"。理解了核心思想,代码就是水到渠成的事。
  • 画图模拟指针变化:在纸上画出链表、节点、指针,一步步模拟每一步操作后指针的变化。特别是24题的三步交换,画图后一目了然。
  • 对比迭代和递归两种写法:两题都有迭代和递归两种解法,对比着学——迭代空间O(1)但代码稍长,递归代码简洁但空间O(n)。面试时能说出两种解法更加分。
  • 记住虚拟头节点这个万能神器:只要头节点可能变,就用虚拟头节点。这是链表题最实用的技巧,没有之一。
  • 写刷题笔记:把核心思想、三步交换的顺序、递归的技巧、易错点记下来。复习时一看就懂,比重新推导效率高多了。

  • 五、总结

    本文详细讲解了 LeetCode 19(删除链表的倒数第N个结点)和 24(两两交换链表中的节点)两道经典链表操作题目:

    • 19题:提供了两种解法——①快慢指针法:dummy虚拟头节点,slow和fast都从dummy出发,fast先走n步,然后同速走直到fast->next= =nullptr,此时slow是待删节点前驱,删除slow->next并delete释放内存;②递归法:成员变量count计数,递归深入到末尾后回溯,count++,count==n时返回head->next跳过该节点,否则返回head。快慢指针法 O(L) 时间 O(1) 空间(一趟扫描),递归法 O(L) 时间 O(L) 空间。
    • 24题:提供了两种解法——①迭代法:dummy虚拟头节点,cur从dummy开始,while(cur后面有两个节点)时保存node1和node2,三步交换(cur->next=node2、node1->next=node2->next、node2->next=node1),cur=node1前进;②递归法:终止条件head或head->next为空,保存newHead=head->next,head->next=递归(newHead->next),newHead->next=head,返回newHead。迭代法 O(n) 时间 O(1) 空间,递归法 O(n) 时间 O(n) 空间。
    • 两题关系:两题都是链表指针操作的经典题,都必须用虚拟头节点统一处理头节点变化,都能用迭代和递归两种方法解决。19题的核心是快慢指针的"距离差"思想,24题的核心是"三步交换指针"。
    • 可复用模板:从两题提炼出2个核心模板——“快慢指针找倒数第N个”(可套用到面试题02.02、1721、剑指Offer22等题)和"虚拟头节点+三步交换"(可套用到25、206、92等题)。

    这两道题是链表操作的进阶必刷题,19题中等难度但快慢指针的距离差思想是链表题的核心,24题中等难度但三步交换指针是链表操作的基本功。希望这篇详解能帮助你理解快慢指针和三步交换的核心思想,也欢迎在评论区交流你的解法和心得!


    下一篇预告: 继续链表专题,将讲解【83.删除排序链表中的重复元素】和【234.回文链表】(链表去重与判回文双经典),敬请期待! 如果本文对你有帮助,欢迎点赞 👍、收藏 ⭐、关注我,一起在算法之路上进步!

    赞(0)
    未经允许不得转载:171主机测评 » 【LeetCode刷题日志】链表删除与交换双经典:19.删除链表的倒数第N个结点 & 24.两两交换链表中的节点(C++详解)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址