LeetCode两数相加:链表竖式加法与虚拟头节点详解
2026/9/14 4:33:15 网站建设 项目流程

说实话,看到“LeetCode hot100——两数相加”这个标题,我第一反应就是想起了自己当年被链表支配的日子。hot100里有两道“两数”题,第一道是个数组题,叫“两数之和”,用哈希表三分钟AC,然后人就飘了。结果点开第二道“两数相加”,看到ListNodenext,直接人麻了。

很多人就是这么被这道题劝退的。但它真的难吗?不是。它就是看着唬人,本质上考的其实是你对链表结构和指针移动的基本功——加法本身,小学二年级就会了。这篇文章我把这道题从头到尾给你拆开讲透:从思路怎么来、代码怎么写、边界条件怎么想、到变体题怎么应对,一次全说清楚。无论你刚开始刷题,还是准备面试突击,这道hot100的链表题,都值得认真啃一遍。

1. 先把题目真正读透:两数相加到底在考什么

1.1 链表存储的“反常识”:数字为什么要逆序存

题目给了两个链表,每个链表存一个非负整数,关键点来了:数字是逆序存储的。

我第一次看到这个设定就很迷惑,正常人表示数字都是高位在前低位在后,比如342这个数字,写成链表不应该是3 -> 4 -> 2吗?怎么题目里全部反过来,是2 -> 4 -> 3

后来才明白,这样设计不是故意恶心人,而是在帮你省事。竖式加法是从低位开始算的,个位对齐、十位对齐、百位对齐,低位算完如果有进位,还要往高位传。链表呢,它天生只能从head往后遍历。如果数字正序存,那你要从个位开始加,就得先找到链表末尾才行。这意味着你得要么先反转链表,要么用栈把节点存起来再弹出来,本来一个简单题,硬生生被搞复杂了。

逆序存储的设计,本质上就是把“链表的起点”和“个位”对齐。链表的第一个节点就是数字的个位,第二个节点是十位,第三个是百位……从头到尾遍历一次,就是一次标准的从低位到高位的竖式加法过程。所以请记住一个直觉:题目给你逆序链表,其实是已经把最麻烦的“反转”替你做完了。

1.2 核心考点:这不是一道加法题,而是链表基本功的试金石

题目本身描述很简单:给定两个非空的链表,表示两个非负整数,请你返回一个新的链表来表示它们的和。很多人的第一反应是“就这?”,然后动手写,写着写着发现到处是坑。

这道题真正的考点,不是加法,而是下面几个“链表基本功”:

  • 链表的遍历:l1 = l1.next这种移动指针的操作要形成肌肉记忆;
  • 节点创建:new ListNode(xxx)之后怎么把节点串到已有的链表尾部;
  • 进位处理:每一位加法都要带上上一轮的进位carry;
  • 循环终止条件:两个链表长度可能不一样,一个走完了另一个还没走完;
  • 边界判断:最高位计算完之后,如果还有进位,需要在结果链表最后补一个节点。

这些考点单独拎出来都很基础,但合在一起,就足够让不熟练的人写出一堆bug。这也是为什么这道题能进hot100——它就像链表章节的“入门关卡”,做透了,后面很多链表题你都会觉得眼熟。

2. 解题思路拆解:从“转成数字再加”到“竖式加法”

2.1 新手最容易踩的坑:为什么不能把链表转成整数再相加

我敢打赌,十个做这道题的人,至少有五个第一反应是同一个:先把两个链表分别遍历一遍,342转成一个int465转成一个int,加起来得到807,再把807逆序存成一个链表,搞定。

这个方案写起来确实很顺,也就十几行代码,而且提交之后还能通过前几个测试用例。问题出在哪?两个非常致命的地方。

第一是溢出。题目根本没有限制链表的长度,链表里面存的完全可能是一个几十位甚至上百位的超大整数。int最大就20多亿,long顶多几十位,一旦测试用例里塞了两个很长的链表,你辛辛苦苦转出来的这个整数,瞬间就溢出了,结果全错。LeetCode的测试用例是故意挖了这种坑的,你要是真用这种解法交上去,后面几个用例大概率会把你打醒。

第二是这个思路从根本上就跑偏了。这道题的考察目标是链表操作,不是字符串转数字。你用类型转换绕过了链表本身的运算逻辑,看起来是“简便解法”,实际上完全没有训练到任何算法能力。面试的时候,如果你给出这种解法,面试官大概率会追问一句:“如果链表有一万位怎么办?”——你总不能现场写一个大数运算类吧。

所以,乖乖用竖式加法模拟,才是这道题的正解。

2.2 竖式加法:从个位开始逐位相加的模拟逻辑

