两两交换链表中的节点:虚拟头节点与指针重连全解析

发布时间:2026/10/19 9:00:19
两两交换链表中的节点:虚拟头节点与指针重连全解析 说实话第一次在题库里刷到“两两交换链表中的节点”这个题很多人的第一反应是这不就是遍历一遍链表的事吗有什么好练的但等你真的在面试现场被问到或者给学弟学妹讲过一次就会明白这道题完全是一只“温水煮青蛙”——表面看是简单题实际上把链表操作里最容易翻车的几个点全占了指针重连顺序、头节点变化、边界条件、递归与迭代的权衡每一个都值得单独拎出来掰扯。先解释一个容易搞混的点。这个题在题库里的标准编号其实是 24题名就叫 Swap Nodes in Pairs。至于 104那是一道完全不同的题目——“二叉树的最大深度”考的是递归和树的遍历。刷题的时候把题号记混太常见了我见过不止一个同学对着 104 的题面找链表解法找了半天发现牛头不对马嘴。所以看题别看编号看内容和考点。题目要求非常干脆给定一个单链表把相邻的两个节点交换位置返回交换后的链表头。硬性约束是——不能只交换节点里的数值必须真实地调整节点之间的指针指向。这个约束很关键它堵死了“改数值糊弄过去”的捷径逼着你真正操作链表结构。举个例子1 - 2 - 3 - 4 交换后是 2 - 1 - 4 - 3如果是奇数个节点最后一个节点原地不动1 - 2 - 3 变成 2 - 1 - 3。这道题我强烈建议所有准备算法面试的人认真刷尤其是刚接触链表的新手。它不像反转链表那样有明显的套路模板也不像合并有序链表那样有现成的双指针逻辑可以套它考验的是你对“指针重新连接”这件事最底层的理解。把这题吃透后面遇到反转链表 II、K 个一组翻转链表这些变种题你会明显觉得轻松一大截。1. 题目拆解这个高频题到底在考察什么1.1 题意与示例别被“简单题”标签骗了题目本身没有任何弯弯绕给定一个单链表把第 1、2 个节点交换第 3、4 个节点交换依此类推。链表为空或者只有一个节点时直接返回原链表。输出只要满足“两两交换后的链表结构”即可不需要额外构造任何容器。举个例子输入是 1 - 2 - 3 - 4输出就是 2 - 1 - 4 - 3。这里要注意的是交换的对象是“节点”而不是“数值”。题目明确禁止只交换节点里的值。至于为什么会有这个约束我多说两句在很多真实的工程场景里链表节点往往不只是存一个整数可能带着一个很大的结构体、一个文件块、甚至是一把锁的持有信息。你把值换了外面拿着旧节点引用的人就会一脸懵。更重要的是面试官想考察的就是你能不能流畅地操作指针而不是靠取巧。节点是奇数个的情况也很常见1 - 2 - 3 两两交换后前两个换成 2 - 1第三个节点没有配对对象所以保持原样最终输出 2 - 1 - 3。这个边界情况虽然不影响核心算法但很多人第一次写代码时根本没往这方面想等到测试用例一跑才发现漏了。1.2 为什么高频出现从一道题看四种基本功我在很多公司的算法面试题单里都见过这道题它不是偶然。一道看起来只有十几行代码的题其实同时踩中了四个基本功考点链表遍历、指针重连、递归理解、边界处理。链表遍历是基础中的基础但这里不是让你简单从头走到尾而是要在遍历过程中改变链表结构。很多初学者能顺着 next 指针走到底但一旦需要在遍历中途“动刀”就不知道如何保证不断链、不错位。指针重连是这道题的核心也是最容易出 bug 的地方。一次交换要涉及至少三次指针修改这些修改的顺序稍有差池轻则丢节点重则链表成环死循环。很多人写完代码看着逻辑好像没毛病一跑就是超时原因往往就是某个指针指错了位置。递归理解是第二种打开方式。同一个问题用递归写代码量能压缩到五行左右但每一行都暗含“缩小问题规模”的设计。能把递归解法的调用栈讲清楚的人通常对“分治”思想已经有比较扎实的感觉。边界处理是最后一道关卡。空链表、单节点、奇数长度、偶数长度这四种情况如果没有在动手前过一遍大脑写出来的代码大概率是漏的。而不同解法的边界敏感点还不一样迭代法的边界藏在 while 条件里递归法的边界藏在 base case 里后面我都会细讲。所以这道题的定位很微妙它简单到新手能写出一个能跑的版本又难到很多人写不出一个完全正确的版本。这种“看似简单、实则容易翻车”的特质正是面试官偏爱它的原因。2. 解法一迭代法 虚拟头节点2.1 虚拟头节点dummy node的设计动机迭代法第一个要解决的问题就是头节点会变。原来的 head 是节点 1两两交换之后整个链表新的头变成节点 2。这意味着如果你直接操作 head最后返回 head 的时候返回的是一个已经不位于链表头部的节点结果必然错误。虚拟头节点dummy node就是为了抹平这种差异而存在的。思路很朴素在真实链表头部之前额外创建一个节点 dummy让 dummy.next 指向原来的 head。这样一来整个交换过程你都可以统一地用“某个节点的 next 需要改成什么”来描述完全不用单独处理“头节点变了怎么办”这个分支。我常打一个比方这就像排队换座位队首的同学要跟后面的同学交换位置这时候队首到底是谁就很难一句话说清。但如果在队伍最前面加一个“管理员位”管理员不动只负责记录当前队首是谁那无论里面怎么换你只需要问管理员。dummy 就是这个“管理员位”。等所有交换完成真正的链表头就是 dummy.next直接返回它。这个技巧在链表题里的通用性极强凡是涉及“头节点可能变化”的场景比如删除某个节点、合并两个链表、反转链表、删除倒数第 N 个节点dummy node 都是最常用的解法之一。你在这道题里把它用熟了等于多了一把万能钥匙。2.2 三指针迭代每一步指针都指到哪有了 dummy 之后核心逻辑就可以统一成一个“三步走”的循环。我们定义三个角色prev 表示已经处理完的链表末尾first 和 second 表示当前这一对里待交换的两个节点。初始时prev dummyfirst 是链表第一个节点second 是第二个节点。每一轮循环做三件事第一件事把 first.next 指向 second.next。这是“保存后续”的动作。当前这一对交换完之后first 会退居这对节点的第二个位置而 first.next 必须指向后面还没处理的那部分链表。这一步同时承担了“断开旧连接”和“备份下一段链表”两个职责。第二件事把 second.next 指向 first。这一步真正完成了这一对内部的反转让原本的第二个节点变成新的一对里的第一个节点。第三件事把 prev.next 指向 second。这是把已经处理好的链表尾巴和当前这对节点的新头部接起来让整条链表保持连贯。三步做完还要有一个收尾动作把 prev 更新为 first。为什么因为经过交换first 已经是这对节点里的第二个而 first.next 指向的正是下一对待处理节点的起点。prev 必须站到这个新的“已处理链表末尾”上下一轮循环才能从正确的位置继续。这里最需要刻意记的顺序是first.next second.next 必须发生在 second.next first 之前。因为一旦 second.next 指向了 first你就失去了“这对节点后面的链表”的引用后面的节点全部会跟丢。这个顺序是所有 bug 里最经典的一个后面我会专门拆开讲。2.3 完整代码与逐行说明Java 的完整实现如下public ListNode swapPairs(ListNode head) { // 虚拟头节点统一处理头节点变化的边界情况 ListNode dummy new ListNode(0); dummy.next head; ListNode prev dummy; // 只有 prev 后面至少还有两个节点时才需要交换 while (prev.next ! null prev.next.next ! null) { ListNode first prev.next; // 待交换的第一个节点 ListNode second first.next; // 待交换的第二个节点 // 第一步first 指向下一对的起点防止链表断开后找不回后续 first.next second.next; // 第二步反转这一对second 成为新头部 second.next first; // 第三步把已经处理完的部分和这一对的新头部接上 prev.next second; // 前进prev 移动到这一对的末尾也就是交换后的 first prev first; } return dummy.next; }我强烈建议第一次写链表题的时候别着急提交先在草稿纸上把 1 - 2 - 3 - 4 画出来每执行一个语句就在旁边更新一遍链表图。不用画得多精致箭头和方框就够了但一定要亲手画。我带过的同学里坚持画图的人写链表题的 bug 率明显低于直接上手敲代码的人这不是玄学是因为指针操作太抽象视觉化之后每一步的对错一眼就能看出来。while 循环条件prev.next ! null prev.next.next ! null是我个人认为最稳妥的写法。它同时处理了两种边界链表为空时prev.next 是 null第一个条件就不成立链表只有一个节点时prev.next 有值但 prev.next.next 是 null第二个条件不成立。两种情况都直接跳过循环返回结果正确完全不需要额外写 if 判断。3. 解法二递归的另一个视角3.1 递归的切入角度把问题缩小成“两个节点”迭代解法是从头到尾“一根筋”地处理递归解法的思维完全不同。递归的核心是我不要想着一次处理完整个链表我只需要想清楚“当前这两个节点怎么处理”剩下的交给函数自己。具体来说假设链表是 1 - 2 - 3 - 4。我可以先只看第一对1 和 2。剩下的 3 - 4本质上也是一个需要两两交换的链表这就是一个规模更小的同类问题。我调用 swapPairs(3 - 4)它会返回 4 - 3。拿到这个结果后我再把 1、2 和递归返回的结果拼起来。拼装顺序是这样的2 要变成新链表的头部1 跟在 2 后面而 1 的后面就是递归返回的 4 - 3。所以最终结果是 2 - 1 - 4 - 3。这个思路一旦理清代码非常短def swapPairs(head): # 空链表或只有一个节点不需要交换 if not head or not head.next: return head first head second head.next # first 的下一个节点交给递归去处理后面的部分 first.next swapPairs(second.next) # second 变成这一对的新开头 second.next first return second这个解法的妙处在于它完全没有显式的循环而是让递归自动把“两两交换”这个操作传递到链表末尾。你不需要关心链表有多长递归会一直深入到最后一对然后从后往前一层层组装回来。3.2 递归代码与调用栈分析很多同学第一次看递归版本时最困惑的问题是更深层到底发生了什么为什么我看到的代码只有这几行但整个链表都被正确交换了我们完整跟踪一遍。以 1 - 2 - 3 - 4 为例第一次调用head 是 1。first 1second 2走到first.next swapPairs(second.next)。此时 second.next 是 3所以递归进入第二层处理以 3 开头的链表。第二层调用head 是 3。first 3second 4second.next 是 null。再次调用 swapPairs(null)进入第三层。第三层调用head 是 null满足 base case直接返回 null。回到第二层拿到了 null接着执行first.next null也就是 3 - null然后 second.next first也就是 4 - 3。第二层返回 second即 4 - 3。回到第一层拿到了 4 - 3执行first.next (4 - 3)也就是 1 - 4 - 3然后 second.next first也就是 2 - 1。最终第一层返回 second也就是 2 - 1 - 4 - 3。整个过程就是典型的“先递后归”递是不断深入直到链表末尾或空节点归是从最深层开始逐层把交换好的子链表返回给上一层。你在纸上跟着这个调用链走一遍比读十遍文字都管用。递归版本里最核心的一行是first.next swapPairs(second.next)。它同时做了两件事把当前这对节点中 second 后面的链表交给递归处理并且把 first 的 next 直接指向递归返回的结果。这一步完成之后second.next 就可以安全地指向 first因为“后面的链表”已经被妥妥地保存在 first.next 里了。3.3 迭代 vs 递归面试时怎么选两种解法的时间复杂度完全一样都是 O(n)但在空间上差别明显迭代法只用常量级别的额外空间O(1)递归法因为每一层调用都会在系统栈上压一个栈帧链表长度为 n 时调用深度是 n/2空间复杂度 O(n)。在工程实践中迭代法显然更可靠。尤其是链表特别长的时候递归可能直接把系统栈打爆我在本地测试环境里跑过十万节点的链表递归版本直接 StackOverflow换成迭代版本秒过。但面试场合我更推荐你采取一个“组合拳”策略先讲递归版本因为它代码短、逻辑清晰面试官很容易跟上你的思路。讲完之后主动补一句“递归版本空间复杂度是 O(n)如果希望做到 O(1) 额外空间我可以改成迭代写法。”然后当场把迭代版本写出来。这一套下来既展示了你对递归的理解又展示了你对空间复杂度的敏感还能展示出你的代码组织能力分数通常不会差。我最怕看到的情况是候选人只写递归答不出空间复杂度是多少或者只写迭代讲不清 dummy node 为什么能省掉一堆边界判断。两种解法之间切换自如才是真正把题吃透的表现。4. 复杂度分析、边界条件与常见坑4.1 时间与空间复杂度先给结论迭代和递归的时间复杂度都是 O(n)空间复杂度分别是 O(1) 和 O(n)。时间上为什么是 O(n)迭代版每轮循环处理两个节点一共执行 n/2 轮每轮内部操作是常数次指针赋值所以总时间正比于链表长度。递归版同样每一层递归处理两个节点总共递归 n/2 层每层操作时间恒定总和也是 O(n)。这个结论不会因为链表长度变化而改变属于“线性复杂度”的典型例子。空间上的差异值得展开说说。迭代版的额外变量只有 dummy、prev、first、second 这四个指针无论链表多长变量个数都不变所以是 O(1)。递归版则不然每次调用 swapPairs 都会在系统栈上压入一层栈帧里面保存了当前层的 first、second 等局部变量和执行位置。链表越长栈帧越多。n 足够大时栈溢出不是理论上的风险而是实实在在会碰到的问题。有同学可能会想“那我是不是应该无脑选择迭代法”其实没必要这么极端。工程中如果明确知道链表长度有限递归的简洁性也是价值面试中你展示了递归再补出迭代优化比直接只给一个版本更有说服力。4.2 边界条件自查清单我把这道题所有边界情况整理成一张表写完代码后按这个清单过一遍基本能覆盖所有测试用例。场景链表状态期望输出解法覆盖点空链表nullnullwhile 条件第一个判断退出 / base case 命中单节点11while 条件第二个判断退出 / base case 命中两个节点1-22-1只走一轮循环偶数长度1-2-3-42-1-4-3完整走完全部交换奇数长度1-2-32-1-3最后一轮条件不满足最后一个节点保留最容易出错的是奇数长度。很多人在写迭代版时图省事把 while 条件写成while (prev.next ! null)看起来像是“只要还有节点就继续处理”但实际上当链表是 1 - 2 - 3 时第二轮循环中 prev 指向 1prev.next 指向 3条件成立进入循环后 second 是 3 的 next也就是 null代码访问 null.next 直接抛空指针异常。所以 while 条件里必须同时检查prev.next ! null prev.next.next ! null把“不足一对”的情况挡在循环外面。4.3 高频 Bug 现场指针顺序错了会怎样我在答疑、辅导和模拟面试里见过的错误九成以上都出在指针重连顺序上。这里拆两个典型错误。第一个错误是“先反转再保存后续”。有人会把second.next first写在first.next second.next之前。表面上看两个语句都有逻辑似乎没少但问题出现在执行顺序上。second.next first执行完之后second 的 next 已经变成了 first原来 second 后面的链表地址就丢了。再执行first.next second.next取到的其实是 first 自己于是 first 指向自己链表成环程序死循环。这种 bug 最坑的地方在于它不容易被直觉发现。你看代码每个变量都用上了语句也没漏逻辑似乎完整但链表就是转圈。如果不画图、不打印盯着屏幕看半天都可能发现不了问题根源。第二个错误是“忘了推进 prev”。只写了prev.next second但最后忘记执行prev first。第一轮交换没有问题第二轮循环开始时 prev 还停在 dummy程序把已经交换好的前两个节点又当成一个“新的一对”交换了一遍结果整个链表被打回原形。输入 1 - 2 - 3 - 4辗转反侧后输出还是 1 - 2 - 3 - 4。这种 bug 比第一个更隐蔽因为它既不崩溃也不死循环只是在功能上错得离谱。不逐步跟踪很多人根本意识不到是漏了一句赋值。针对这两个高频 bug我分享一个自己的排查习惯每次写完链表操作代码都手动模拟两轮循环。第二轮模拟时格外注意 prev 位置是否正确。这个习惯帮我省下了无数次无意义的提交也推荐给你。写链表题慢就是快。5. 从这道题延伸出去面试加分技巧与相关题串5.1 面试官想看到的答题节奏很多人以为面试题答对了就行其实答题节奏同样重要。链表这类基础题面试官的预期是你看一眼能识别出考点然后能讲清思路、写出代码、分析复杂度、再答出边界条件。整个过程控制在 15 到 20 分钟内是合理目标。我个人推荐的节奏是前 3 分钟讲思路中间 8 到 10 分钟写代码最后 2 到 3 分钟主动整理边界条件并口头跑一遍测试用例。写代码前一定要先跟面试官对齐思路不要拿过笔就写。你讲思路的过程既是展示深度理解的机会也是给自己争取一个“白板推演”的过程。针对这道题一个较好的展开方式是这样的先说“这个题最麻烦的地方在于头节点会变所以我会加一个虚拟头节点来统一处理”然后写迭代版本同时解释每一步指针操作的意图写完主动说“其实这个题还有递归写法代码更短但空间复杂度是 O(n)如果要严格做到 O(1) 额外空间迭代版更优”。这段话一出口面试官大概率会觉得你掌握得很全面因为你不仅给出了答案还顺手展示了复杂度意识和多方案对比能力。5.2 真题串讲同套路题目对照表两两交换链表中的节点不是孤立的题它属于“链表指针重连”这个大家族。把它吃透之后下面这些题你再看会觉得眼熟不少题目与本体的联系关键差异反转链表同样是重连指针从头到尾全部反转而非两两一组反转链表 II指定区间内反转需要先定位区间边界K 个一组翻转链表两两交换是 K2 的特例每组长度变成 K还要处理剩余不足一组的情况删除链表倒数第 N 个节点同样依赖 dummy node 处理头节点目标是找边界而非重连指针我个人尤其推荐做一下“K 个一组翻转链表”。你会发现两两交换节点本质上就是 K2 的“分组翻转”两者的处理框架几乎可以平滑迁移先找到一组节点在组内做翻转再把组与组之间接起来。差别只在于组内翻转的逻辑从“交换两个节点”变成了“翻转 K 个节点”难度上是一个自然的递进。从更宏观的角度看链表题的核心能力其实是“精确追踪指针变化”的能力。不管是这一题、反转链表、还是合并两个有序链表底层都是同一套能力你知不知道当前每个指针指向谁以及改完一条连接之后还有没有途径访问到剩下的节点。把这道题真正练透比囫囵吞枣刷十道同类题要有用得多。我个人在实际操作中的体会是链表题最忌讳“背代码”。我初学阶段曾经把这道题的迭代写法背得滚瓜烂熟但面试官换了一种链表场景后我一时间不知道从哪下手。后来我改变了方法每次遇到链表题先把链表画出来把每一步指针变化标在图上再对着图写代码。坚持了几十道题之后链表类题目对我来说就不再是“记忆题”而是真正变成了“逻辑题”。最后再分享一个小技巧在本地调试链表题时可以写一个简单的打印函数把每个节点的值按顺序输出节点之间用箭头连接。每次修改指针后立刻打印一次你会非常直观地看到链表结构是如何逐步变化的。这个方法虽然原始但在排查指针顺序错误时效率比大多数调试器都高。刷题这件事慢就是快把一道经典题彻底吃透远比追求刷题数量更有价值。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询