Leetcode 104 两两交换链表中的节点

期末周 / 考试月 终于结束!!现在回归开发~好好刷会leetcode!

从现在开始刷leetcode要逐渐过渡到用JavaScript了,熟悉一下语法的API 。

1 题目

24. 两两交换链表中的节点

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

示例 1:

复制代码
输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

复制代码
输入:head = []
输出:[]

示例 3:

复制代码
输入:head = [1]
输出:[1]

提示:

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

2 代码实现

c++

cpp 复制代码
/**
 * 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) {
        ListNode* dummy = new ListNode(0);
        dummy -> next = head;
        ListNode* cur = dummy;
        while (cur -> next != nullptr && cur -> next -> next != nullptr){
            ListNode* node1 = cur -> next ;
            ListNode* node2 = cur -> next -> next ;
            node1 -> next = node2 -> next ;
            node2 -> next = node1 ;
            cur -> next = node2 ;

            cur = node1;
        }
        ListNode* newHead = dummy -> next ;
        delete dummy ;
        return newHead ;
    }
};

js

javascript 复制代码
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var swapPairs = function(head) {
    const dummy = new ListNode(0);
    dummy.next = head ;
    let cur = dummy;

    while (cur.next != null && cur.next.next != null){
        const node1 = cur.next ;
        const node2 = cur.next.next ;

        node1.next = node2.next ;
        node2.next = node1 ;
        cur.next = node2;

        cur = node1 ;
    }
    const new_head = dummy.next ;
    return new_head;
};

思考

如果是奇数个呢?123输入会变成什么。213还是231?

我个人推断是213,因为1234输入是2143而不是2413等,没有可重叠的两两交换

这个疑问解答了。

那么其实很重要的一个停止swap(交换)的要点就是看看余下数能否进行配对,如果是0个,那么就是配完了,结束;如果是1个,那么就是不能配对了,也结束。

啊,怎么实现两两交换啊,又不是一个数组用个空的temp存放一下就好了?

回忆一下之前做的Leetcode 102 反转链表-CSDN博客

以下是反转链表的做法:

cpp 复制代码
/**
 * 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* reverseList(ListNode* head) {
        if (head == nullptr || head -> next == nullptr)
        {
            return head ;
        }
            ListNode * cur = head ;
            ListNode * prev = nullptr ;
            while(cur != nullptr){
                ListNode* temp = cur -> next ;
                cur -> next = prev ;
                prev = cur ;
                cur =  temp ;
            }
            return prev ;
        
    }
};

不知道,难道这个题目特别的地方就在于所谓两两互换,cur的执行位置是特殊一点的吗?

题解

需要ai帮助解答。

---------

恭喜你结束期末周!🎉 终于能专心刷 LeetCode 了。

你想解决的是 LeetCode 24 题「两两交换链表中的节点」,并且已经理清了核心逻辑:

只有成对的节点才交换,奇数个节点时最后一个保持不动(比如输入 [1,2,3] 输出 [2,1,3]),这个理解完全正确。

接下来我用 C++ 结合你的思考,详细拆解这道题的实现思路和代码。

核心思考对齐

你提到的两个关键点完全命中本题核心:

  1. 终止条件:剩余节点不足 2 个(0 个 / 1 个)时停止交换,这是循环终止的核心依据;
  2. 和反转链表的区别 :反转链表是「逐个反转指针方向」,而本题是「成对交换节点位置」,cur的定位确实更特殊 ------ 它需要停在「待交换节点对的前驱节点」,而非待交换节点本身。

解题思路(C++ 版)

链表操作的核心痛点是「避免断链」,两两交换的关键是:

  1. 虚拟头节点:解决头节点没有前驱、交换后新头难定位的问题;
  2. 前驱指针curcur始终指向「待交换节点对的前一个节点」,通过它串联交换后的节点;
  3. 三步走交换 :先保存临时节点 → 交换节点指针 → 移动cur到下一组前驱位置。

C++ 完整实现(带详细注释)

cpp 复制代码
/**
 * 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) {
        // 1. 创建虚拟头节点(核心技巧):解决头节点交换后找不到新头的问题
        ListNode* dummy = new ListNode(0);
        dummy->next = head;  // 虚拟头指向原头节点
        ListNode* cur = dummy;  // cur是"待交换节点对的前驱节点",初始在虚拟头

        // 2. 循环终止条件:你的核心思考------剩余节点不足2个则停止
        // cur->next == null → 无节点;cur->next->next == null → 只剩1个节点
        while (cur->next != nullptr && cur->next->next != nullptr) {
            // 3. 保存临时节点(避免交换时丢失引用,导致断链)
            ListNode* node1 = cur->next;       // 第一个待交换节点(比如1)
            ListNode* node2 = cur->next->next; // 第二个待交换节点(比如2)
            
            // 4. 核心交换逻辑(三步走,顺序不能乱)
            node1->next = node2->next;  // 第一步:1指向3(先把后续链表挂到1上,避免丢链)
            node2->next = node1;        // 第二步:2指向1(完成2和1的交换)
            cur->next = node2;          // 第三步:前驱节点指向2(把交换后的节点对挂回原链表)

            // 5. 移动cur到下一组的前驱位置(即交换后的node1)
            cur = node1;
        }

        // 6. 返回新头节点(虚拟头的next)
        ListNode* newHead = dummy->next;
        delete dummy;  // 释放虚拟头节点(避免内存泄漏,LeetCode可省略但工程中建议加)
        return newHead;
    }
};

关键细节解释(结合你的疑问)

1. 和你写的「反转链表」对比
反转链表 两两交换节点
cur指向「当前待反转节点」 cur指向「待交换节点对的前驱」
核心操作:cur->next = prev(反转单个指针) 核心操作:调整两个节点的相互指向
终止条件:cur == null 终止条件:cur->next/next->next == null

你的反转链表代码中,curhead开始,逐个修改指针方向;

而本题cur从虚拟头开始,每次处理「下两个节点」,这是最核心的区别。

2. 测试用例验证(对应你的奇数节点疑问)
  • 输入[1,2,3]
    • 第一轮:cur=dummy,交换 1 和 2 → 链表变为dummy→2→1→3cur移到 1;
    • 检查终止条件:cur->next=3cur->next->next=null → 停止;
    • 输出[2,1,3](和你的推断一致)。
  • 输入[1,2,3,4]
    • 第一轮交换 1&2 → dummy→2→1→3→4cur=1
    • 第二轮交换 3&4 → dummy→2→1→4→3cur=3
    • 停止,输出[2,1,4,3]

总结

  1. 核心技巧 :虚拟头节点解决头节点交换的特殊处理,cur定位在「待交换节点对的前驱」避免断链;
  2. 终止条件:严格遵循你的思考 ------ 剩余节点不足 2 个则停止,天然处理奇数 / 偶数节点;
  3. 交换逻辑:先保存临时节点,再按「挂后续→交换节点→挂前驱」的顺序操作,避免链表断裂。

3 反思

本题cur的正确定位:

必须指向 "本次交换后的 node1",才能保证下一轮处理的是下一组相邻节点。

其实看了题解以后我还写了一版错误的代码,是这样的,主要是cur的遍历逻辑有问题。

cpp 复制代码
/**
 * 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) {
        ListNode* dummy = new ListNode(0);
        dummy -> next = head;
        ListNode* cur = dummy;
        while (cur -> next != nullptr && cur -> next -> next != nullptr){
            ListNode* node1 = cur -> next ;
            ListNode* node2 = cur -> next -> next ;
            node1 -> next = node2 -> next ;
            node2 -> next = node1 ;
            cur -> next = node2 ;

            cur = cur -> next ;//这里不对
        }
        ListNode* newHead = dummy -> next ;
        delete dummy ;
        return newHead ;
    }
};

用输入[1,2,3,4]模拟错误执行过程

我们对比正确逻辑错误逻辑的每一步,就能直观看到差异:

初始状态
cpp 复制代码
dummy → 1 → 2 → 3 → 4
cur = dummy
第一轮循环(第一次交换)

无论cur后续怎么移,第一轮交换的操作是一样的:

cpp 复制代码
node1 = cur->next (1), node2 = cur->next->next (2)
node1->next = node2->next (1→3)
node2->next = node1 (2→1)
cur->next = node2 (dummy→2)

第一轮结束后链表状态:

cpp 复制代码
dummy → 2 → 1 → 3 → 4
关键差异:cur 的移动
正确逻辑(cur = node1) 错误逻辑(cur = cur->next)
cur 指向 1(node1) cur 指向 2(cur->next)
下一轮处理 3&4 下一轮错误处理 1&3
第二轮循环(错误逻辑的问题)

错误逻辑中cur=2,进入循环(cur->next=1cur->next->next=3,满足循环条件):

cpp 复制代码
node1 = cur->next (1), node2 = cur->next->next (3)
node1->next = node2->next (1→4)
node2->next = node1 (3→1)
cur->next = node2 (2→3)

第二轮结束后链表状态:

cpp 复制代码
dummy → 2 → 3 → 1 → 4

然后cur = cur->next(cur 指向 3),此时cur->next=1cur->next->next=4,仍满足循环条件,会继续第三轮循环......

最终错误结果

经过多轮错误交换,最终输入[1,2,3,4]会输出[2,3,1,4](而非正确的[2,1,4,3]),完全不符合 "两两交换" 的要求。

小结

这道题我其实能有思路,但是具体代码写不出来,特别是cur位置的遍历,就是看了题解还是有一点理解上的偏差。

我想还是要把反转链表的遍历操作学扎实一点,自己再草稿纸上过一遍模拟测试数据以后其实很清晰,但是用代码就还是会有各种问题。多多练习!

召唤ai老师!!!


先帮你梳理:为什么cur的定位是本题的 "灵魂"?

你觉得 "思路有,但代码写不对",核心是没吃透链表遍历指针的 "使命"

  • 反转链表中,cur的使命是「修改当前节点的指针方向」,所以它要停在待修改的节点上;
  • 两两交换中,cur的使命是「串联交换后的节点对」,所以它要停在待交换节点对的前驱上。

用一句话总结:指针的移动,永远服务于它的 "使命" 。你错误地写cur = cur->next,本质是忘了cur的使命 ------ 它需要为「下一组交换」做准备,而不是单纯 "往后走一步"。

更直观的记忆技巧:给指针 "贴标签"

新手写链表代码容易乱,是因为指针命名太笼统(比如都叫cur)。你可以给指针起 "有意义的名字",比如把本题的cur改成prev_pair(表示 "上一组交换后的尾节点 / 下一组的前驱"):

cpp 复制代码
// 把 cur 改成 prev_pair,语义更清晰
ListNode* prev_pair = dummy; 
while (prev_pair->next != nullptr && prev_pair->next->next != nullptr) {
    // ... 交换逻辑 ...
    prev_pair = node1; // 下一组的前驱就是本次交换后的node1
}

改名后,你会立刻意识到:prev_pair要指向 "下一组的前驱",而不是随便往后走,自然不会写错成prev_pair = prev_pair->next

针对性练习建议(从易到难,巩固遍历逻辑)

既然你想把 "反转链表的遍历操作学扎实",推荐按这个顺序练,每道题都先在草稿纸模拟指针移动,再写代码:

  1. 基础巩固 :重新实现「反转链表」(迭代版),要求:
    • 给每个指针写注释说明 "使命"(cur:当前待反转节点;prev:已反转的尾节点;temp:保存后续链表);
    • 模拟输入[1,2,3]的每一步指针变化,画在纸上。
  2. 本题强化
    • 用「错误代码」再模拟一遍[1,2,3,4]的执行过程,把每一步的链表结构写下来,明确 "错在哪一步、为什么错";
    • 尝试用递归版实现本题(递归的核心也是 "明确每一步的使命":交换当前两个节点,递归处理后续链表)。
  3. 进阶拓展:LeetCode 25 题「K 个一组反转链表」(本题是 K=2 的特例),吃透后链表遍历就彻底过关了。

递归版实现(补充思路,帮你多角度理解)

递归版的逻辑更贴合 "两两交换" 的直觉,也能帮你理解指针的 "使命":

cpp 复制代码
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        // 终止条件:剩余节点不足2个,直接返回
        if (head == nullptr || head->next == nullptr) {
            return head;
        }
        // 要交换的两个节点
        ListNode* node1 = head;
        ListNode* node2 = head->next;
        // 递归处理后续链表,返回的是后续交换后的头节点
        ListNode* nextHead = swapPairs(node2->next);
        // 交换当前两个节点
        node2->next = node1;
        node1->next = nextHead;
        // 返回当前交换后的头节点(node2)
        return node2;
    }
};

总结

  1. 核心痛点:链表指针的移动,必须围绕 "指针的使命",而不是 "随便往后走";
  2. 解题技巧:先在草稿纸模拟指针移动,明确每一步的链表结构,再写代码;
  3. 学习方法:给指针起 "有语义的名字"、写注释说明使命,能大幅降低出错概率。

4 cpp to js

front-end learner 必须把js语法熟悉一下,算法思想是一样的,这个新的模块为了监督自己转型,夯实语法基础!

我直接把你这段 C++ 代码一对一翻译成 JavaScript 版本,保证核心逻辑、变量命名、执行流程完全一致,同时适配 JS 的语法特性,你可以直接参考使用:

javascript

运行

javascript 复制代码
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var swapPairs = function(head) {
    // 对应C++: ListNode* dummy = new ListNode(0);
    const dummy = new ListNode(0);
    // 对应C++: dummy -> next = head;
    dummy.next = head;
    // 对应C++: ListNode* cur = dummy;
    let cur = dummy;

    // 对应C++: while (cur -> next != nullptr && cur -> next -> next != nullptr)
    while (cur.next !== null && cur.next.next !== null) {
        // 对应C++: ListNode* node1 = cur -> next ;
        const node1 = cur.next;
        // 对应C++: ListNode* node2 = cur -> next -> next ;
        const node2 = cur.next.next;

        // 对应C++: node1 -> next = node2 -> next ;
        node1.next = node2.next;
        // 对应C++: node2 -> next = node1 ;
        node2.next = node1;
        // 对应C++: cur -> next = node2 ;
        cur.next = node2;

        // 对应C++: cur = node1 ;
        cur = node1;
    }

    // 对应C++: ListNode* newHead = dummy -> next ;
    const newHead = dummy.next;
    // JS自动垃圾回收,无需手动delete dummy
    // 对应C++: return newHead ;
    return newHead;
};

关键语法对应说明(C++ → JS)

C++ 语法 JavaScript 对应写法 说明
new ListNode(0) new ListNode(0) 节点构造方式一致
nullptr null JS 中用 null 表示空指针
ListNode* cur = dummy let cur = dummy JS 用 let 声明可变指针
const ListNode* node1 const node1 = cur.next 不可变变量用 const
delete dummy 无需手动执行 JS 自动垃圾回收
-> 访问属性 . 访问属性 JS 对象属性访问语法

关键说明:let/const vs var

  1. 为什么不用 var
    • var 存在变量提升函数级作用域的问题,容易导致变量泄露、重复声明等 bug;
    • let/const块级作用域(和 C++ 的变量作用域逻辑一致),更符合直觉,也更安全。
  2. letconst 的选择原则
    • const:声明后不重新赋值 的变量(如 dummynode1node2newHead、函数本身),强制不可变,减少 bug;
    • let:声明后需要重新赋值 的变量(如 cur,因为要移动指针)。

总结

  1. 核心逻辑和你的 C++ 代码完全一致:虚拟头节点、循环终止条件、节点交换顺序、cur 的移动规则均未改变;
  2. 仅做了 JS 语法适配,变量名(dummy/cur/node1/node2)和执行流程和你的 C++ 代码一一对应;
  3. JS 无需手动释放内存 ,因此删除了delete dummy这一步,其余代码结构完全复刻。
相关推荐
阿星_2 小时前
github项目分享
笔记
鹿角片ljp2 小时前
力扣112. 路径总和:递归DFS vs 迭代BFS
leetcode·深度优先·宽度优先
2501_937798392 小时前
2026双效监测利器:GEO监测工具+AI搜索优化监测工具实战
笔记
Das12 小时前
【机器学习】07_降维与度量学习
人工智能·学习·机器学习
程序员-King.2 小时前
day159—动态规划—打家劫舍(LeetCode-198)
c++·算法·leetcode·深度优先·回溯·递归
小雨下雨的雨2 小时前
禅息:在鸿蒙与 Flutter 之间寻找呼吸的艺术
算法·flutter·华为·重构·交互·harmonyos
浅念-2 小时前
C语言——单链表
c语言·开发语言·数据结构·经验分享·笔记·算法·leetcode
hans汉斯2 小时前
建模与仿真|基于GWO-BP的晶圆机器人大臂疲劳寿命研究
大数据·数据结构·算法·yolo·机器人·云计算·汉斯出版社
wazmlp0018873692 小时前
python第一次作业
开发语言·python·算法