竖式加法大家都学过,列竖式的时候从个位加起,满十进一。这道题的模拟逻辑就是把竖式放进了链表里。

两个链表从头开始,同时往后走。每一轮取出两个链表当前节点的值,加上上一轮留下的进位carry,得到一个总和sum = x + y + carry。当前位要放进结果链表的值,就是sum % 10(取个位),新一轮要传给下一位的进位,就是sum / 10(取十位)。

我习惯用一个生活化的类比来理解这个过程:你把两个链表想象成两排排队的人,每个人手里举着一张数字牌。每次两个队伍各出一个人,把两人手里的数字加上上一位传过来的进位纸条,算出一个结果。结果的个位数写到结果链表的新节点上,十位数揣进口袋,等下一轮传给下两个人。谁那边队伍先走完了,就当作手里拿着0继续陪跑,直到两个队伍都走完,而且口袋里也没有任何进位了,整个流程才结束。

这个循环的终止条件,不是“两个链表都走到头”,而是“两个链表都走到头并且进位为0”——这个细节,后面会专门讲。

2.3 虚拟头节点:避免头节点特殊判断的关键技巧

链表题里有一个非常实用的技巧,叫“虚拟头节点”,英文叫dummy head。这道题里,如果不用dummy,代码会写得很别扭。

为什么?因为结果链表的头节点,在开始计算之前是未知的。你虽然知道第一个节点应该有值,但在循环里创建它的时机和处理后续节点是完全一样的。如果直接用一个ListNode cur = null去收拢结果链表,那第一轮循环执行cur.next = new ListNode(...)的时候,cur还是null,直接空指针异常。

于是很多人会这么处理:先判断一下cur是不是null,是就新建节点并让cur指向它,不是就cur.next = node。能跑,但代码里到处都是分支判断,看一会儿就晕了。

dummy的思路很简单:我先new一个值无关紧要的节点放在最前面,让cur从它开始。之后所有逻辑都统一成“cur.next = 新节点; cur = cur.next”,完全不需要关心是不是第一次创建节点。算完之后,整个结果链表的真正头节点,就是dummy.next

这个技巧在“合并两个有序链表”“分隔链表”“两两交换链表节点”这些题里全都用得上,属于链表题里一定要掌握的通用模板。这道题是一个很自然的练手场景。

3. 完整代码实现与逐行逻辑讲解

3.1 先看完整代码(Java版)

我用的主力语言是Java,所以先把Java版完整贴出来。思路吃透了,用别的语言写就是一个语法翻译的问题。

public ListNode addTwoNumbers(ListNode l1, ListNode l2) { ListNode dummy = new ListNode(0); ListNode cur = dummy; int carry = 0; while (l1 != null || l2 != null) { int x = (l1 != null) ? l1.val : 0; int y = (l2 != null) ? l2.val : 0; int sum = x + y + carry; carry = sum / 10; cur.next = new ListNode(sum % 10); cur = cur.next; if (l1 != null) { l1 = l1.next; } if (l2 != null) { l2 = l2.next; } } if (carry > 0) { cur.next = new ListNode(carry); } return dummy.next; }

这段代码很干净,核心逻辑就两个部分:循环体内按位相加并串链表,循环结束后处理残留进位。下面逐行拆开讲。

3.2 关键步骤的意图:为什么这么写

先从dummycur说起。dummy里存的0没有任何实际含义,它的存在只是为了让cur有一个真实对象可以指向。每轮循环里,cur.next = new ListNode(...)负责把新节点挂到结果链表的尾部,然后cur = cur.nextcur往后移动,指向刚创建好的节点。这个移动动作很重要,漏了就直接原地死循环或者覆盖前面的节点了。

再看循环条件。这里用的是l1 != null || l2 != null,写成了“或”而不是“且”。原因很直白:两个链表可能不一样长。如果用的是&&,短的链表一旦走到头,循环就停了,长链表后面的那些高位数字就全被漏掉,结果必然错误。用||,配合循环体里的三元表达式(l1 != null) ? l1.val : 0,哪个链表走到头了,就给它补一个0继续参与计算。这个“短链表补0”的思路,是本题最精髓的设计之一,也是代码能保持简洁的关键原因。

进位计算carry = sum / 10,因为sum最多是9 + 9 + 1 = 19,所以这个进位只可能是0或1。有的人在这里喜欢写carry = sum >= 10 ? 1 : 0,也行,但sum / 10更简洁。当前位结果用sum % 10,保证取的是个位数。

还有一点容易被忽略:循环内移动指针的时候,l1l2都要做判空处理。你不能写l1 = l1.next,因为如果l1已经是null了,再取.next就会报空指针异常。所以都要放在if里面,只有不为空才往后移。

