【LeetCode Hot100 刷题日记(30/100)】24.两两交换链表中的节点 —— 链表、递归、迭代、指针操作🔗

0 阅读6分钟

🔗 题目链接:24. 两两交换链表中的节点 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:链表、递归、迭代、指针操作

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(n)(递归)、O(1)(迭代)


题目分析

💬 给你一个单向链表,要求两两交换相邻的节点,并返回新的头节点。
不能修改节点的值,只能通过调整指针实现“交换”。
✅ 示例输入 [1,2,3,4] → 输出 [2,1,4,3]

关键点提炼:

  • 每两个节点为一组进行交换;
  • 若链表长度为奇数,则最后一个节点保持不变;
  • 必须原地操作,不额外创建节点;
  • 考察对链表结构的理解指针重连能力
  • 是后续更难题目如「K个一组翻转链表」的基础。

核心算法及代码讲解

本题有两种经典解法
递归法:利用函数调用栈自然处理子问题;
迭代法:使用哑结点 + 双指针模拟交换过程。

我们先从递归入手,理解其背后的思想逻辑。


🔁 递归核心思想(自顶向下)

ListNode* swapPairs(ListNode* head) {
    if(head == nullptr || head->next == nullptr){
        return head;
    }
    ListNode* newHead = head->next;           // 新头是原第二个节点
    head->next = swapPairs(newHead->next);    // 递归处理剩余部分
    newHead->next = head;                     // 原第一个接在新头后
    return newHead;
}

🧠 分步解析:

步骤说明
1. `if(head == nullptrhead->next == nullptr)`边界条件:空链表或只有一个节点时无法交换,直接返回
2. ListNode* newHead = head->next;记录新头节点(即当前节点的下一个),这是交换后的头
3. head->next = swapPairs(newHead->next);递归处理剩下的链表(从 newHead->next 开始),返回的是这部分交换后的头
4. newHead->next = head;把原第一个节点接到新头之后,完成局部交换
5. return newHead;返回新链表的头

🌀 这种写法本质是“分治 + 指针重连”:把大问题拆成小问题,解决后再拼接。

⚠️ 注意事项:

  • 递归深度等于链表长度的一半左右;
  • 空间复杂度为 O(n),因为每次递归都会压栈;
  • 面试中若被追问空间优化,应主动提出迭代方案。

🔄 迭代核心思想(自底向上)

使用**哑结点(dummy head)**避免对头节点特殊处理。

ListNode* swapPairs(ListNode* head) {
    ListNode* dummyHead = new ListNode(0);
    dummyHead->next = head;
    ListNode* temp = dummyHead;
    while (temp->next != nullptr && temp->next->next != nullptr) {
        ListNode* node1 = temp->next;
        ListNode* node2 = temp->next->next;
        temp->next = node2;
        node1->next = node2->next;
        node2->next = node1;
        temp = node1;
    }
    ListNode* ans = dummyHead->next;
    delete dummyHead;
    return ans;
}

🧠 分步解析:

步骤说明
1. 创建 dummyHead 并连接到 head解决头节点需要特殊处理的问题
2. temp 指向当前待交换的前一个节点初始为 dummyHead
3. 循环判断是否有两个连续节点temp->next && temp->next->next
4. 定义 node1, node2分别表示要交换的两个节点
5. temp->next = node2;temp 的下一个指向 node2(即新位置)
6. node1->next = node2->next;断开 node1node2 的连接,指向后面的节点
7. node2->next = node1;node2 指向 node1,完成交换
8. temp = node1;移动到下一对的前驱节点
9. 最终返回 dummyHead->next即新链表的头节点

✅ 优势:空间复杂度 O(1),适合大规模数据;
❌ 缺点:需手动管理哑结点内存(C++ 中需 delete)。


解题思路

