1. 题号背后的乌龙:Leetcode 108 与“交换链表节点”的真实身份
先把这个标题里的坑说清楚。如果你拿着“Leetcode 108”去力扣搜题,出来的其实是“将有序数组转换为二叉搜索树”,跟链表八竿子打不着。真正讨论“交换链表中的节点”这道题,对应的是 Leetcode 1721,题名叫 Swapping Nodes in a Linked List,中文站一般叫“交换链表中的节点”。这两个题号混淆的现象在社区里并不少见,原因很简单:力扣中文站的题号偶尔因为收录时间、题目排序的差异,在不同语言版本之间对不上号;再加上 108 这个数字朗朗上口,很容易被搜索热词带偏。
所以这篇博文的核心对象是 Leetcode 1721:给定一个链表头节点 head 和一个整数 k,要求交换从头开始数的第 k 个节点和从尾开始数的第 k 个节点,返回交换后的链表头。题目本身不长,但它的解法思路、边界条件和指针操作细节,非常适合用来检验对单链表遍历、双指针、虚拟头节点这些基本功的掌握程度。无论你是刚开始刷链表题目的新手,还是准备面试前想快速过一遍高频题的老手,这道题都值得认真做一遍。我会从题目解析、双指针核心逻辑、代码实现到测试验证,完整走一遍,顺带把一些常踩的坑和调试经验一并分享。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么这道题值得单独拿出来写
单看题目描述,“交换链表中两个节点”听起来像是链表的入门操作,但实际上这三类人最容易在这道题上栽跟头:刚学会链表遍历的初学者,会分不清“交换节点值”和“交换节点本身”的区别;准备面试的求职者,容易忽略第 k 个节点“从尾数也是第 k 个”的对称性,导致双指针步进逻辑写错;日常写业务代码的开发者,则可能在边界条件下(k=1、k=链表长度、链表只有两个节点等)翻车。我在实际给别人讲这道题的时候发现,大部分写错的人并不是不会双指针,而是没想清楚“两个指针应该怎么走、走到哪里停、交换之后头节点会不会变”这三个问题。
更值得注意的是,这道题和我搜到的一堆热词高度关联:单链表的基本操作、链表插入、链表遍历、合并两个有序的单链表、C++结构体链表基本语法、Python单链表逆序……它们其实都是同一个知识网络里的节点。Leetcode 1721 把“遍历”“定位”“交换”三个动作组合在一起,相当于一次小型的链表操作综合训练。而且这道题有两种主流的解题方向:一是直接交换两个节点的值,代码极短但改变了节点内容;二是通过指针操作真正交换节点位置,代码长、细节多,但更贴近工程中对“节点”本身进行操作的场景。平时练手用第一种理解思路,面试手写推荐第二种展示硬功夫,这也是我会在这篇博文里把两种方法都展开的原因。
3. 核心思路拆解:双指针定位与“从尾数第 k 个”的本质
3.1 一句话理解题目要做什么
链表是 1 -> 2 -> 3 -> 4 -> 5,k = 2,那就是交换正数第 2 个节点(值 2)和倒数第 2 个节点(值 4),结果是 1 -> 4 -> 3 -> 2 -> 5。如果链表长度是 5,k = 1,就交换头节点和尾节点;k = 5,交换尾节点和头节点,其实是同一个操作。所以你会发现一个关键性质:正数第 k 个和倒数第 k 个在 k 等于链表长度的一半左右时会指向同一个节点吗?不会,只有当链表长度是奇数且 k 正好在最中间时,两个位置重合。比如链表 1 -> 2 -> 3 -> 4 -> 5,k = 3,正数第 3 个和倒数第 3 个都是节点 3,此时“交换”等于原地不动。这是一个容易忽略、但在代码里天然成立的边界情况:只要你的定位逻辑正确,两个指针指向同一个节点时交换操作也不会产生副作用,但如果你写的交换代码没判空或者对同一节点做了重复断链,反而可能把链表搞坏。这个细节后面实操部分我会专门讲。
3.2 双指针法的核心:快指针多走 k-1 步
双指针解法是这道题的主流解法,Python、Java、C++、Go 都能用同一套逻辑。思路分成三步,第一步用快指针从头开始走 k-1 步,停在正数第 k 个节点上;第二步让慢指针从头出发,快指针继续走,直到快指针走到最后一个节点(注意不是空节点),此时慢指针正好停在倒数第 k 个节点;第三步交换这两个节点。很多资料会把第二步描述成“快指针走到空节点”,这其实是一种常见的写法差异:如果快指针走到 null 才停,那慢指针停的位置就是倒数第 k+1 个节点,需要额外往前走一步,容易把自己绕晕。我建议统一让快指针停在最后一个非空节点上,这样慢指针停留的位置刚好是倒数第 k 个节点,逻辑最顺。
为什么快指针要先走 k-1 步而不是 k 步?因为链表的 head 本身是第 1 个节点,走到第 k 个节点只需要移动 k-1 次。这个“差一”的问题几乎是所有链表下标类题目的重灾区。我见过很多人在 Leetcode 1721 的题解区吐槽“为什么和之前做过的某题不一样”,其实都是没有统一好“从 0 开始计数”和“从 1 开始计数”的语义。用一个生活化的类比来解释:如果队伍排队买票,你站在第 k 个位置,你前面有 k-1 个人;想让另一个人跟你对齐,他需要从队首走过 k-1 个人,不多不少。链表里的次数遍历也是这个道理。
3.3 边界条件与语义统一
这道题的边界条件主要有三个:k=1、k=链表长度、链表长度为奇数且 k 在中点。先说 k=1 的情况,正数第 1 个节点是 head,倒数第 1 个是链表的最后一个节点,双指针走完,slow 会停在尾节点,交换头尾即可。这里有个很隐蔽的坑:如果同时交换“节点位置”,需要处理 prev 指针的更新,因为头节点的前驱是 null,交换头尾会让头节点本身发生变化。如果只是交换值,代码简单很多,但严格来说链表节点的地址没有变,只是每个节点里的 val 换了一下。刷题阶段用值交换做优化是没问题的,但题目里如果有“不能只交换值”的附加要求,就必须走真正的断链重接。
再说 k=链表长度的情况。正数第 k 个节点就是最后一个节点,倒数第 k 个就是头节点,和 k=1 本质上是同一个对称操作。所以只要你写的双指针逻辑对,这两个边界会自动满足。链表长度为奇数且 k 在中点时(比如长度 5、k=3),两个指针会指向同一个节点。做值交换时,a.val 和 b.val 互换等于没换,结果不变;做节点交换时,因为 a 和 b 是同一个对象,需要先判 a == b 直接 return,否则你断链再重接会把自己绕晕。我实际跑测试的时候,第一次没做这个判断,结果中间节点被分割成了两个部分,链表直接出现了环,调试了半天才发现是对同一节点执行了两次指针操作。
4. 实操路径一:交换节点的值(最简方案)
4.1 算法流程与代码示例
如果题目没要求物理交换节点,只要求最终链表呈现交换后的顺序值,直接交换两个节点的值是最推荐的做法。因为链表节点的内存地址没有变,外部持有的任何指向这些节点的引用依然有效,不会出现悬垂指针。我先把最简单、最不易出错的 C++ 实现写出来:
cpp复制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* swapNodes(ListNode* head, int k) {
ListNode* fast = head;
ListNode* slow = head;
// 1. fast 先走 k-1 步,定位到正数第 k 个节点
for (int i = 1; i < k; ++i) {
fast = fast->next;
}
ListNode* first = fast;
// 2. fast 继续走,走到最后一个节点为止,slow 同步走
while (fast->next) {
fast = fast->next;
slow = slow->next;
}
ListNode* second = slow;
// 3. 交换两个节点的值
swap(first->val, second->val);
return head;
}
};
这段代码有两个地方特别值得注意。第一,fast 先走 k-1 步时,如果 k 等于链表长度,最后一次循环 fast = fast->next 之后 fast 依然非空,因为第 k 个节点恰好是最后一个节点,它的 next 是 nullptr 但不影响我们持有这个节点的指针。第二,while (fast->next) 循环里 fast 最终停在最后一个非空节点,slow 最终停在倒数第 k 个节点,这个对应关系需要自己走一遍例子验证。拿 1 -> 2 -> 3 -> 4 -> 5,k = 2 举例:fast 先走 1 步到节点 2,first 指向节点 2;然后循环开始,fast 当前是节点 2,fast->next 是节点 3,所以进入循环 fast 变为节点 3、slow 变为节点 2;接着 fast 变为节点 4、slow 变为节点 3;接着 fast 变为节点 5、slow 变为节点 4;此时 fast->next 是 nullptr,循环终止,slow 指向节点 4,正好是倒数第 2 个节点的位置。整个过程 fast 一共走了 (k-1) + (n-k) = n-1 步,slow 走了 n-k 步,完全符合预期。
4.2 值交换的局限性与适用场景
值交换代码短、可读性高、不出错,但它有个隐含假设:节点的 val 是允许被修改的。如果链表节点里除了 val 还有额外的字段,比如 id、name、指向其他数据的指针,那交换值就只能保证 val 字段符合题目预期,其他字段不变,这在工程场景里往往不是真正的“交换节点”。力扣上的题目不会考这个,但你去面试时如果主动提出“只交换值可以吗”,面试官大概率会追问一句“如果节点里有一个外部索引指向这些节点,值交换会有问题吗”——这就是在考察你有没有理解链表节点的物理本质。我的建议是:刷题时用值交换快速通过没问题,但面试前一定要把断链重接的写法也吃透,因为你永远不知道面试官会不会追加一个“不能用值交换”的限制条件。
5. 实操路径二:真正地交换节点位置(断链重接)
5.1 为什么要做真正的节点交换
当题目要求“交换节点”且不允许只交换值时,你需要通过修改链表节点的 next 指针来交换节点在链表中的位置。这就像现实中交换两个员工的位置,不是交换他们胸牌上的名字,而是让他们真的走到对方的工位上去。断链重接的过程稍微复杂,因为你要处理四个指针:第一个节点的前驱、第一个节点、第二个节点的前驱、第二个节点。但好消息是,只要先定位好这两个节点以及它们各自的前驱,剩下的事情就是一板一眼的指针重新指向。为了防止头节点被换掉导致返回结果出错,通常需要引入虚拟头节点 dummy,让 head 也有一个统一的前驱。
5.2 用虚拟头节点统一处理头尾交换
虚拟头节点是个非常实用的技巧,很多链表面试题在需要修改头节点时都会用到。做法是创建一个值为 0、next 指向 head 的新节点 dummy,然后所有关于“前驱”的判断都从 dummy 开始。这样做的好处是:哪怕交换的是头节点和尾节点,头节点的前驱是 dummy 而不是 null,代码里不需要单独写 if 分支。我先把 C++ 的完整实现写出来,再加注释逐行解释。
cpp复制class Solution {
public:
ListNode* swapNodes(ListNode* head, int k) {
ListNode* dummy = new ListNode(0, head);
ListNode* fast = dummy;
ListNode* slow = dummy;
// fast 先走 k 步,注意这里从 dummy 出发,走 k 步到正数第 k 个节点
for (int i = 0; i < k; ++i) {
fast = fast->next;
}
ListNode* first = fast;
ListNode* firstPrev = slow; // 此时 slow 指向 first 的前驱?不对,这里要小心
// 这个写法我写错了,重新整理
// 正确版本:统一从 dummy 出发
fast = dummy;
slow = dummy;
for (int i = 0; i < k; ++i) {
fast = fast->next;
}
ListNode* first = fast;
// 记录 first 的前驱,方法是让 slow 同步走 k-1 步
slow = dummy;
for (int i = 1; i < k; ++i) {
slow = slow->next;
}
ListNode* firstPrev = slow;
// 重新定位 fast,从 first 位置继续走到最后一个节点
while (fast->next) {
fast = fast->next;
slow = slow->next;
}
ListNode* second = slow;
ListNode* secondPrev = ?; // 这里需要用另一个指针记录 second 的前驱
// 这种写法越写越复杂,其实有更简洁的做法
}
};
说实话,上面这个版本我写着写着就发现前驱跟踪容易乱,因为 secondPrev 这一步不好直接得到。实际上更常见的做法是先用双指针定位 first 和 second,再分别遍历一次链表找到它们各自的前驱。代价是多了一次 O(n) 前驱查询,但逻辑清晰很多,面试时不容易说错。
cpp复制class Solution {
public:
ListNode* swapNodes(ListNode* head, int k) {
ListNode* first = head;
ListNode* second = head;
ListNode* fast = head;
for (int i = 1; i < k; ++i) {
fast = fast->next;
}
first = fast;
while (fast->next) {
fast = fast->next;
second = second->next;
}
// 如果两个节点是同一个,直接返回
if (first == second) return head;
// 分别找前驱
ListNode* dummy = new ListNode(0, head);
ListNode* firstPrev = dummy;
while (firstPrev->next != first) {
firstPrev = firstPrev->next;
}
ListNode* secondPrev = dummy;
while (secondPrev->next != second) {
secondPrev = secondPrev->next;
}
// 断链重接
ListNode* firstNext = first->next;
ListNode* secondNext = second->next;
// 处理相邻场景:first 是 second 的前驱
if (first->next == second) {
first->next = secondNext;
second->next = first;
firstPrev->next = second;
}
// 处理相邻场景:second 是 first 的前驱
else if (second->next == first) {
second->next = firstNext;
first->next = second;
secondPrev->next = first;
}
// 一般场景
else {
firstPrev->next = second;
secondPrev->next = first;
first->next = secondNext;
second->next = firstNext;
}
return dummy->next;
}
};
这段代码里最值得讲的就是相邻节点分支处理。如果你不处理 first->next == second 这种相邻情况,直接用通用交换逻辑,会出现一个经典 bug:firstPrev->next = second 之后,secondPrev 如果是 first,那么 secondPrev->next = first 这句又把 second 的 next 指回了 first,形成一个环。我最初没写相邻判断,测试用例链表 1 -> 2 -> 3、k = 2 时直接超时,因为链表里出现了循环。之后我打印每一步的指针地址才发现问题:当两个目标节点相邻时,它们的 next 关系互为交错,必须先处理相邻场景,否则指针操作会互相覆盖。
5.3 相邻节点交换的分支处理解析
拿链表 1 -> 2 -> 3,k = 2 举例。first 是节点 2,second 是节点 2?不对,长度是 3,k = 2,正数第 2 个是节点 2,倒数第 2 个也是节点 2,它们是一个节点,会提前 return。所以相邻场景需要一个更合适的例子:链表 1 -> 2 -> 3 -> 4,k = 2,正数第 2 个是节点 2,倒数第 2 个是节点 3,它们相邻。此时 firstPrev 是节点 1,secondPrev 是节点 2(也就是 first),firstNext 是节点 3(也就是 second),secondNext 是节点 4。如果走通用逻辑:firstPrev->next = second,链表变成 1 -> 3 -> ...;secondPrev->next = first,即节点 2 的 next 指向节点 2,形成自环;first->next = secondNext,节点 2 指向节点 4;second->next = firstNext,节点 3 指向节点 3,也是自环。这一步错得非常隐蔽,单看某一句似乎都合理,合在一起就会产生环。用了相邻分支之后,first->next == second 成立,执行 first->next = secondNext(节点 2 指向节点 4)、second->next = first(节点 3 指向节点 2)、firstPrev->next = second(节点 1 指向节点 3),最终链表变成 1 -> 3 -> 2 -> 4,完全正确。
还有一个对称的相邻场景:second->next == first,出现在 k = 3、链表长度 4 的情况,正数第 3 个是节点 3,倒数第 3 个是节点 2,两者相邻且顺序是 second 在前、first 在后。代码里第二个 else if 分支专门处理这种情况。
如果不引入相邻分支,也可以统一用“保证 first 在前、second 在后”的交换逻辑,先判断两者在链表中的先后位置,再统一处理成 first 在前的情况,之后所有操作都按一种相邻模式执行。这种写法更少分支,但理解成本高一些。我在面试时更倾向用显式分支,因为思路直白,不容易在紧张状态下写错。
6. 递归视角:从“删除倒数第 N 个节点”的迁移
6.1 递归定位倒数第 k 个节点
写代码之外,还有一类解法是递归。Leetcode 1721 不是递归最优解,但递归思想可以帮你加深对“倒数第 k 个节点”的理解。定义递归函数返回当前节点后面还有多少个节点,当计数等于 k 时,当前节点就是倒数第 k 个节点。这种思路可以迁移到删除倒数第 N 个节点、删除链表中间节点等题目。缺点是递归会用到系统栈,链表很长时(比如 10 万个节点)可能栈溢出。实际的力扣测试用例一般不会那么极端,但面试时最好提一句“递归解法需要额外 O(n) 栈空间,双指针是 O(1) 空间”的取舍。
6.2 递归写法的 Python 示例
python复制class Solution:
def swapNodes(self, head: ListNode, k: int) -> ListNode:
self.first = None
self.second = None
self.count = 0
def dfs(node):
if not node:
return 0
depth = dfs(node.next) + 1
if depth == k:
self.second = node
return depth
# 定位正数第 k 个
cur = head
for _ in range(k - 1):
cur = cur.next
self.first = cur
dfs(head)
if self.first and self.second:
self.first.val, self.second.val = self.second.val, self.first.val
return head
这段代码里,dfs 返回的是当前节点到链表末尾的距离,等于 n - index + 1。当返回值等于 k 时,当前节点就是倒数第 k 个节点。如果你第一次写递归链表题,建议先在纸上画一个 5 节点的链表,手动走一遍递归的过程,体会“回调时计数”和“正向遍历计数”的区别。递归写法的优点是代码短、不用双指针,缺点是空间复杂度不是常数,在工程上不如迭代方案实用。我一般用这种方式作为双指针方案的“对照实验”,面试里如果面试官问“能不能用递归做”,可以立刻切换。
7. 从热词延展:双链表、循环链表与相关变种
7.1 双链表交换节点与 C++ 结构体基础
我注意到热词里出现了很多“php双链表”“c++结构体链表基本语法”“循环单链表”这类词,它们并不是 Leetcode 1721 的直接内容,但确实说明了链表话题下大家最常搜的方向。双向链表在做节点交换时有一个天然优势:每个节点都存了 prev 指针,找前驱不需要额外遍历,直接 prev 就能拿到。代价是每个节点多一个指针的内存开销。C++ 里写双向链表结构体通常是:
cpp复制struct DoublyListNode {
int val;
DoublyListNode* prev;
DoublyListNode* next;
DoublyListNode(int x) : val(x), prev(nullptr), next(nullptr) {}
};
如果是单向链表,想交换任意两个节点,前驱只能通过遍历查找。Leetcode 1721 给的是单向链表,所以前驱查找天然是 O(n) 的。这正是它和“数组交换元素”最大的区别:数组可以通过下标直接访问相邻元素,链表必须在物理上前进。
7.2 循环链表与“从尾到头数”的另一种理解
循环单链表里没有“最后一个节点”的概念,因为最后一个节点的 next 会指回 head。如果把 Leetcode 1721 改成循环链表版本,“倒数第 k 个节点”的定义就会发生变化:需要先确定链表长度,或者从某个起点开始数到第 n-k+1 个节点。热词里出现“循环单链表”说明有不少人在刷这类变种。我建议先把 Leetcode 1721 的普通链表版本吃透,再去想循环链表的处理方式,否则很容易混。
7.3 相关高频题的横向对比
Leetcode 1721 和“删除倒数第 N 个节点”“合并两个有序的单链表”“单链表逆序”是同一批高频链表题。它们的共同点都是训练你对“指针移动次数”和“节点引用”的敏感度。比如删除倒数第 N 个节点,常见的做法也是双指针:快指针先走 N 步,然后快慢指针同时走,快指针到 null 时慢指针停在待删节点的前驱。这和 Leetcode 1721 的 slow 停在倒数第 k 个节点不同,因为删除需要前驱,而交换需要自身。这种细节差异特别适合当作对比学习材料,可以加深对双指针模板的理解,而不是死记硬背每一题的解法。
8. 常见问题与排查实录
8.1 问题速查表
我整理了这道题最常见的 6 个问题和对应的排查思路。
| 异常现象 | 可能原因 | 排查与修复 |
|---|---|---|
| 超时(链表一直循环) | 交换两个相邻节点时没做分支处理,next 指针互相覆盖成环 | 打印指针地址,检查是否 first->next == second 或 second->next == first |
| 结果链表中出现重复节点 | 断链重接顺序有误,节点同时被多个前驱指向 | 在交换前保存 firstNext 和 secondNext,交换时按“先改前驱再改孩子”的顺序 |
| k=1 时结果不对 | 没有使用虚拟头节点,头节点被换掉后返回的还是旧 head | 统一引入 dummy 节点,最后返回 dummy->next |
| k 等于链表长度时结果不对 | 快指针先走步数语义混乱,走了 k 步或 k-1 步没统一 | 严格统一:快指针定位正数第 k 个节点需要走 k-1 步 |
| 奇数长度、k 在中点时链表损坏 | 两个节点是同一个节点,交换代码未判等 | 在交换前增加 if (first != second) 的判断 |
| 空链表或 k 越界 | 没有处理输入异常 | 根据题目约束判断 head 是否为空,k 是否在 1 到链表长度之间 |
8.2 现场调试记录
我在本地用 Python 写了一个测试脚本,用链表 1 -> 2 -> 3 -> 4 -> 5 分别测 k=1、2、3、4、5,全部走通之后,又测试了链表 1 -> 2,k=1 和 k=2 这两种极端场景。只有两个节点时,k=1 交换的是节点 1 和节点 2,值交换结果 2 -> 1,节点交换结果也是 2 -> 1,但相邻节点分支处理时必须非常小心,否则会产生只有两个节点的链表被交换成只有单个节点的悲剧。我实测中发现,链表 1 -> 2,k=1,如果走通用断链逻辑,firstPrev 是 dummy,secondPrev 是节点 1,firstNext 是节点 2,secondNext 是 nullptr,交换后 dummy->next 应该指向节点 2,但通用逻辑会先执行 firstPrev->next = second,即 dummy->next = 节点 2;再执行 secondPrev->next = first,即节点 1(secondPrev 是节点 1)的 next 指向节点 1,形成自环;执行 first->next = secondNext,即节点 1 指向 nullptr;执行 second->next = firstNext,即节点 2 指向节点 2,形成另一个自环。所以只有两个节点时就是相邻分支,必须走 first->next == second 的逻辑,处理完结果是 dummy->next = 节点 2,节点 2 的 next = 节点 1,节点 1 的 next = nullptr,输出 2 -> 1,正确。
我用这个表当排查手册,每次出错先按表里的类别定位,效率明显比漫无目的地打印日志高得多。
8.3 一个容易忽略的性能细节
这道题如果用“先定位两个节点,再分别遍历找前驱”的写法,总时间复杂度是 O(n),但常数项比双指针一次定位要大。力扣的测试用例规模通常不会让二者有明显差距,但面试官如果追问“两次遍历和一次遍历有什么本质区别”,你要能答出来:两次遍历的唯一区别是多了常数时间的遍历次数,量级没有变化。实际工程中,如果链表很大且交换操作频繁,应优先考虑双向链表或维护节点地址索引,而不是反复遍历单向链表。这个思路也可以直接迁移到“边缘节点去重算法”“worker 节点删除”等热词背后的场景——凡是频繁需要定位和修改链表节点的系统,都要谨慎设计数据结构。
9. 实操心得与扩展建议
这道题我在不同阶段做过三遍,每一遍的感觉都不一样。第一遍用值交换,觉得 10 行代码就搞定了;第二遍要求自己写断链重接,发现坑比想象中多;第三遍开始把双链表、循环链表、递归这些变体全部拉通,才算真正觉得链表的基本功扎实了。所以我能给的第一个建议是:不要满足于“做对”,要刻意逼自己用不同的解法做同一道题。每换一种解法,你对链表指针的理解就会深一层。
第二个建议是关于刷题顺序的。如果你是一个刚接触链表的初学者,我推荐的顺序是:先做 206 反转链表,掌握最基本的指针重指向;再做 19 删除倒数第 N 个节点,掌握双指针定位倒数节点的模板;接着做 1721 交换链表中的节点,把定位和交换结合;最后做 21 合并两个有序链表,练习多指针同时移动。这套组合拳打下来,链表类题目的基础就相当扎实了。我见过太多人一上来就啃困难题,结果在 medium 题上反复卡壳,其实是因为基础题里的指针操作没有形成肌肉记忆。
第三个建议关于面试现场:如果碰到这道题,先说清楚“是否可以只交换值”,再看面试官的反应决定写值交换还是节点交换。即使题目本身没有额外要求,你主动指出这个前提条件,会让面试官觉得你边界意识很强,而不是一个只会背模板的人。写代码的时候,先画两个几节点的链表,把交换过程手动走一遍,再动键盘写代码。这个习惯帮你减少至少一半的调试时间。
从更广的角度看,Leetcode 1721 的核心价值不只是这道题本身,而是它依赖的三个通用抽象能力:定位(通过步数控制找到任意位置的节点)、修改(通过前驱和后继的指针操作改变链表结构)、边界(处理相邻、单节点、奇偶长度等特殊情况)。这三个能力几乎贯穿了所有链表题目,也是在实际工程里操作链表、树、图等引用型数据结构的基础。后续如果你继续深入“合并两个有序的单链表”“单链表逆序”“边缘节点去重算法”,会发现它们仍然是同一个思维模型在不同场景下的变体。把这层关系想清楚,刷题就不再是背题,而是真正掌握一类问题。