3.3 循环结束后的进位检查:最后一位别丢了

这是整道题里最经典的“坑”之一。当while循环结束的时候,l1l2都已经走到头了,但carry可能不是0。比如5 + 5 = 10,循环体执行的过程是:sum = 0 + 0 + 1吗?不对,从头捋一下:5 + 5sum = 5 + 5 + 0 = 10carry = 1,结果链表第一个节点存的是0,然后指针移动,循环判断l1l2都已经为null,循环退出。此时carry还留着1呢,如果你不处理,结果就变成一个只有0的链表,正确答案应该是0 -> 1,即数字10。

所以循环结束后,一定要加上if (carry > 0) { cur.next = new ListNode(carry); }这一段,把最后一轮进位补成一个新的节点。这个操作,就是“最高位进位”的最后一步处理。

3.4 复杂度分析

时间复杂度是O(max(m, n)),其中m和n分别是两个链表的长度。因为循环遍历的次数,由较长的那个链表决定,短链表走完后就一直在补0陪跑。空间复杂度是O(1),这里指的是额外辅助空间只有dummycurcarry这些常数量级的变量。结果链表占用的空间是题目要求返回的,不计入额外空间复杂度。

有的同学会拿递归写法来对比,递归写起来确实也能AC,而且代码显得很“优雅”。但递归在处理长链表的时候有栈溢出的风险,并且可读性不一定更好。对这道题来说,我推荐就用迭代写法,逻辑最直白,也最好调试。

3.5 Python版顺手给一下

如果你主用Python,原理一模一样,就是语法风格变了。我也给一份参考,方便对照着看。

class Solution: def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]: dummy = ListNode(0) cur = dummy carry = 0 while l1 or l2: x = l1.val if l1 else 0 y = l2.val if l2 else 0 total = x + y + carry carry = total // 10 cur.next = ListNode(total % 10) cur = cur.next if l1: l1 = l1.next if l2: l2 = l2.next if carry > 0: cur.next = ListNode(carry) return dummy.next

不要觉得会了Java版就完了,Python版循环体内少了些判空括号,写起来更舒适。你在面试时可以先用Java把思路讲清楚,再用自己最熟的语言写代码,效果会更好。

4. 易错点与边界条件:90%的错误都出在这里

4.1 边界条件一:最高位进位被丢掉

前面已经讲过,当两个链表遍历完之后,carry里可能还留着一个1。这个进位表示最高位相加产生了新的一位,必须新建一个节点接到结果链表后面。这是这道题出错率最高的一个点,没有之一。

我见过不少人写代码时,循环里的逻辑全对,但就是忘了循环后面那句if (carry > 0),交上去直接挂掉比如[9, 9] + [1]这类用例。这里我特意给大家一个验证思路:算完手头用例后,要专门测一个“最高位有进位”的场景,比如9 + 9999 + 19999999999 + 1,确保最高位的1没有丢。

4.2 边界条件二:两个链表长度不一致

题目并没有保证两个链表一样长。[1, 8]表示81,[0]表示0,加出来是81,但光看链表头一个是1一个是0,不对齐的话很容易漏掉高位的8。

解决方案就是我前面代码里的写法:在循环体内部,通过三元表达式把已经走完的链表当作0处理。这样就不用先把短链表“补齐”到跟长链表一样长再去遍历,省了一次额外的遍历操作。

这段逻辑还有一个好处:它天然处理了“一个链表为空”的情况。虽然题目说了链表非空,但面试官很可能会追问“如果有一个是空链表呢?”你只需要回答“循环里它会一直按0来参与运算,另一个链表原样返回”,就直接过了这个追问。

4.3 边界条件三:指针忘了移动

链表题最常见的低级错误,就是循环体里创建完新节点,忘了把l1l2往后移动,导致死循环,或者结果链表永远只能读到第一个节点。

l1 = l1.next的时候一定要记得放在判空逻辑里。用while (l1 != null || l2 != null)作为循环条件时,如果不判空就移动,极容易在短链表走到头之后触发空指针异常。

这里有个自查技巧:每轮循环结束前,用脑子过一遍三个指针的状态——l1走了没有,l2走了没有,cur有没有成功指向新节点。三个都移动了,这一轮才算闭环。

4.4 一个可视化辅助建议