方法一:递归法(推荐用于理解)

  1. 终止条件:链表为空或只有一个节点 → 不交换,返回原链表。
  2. 递归关系
    • 先交换当前两个节点;
    • 再递归处理后面的部分;
    • 最后将前后两段拼接起来。
  3. 关键技巧
    • swapPairs(newHead->next) 处理后续部分;
    • 返回 newHead 作为新的头节点。

🎯 类比:就像剥洋葱,一层一层往下走,直到最内层再逐层往上还原。


方法二:迭代法(推荐用于面试)

  1. 引入哑结点:简化边界处理;
  2. 使用双指针temp 控制当前位置,node1/node2 表示待交换节点;
  3. 循环交换:每轮交换两个节点,并移动 temp 到下一组的前驱;
  4. 释放资源:记得删除哑结点以防止内存泄漏。

💡 提示:面试官常会问:“有没有办法不用递归?” → 此时你应该立刻说出迭代解法!


算法分析

方法时间复杂度空间复杂度是否推荐
递归O(n)O(n)✅ 理解用
迭代O(n)O(1)✅ 面试用

📈 为什么递归空间是 O(n)?
因为每次递归调用都占用栈空间,最多调用 n/2 次 → 栈深 ≈ n/2 → O(n)

🔍 实际场景选择建议:

  • 如果允许牺牲空间换取简洁性,可用递归;
  • 如果强调效率与稳定性(如生产环境),优先选迭代。

代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    // 方法一:递归法
    ListNode* swapPairs(ListNode* head) {
        // 判空:若为空或只有一个节点,无法交换
        if(head == nullptr || head->next == nullptr){
            return head;
        }
        // 新头是原第二个节点
        ListNode* newHead = head->next;
        // 递归处理剩余部分:head->next 接上交换后的结果
        head->next = swapPairs(newHead->next);
        // 新头的下一个指向原头节点,完成交换
        newHead->next = head;
        return newHead;
    }

    // 方法二:迭代法(推荐)
    ListNode* swapPairsIterative(ListNode* head) {
        // 创建哑结点,方便统一处理头节点
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* temp = dummyHead;

        // 当还有至少两个节点可交换时继续
        while (temp->next != nullptr && temp->next->next != nullptr) {
            ListNode* node1 = temp->next;      // 第一个节点
            ListNode* node2 = temp->next->next; // 第二个节点

            // 交换:temp -> node2 -> node1
            temp->next = node2;
            node1->next = node2->next;
            node2->next = node1;

            // 移动 temp 到下一组的前驱
            temp = node1;
        }

        // 获取结果并清理内存
        ListNode* ans = dummyHead->next;
        delete dummyHead;
        return ans;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 构造测试链表 [1,2,3,4]
    ListNode* head = new ListNode(1);
    head->next = new ListNode(2);
    head->next->next = new ListNode(3);
    head->next->next->next = new ListNode(4);

    Solution sol;
    ListNode* result = sol.swapPairs(head);

    // 打印结果
    while(result != nullptr){
        cout << result->val << " ";
        result = result->next;
    }
    cout << endl;

    return 0;
}

🛠️ 测试说明:

  • 输入:[1,2,3,4]
  • 输出:2 1 4 3
  • 验证了递归法正确性(也可测试迭代法)

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第31题 —— 25.K 个一组翻转链表(困难)

🔹 题目:给定一个链表,将其每 K 个节点为一组进行翻转,返回翻转后的链表。

🔹 核心思路:基于本题的“两两交换”,扩展为“K个翻转”,需结合递归 + 指针重连 + 长度统计

🔹 考点:链表操作、递归思维、边界处理、复杂度控制。

🔹 难度:困难,是链表类题目的巅峰之一,常出现在字节、腾讯、阿里等大厂面试中。

🔹 提示:不要暴力遍历!要学会用“断开-翻转-连接”的三步法!

💡 提示:可以先尝试用递归实现,再优化为迭代,掌握通用模板!


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!

👉 下一期我们将挑战「K 个一组翻转链表」,带你打通链表操作的最后一公里!🚀