期末周 / 考试月 终于结束!!现在回归开发~好好刷会leetcode!
从现在开始刷leetcode要逐渐过渡到用JavaScript了,熟悉一下语法的API 。
1 题目
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 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++ 结合你的思考,详细拆解这道题的实现思路和代码。
核心思考对齐
你提到的两个关键点完全命中本题核心:
- 终止条件:剩余节点不足 2 个(0 个 / 1 个)时停止交换,这是循环终止的核心依据;
- 和反转链表的区别 :反转链表是「逐个反转指针方向」,而本题是「成对交换节点位置」,
cur的定位确实更特殊 ------ 它需要停在「待交换节点对的前驱节点」,而非待交换节点本身。
解题思路(C++ 版)
链表操作的核心痛点是「避免断链」,两两交换的关键是:
- 虚拟头节点:解决头节点没有前驱、交换后新头难定位的问题;
- 前驱指针
cur:cur始终指向「待交换节点对的前一个节点」,通过它串联交换后的节点; - 三步走交换 :先保存临时节点 → 交换节点指针 → 移动
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 |
你的反转链表代码中,cur从head开始,逐个修改指针方向;
而本题cur从虚拟头开始,每次处理「下两个节点」,这是最核心的区别。
2. 测试用例验证(对应你的奇数节点疑问)
- 输入
[1,2,3]:- 第一轮:
cur=dummy,交换 1 和 2 → 链表变为dummy→2→1→3,cur移到 1; - 检查终止条件:
cur->next=3,cur->next->next=null→ 停止; - 输出
[2,1,3](和你的推断一致)。
- 第一轮:
- 输入
[1,2,3,4]:- 第一轮交换 1&2 →
dummy→2→1→3→4,cur=1; - 第二轮交换 3&4 →
dummy→2→1→4→3,cur=3; - 停止,输出
[2,1,4,3]。
- 第一轮交换 1&2 →
总结
- 核心技巧 :虚拟头节点解决头节点交换的特殊处理,
cur定位在「待交换节点对的前驱」避免断链; - 终止条件:严格遵循你的思考 ------ 剩余节点不足 2 个则停止,天然处理奇数 / 偶数节点;
- 交换逻辑:先保存临时节点,再按「挂后续→交换节点→挂前驱」的顺序操作,避免链表断裂。
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=1、cur->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=1、cur->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。
针对性练习建议(从易到难,巩固遍历逻辑)
既然你想把 "反转链表的遍历操作学扎实",推荐按这个顺序练,每道题都先在草稿纸模拟指针移动,再写代码:
- 基础巩固 :重新实现「反转链表」(迭代版),要求:
- 给每个指针写注释说明 "使命"(
cur:当前待反转节点;prev:已反转的尾节点;temp:保存后续链表); - 模拟输入
[1,2,3]的每一步指针变化,画在纸上。
- 给每个指针写注释说明 "使命"(
- 本题强化 :
- 用「错误代码」再模拟一遍
[1,2,3,4]的执行过程,把每一步的链表结构写下来,明确 "错在哪一步、为什么错"; - 尝试用递归版实现本题(递归的核心也是 "明确每一步的使命":交换当前两个节点,递归处理后续链表)。
- 用「错误代码」再模拟一遍
- 进阶拓展: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;
}
};
总结
- 核心痛点:链表指针的移动,必须围绕 "指针的使命",而不是 "随便往后走";
- 解题技巧:先在草稿纸模拟指针移动,明确每一步的链表结构,再写代码;
- 学习方法:给指针起 "有语义的名字"、写注释说明使命,能大幅降低出错概率。
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
- 为什么不用
var:var存在变量提升 和函数级作用域的问题,容易导致变量泄露、重复声明等 bug;let/const是块级作用域(和 C++ 的变量作用域逻辑一致),更符合直觉,也更安全。
let和const的选择原则 :const:声明后不重新赋值 的变量(如dummy、node1、node2、newHead、函数本身),强制不可变,减少 bug;let:声明后需要重新赋值 的变量(如cur,因为要移动指针)。
总结
- 核心逻辑和你的 C++ 代码完全一致:虚拟头节点、循环终止条件、节点交换顺序、cur 的移动规则均未改变;
- 仅做了 JS 语法适配,变量名(dummy/cur/node1/node2)和执行流程和你的 C++ 代码一一对应;
- JS 无需手动释放内存 ,因此删除了
delete dummy这一步,其余代码结构完全复刻。