链表题不像数组题,光靠脑子想很容易漏。做这道题的时候,强烈建议在纸上画一下这几种情况的示意图:

  • [2, 4, 3]+[5, 6, 4](教科书用例,结果是[7, 0, 8]
  • [9, 9, 9, 9, 9, 9, 9]+[9, 9, 9, 9](长度不等且连续进位)
  • [0]+[0](结果为0,基本用例)

把这些用例跑一遍,你基本就能确定代码没大问题了。

5. 常见问题排查与本地测试技巧

5.1 常见报错和异常对照表

我做这道题的时候,以及后来帮别人看代码,总结出了一些高频报错和排查思路。用表格整理出来,方便大家对照自查:

现象可能原因排查方向
空指针异常(NullPointerException)循环内移动l1.nextl2.next时没有判空检查指针移动是否放在if (l1 != null)
输出结果少了最高位循环结束后没处理残留的carry确认有if (carry > 0) cur.next = new ListNode(carry)
输出结果非常长且包含重复节点cur在循环里忘了移动,导致新节点反复覆盖同一个cur.next检查cur = cur.next是否执行到了
长链表用例答案错误把链表转成了intlong计算导致溢出改用逐位加法模拟,不要转换类型
两个链表长度不同时不通过循环条件写成了&&而不是`
输出全0或者结果缺失sum % 10sum / 10理解反了重新理一遍进位和当前位的计算逻辑

5.2 手写几个专门的测试用例

除了跑LeetCode自带的用例,我强烈建议你本地或在线编辑器里多跑几组特殊用例,专门验证边界条件:

第一个是[9, 9, 9, 9, 9, 9, 9] + [9, 9, 9, 9]。这两个链表九个长度不同,从个位开始一路连续进位,最终结果应该是[8, 9, 9, 9, 0, 0, 0, 1]。这个用例能同时检验长度不同和最高位进位两个边界。

第二个是[0] + [7, 3]。一个是0,一个表示37,结果应该是[7, 3]。主要验证短链表补0的逻辑。

第三个是[5] + [5],结果应该是[0, 1]。这个用例专门练“最高位进位”,很多人在这一步翻车。

把这些用例跑通了,你的代码基本就能应对绝大多数测试了。

5.3 从这道题延伸:hot100链表题的通用套路

说句实在话,刷题这事最忌讳的就是“做一题忘一题”。而链表类的题尤其讲究模板复用。这道“两数相加”其实已经把链表题的核心模板暴露得很清楚了:

  • 虚拟头节点dummy,用来统一头节点处理;
  • while循环遍历链表,循环条件是“或”,并配合判空;
  • 每次循环移动两个指针,并且只在非空时移动;
  • 循环结束后检查有没有“尾巴”需要补上。

你用这个模板再去看“合并两个有序链表”“两两交换链表中的节点”“分隔链表”,会发现思路惊人的一致。所以这道题不只是hot100里的一道题,它其实是链表题族的“祖师爷”。把它的每一个细节吃透,比盲目刷二十道同类型题都管用。

6. 常见变体题与面试追问怎么接

面试官出了“两数相加”,几乎必然还会出变体题来试探你的理解深度。我遇到过的主要是下面两种,提前准备好,现场就不慌。

第一种是:如果链表是正序存储数字,怎么处理?比如3 -> 4 -> 2表示342,4 -> 6 -> 5表示465,加起来还是807。这时候最直接的思路是把两个链表先反转,变成逆序场景,再用这道题的解法,最后把结果再反转回来。如果你在面试中能主动说出“正序就反转成逆序再算”,面试官会立刻觉得你确实学透了。

第二种是:如果要求不用新增链表节点,在原链表上就地完成计算,怎么做?这种题一般会要求结果存在较长的那个链表里。思路还是一样的竖式加法,但是循环结束时,要记得把长链表剩余的高位节点直接复用,并处理最后可能新增的一个节点。

另外还有一种很常见的追问:如何处理大数溢出?你直接回答“这道题遍历过程中每一位只存0~9,进位最多是1,所以即使链表有一万位也不会溢出”,这句话本身就能体现你对题目本质的理解。

7. 一个建议:把这题放进你的“链表基础模板”

最后分享一点个人体会。LeetCode hot100是一个非常好的题库,因为它选出来的题每一道都有明确的训练价值。“两数相加”作为链表章节的开篇题,价值就在于它是链表模板的浓缩。

我在第一次做这道题的时候,因为没画图、没先想边界,直接上手写代码,结果反反复复提交了五版才通过。从那时起我养成一个习惯:凡是链表题,先花两分钟在纸上画一遍节点走向和循环终止条件,再动键盘。这个习惯帮我省下了大量调试时间。

如果你正在按hot100顺序刷题,建议把这道题不仅刷一遍,而是做三遍:第一遍照着题解理解思路,第二遍关掉题解自己写,第三遍隔两天再独立写一次,并把变体题也顺手过一遍。三遍下来,链表的基本功就真正长在脑子里了。

这道题本身不难,但它是很好的“试金石”——链表熟不熟,写一遍就知道。希望这篇文章能帮你把它从“拦路题”变成“送分题”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询