2. 两数相加:链表模拟与进位处理详解

题目链接:2. 两数相加

题解类型:

链表模拟 / 逐位相加 / 进位处理

这题的核心不难:像手算加法一样,从个位开始逐位相加。

真正容易出错的地方是边界处理:

两条链表长度不同怎么办?
最后还有进位怎么办?
当前位和下一轮进位分别怎么计算?
结果链表的第一个节点怎么连接?

算法思路

题目使用倒序链表存储一个非负整数。

例如:

2 -> 4 -> 3

表示的数字是:

342

因为链表头部存的是个位:

个位 2 -> 十位 4 -> 百位 3

所以:

l1:2 -> 4 -> 3,表示 342
l2:5 -> 6 -> 4,表示 465

342 + 465 = 807

结果也要倒序存储:

7 -> 0 -> 8,表示 807

倒序存储让链表头部正好是个位,因此可以直接从 head 开始模拟手算加法。


1. 每一位到底算什么

假设当前轮:

l1 当前位:x
l2 当前位:y
上一位进位:carry

当前轮的总和是:

sum = x + y + carry;

其中:

sum % 10:当前结果节点保存的个位。
sum / 10:交给下一轮的进位。

例如:

9 + 8 + 0 = 17

因此:

当前结果位:17 % 10 = 7
下一轮进位:17 / 10 = 1

当前结果链表接入 7,并把 1 留给下一轮。

在本题中每一位最大是:

9 + 9 + 1 = 19

所以 carry 只可能是 01


2. 完整推演:342 + 465

l1:2 -> 4 -> 3
l2:5 -> 6 -> 4

初始:

carry = 0

第 1 轮:个位

2 + 5 + 0 = 7
结果位:7 % 10 = 7
进位:  7 / 10 = 0

结果链表:

7

第 2 轮:十位

4 + 6 + 0 = 10
结果位:10 % 10 = 0
进位:  10 / 10 = 1

结果链表:

7 -> 0

第 3 轮:百位

3 + 4 + 1 = 8
结果位:8 % 10 = 8
进位:  8 / 10 = 0

结果链表:

7 -> 0 -> 8

两条链表都走完,且 carry == 0,结束。


3. 易错点一:两条链表长度不同,空位按 0 处理

例如:

l1:2 -> 4 -> 9,表示 942
l2:5 -> 6,表示 65

百位时,l2 已经没有节点,但不是停止计算,而是应该把缺失的一位当作 0

9 + 0 + carry

所以每轮取值时写成:

int x = (l1 == null) ? 0 : l1.val;
int y = (l2 == null) ? 0 : l2.val;

含义是:

对应链表还有节点:取当前节点值。
对应链表已结束:当前位补 0。

不能因为其中一条链表结束,就直接停止循环;另一条链表的高位仍然需要放进结果。


4. 易错点二:两个链表结束后,进位可能还存在

例如:

l1:9 -> 9,表示 99
l2:1,表示 1

99 + 1 = 100

推演:

第 1 轮:9 + 1 + 0 = 10 -> 当前位 0,carry = 1
第 2 轮:9 + 0 + 1 = 10 -> 当前位 0,carry = 1

此时:

l1 == null
l2 == null
carry == 1

最高位的 1 仍必须创建为结果节点:

0 -> 0 -> 1

因此循环条件必须写为:

while (l1 != null || l2 != null || carry != 0)

三个条件的含义是:

l1 != null:l1 还有位没有处理。
l2 != null:l2 还有位没有处理。
carry != 0:还有最高位进位没有写入结果。

只要其中任何一个成立,就必须继续。


5. 易错点三:% 10/ 10 不能写反

假设:

sum = 17

正确写法:

int digit = sum % 10; // 7,当前节点保存的个位
carry = sum / 10;     // 1,下一轮使用的进位

原因是十进制数 17 可以拆成:

17 = 1 * 10 + 7

其中:

7 留在当前位。
1 移交给更高一位。

因此不能写成:

tail.next = new ListNode(sum / 10);
carry = sum % 10;

那样会把进位与当前位完全弄反。


6. 为什么需要 dummytail

每一轮相加都会产生一个新的结果节点,因此需要不断向结果链表末尾追加节点。

先创建:

ListNode dummy = new ListNode(-1);
ListNode tail = dummy;

初始结构:

dummy -> null
  ^
tail

每算出一位:

tail.next = new ListNode(sum % 10);
tail = tail.next;

tail 始终指向结果链表当前最后一个节点。

dummy 是虚拟头节点,不属于答案;真正的结果从:

dummy.next

开始。

使用 dummy 的价值是:第一个结果节点和之后的结果节点可以使用同一套连接代码,不需要专门处理“结果链表还没有头节点”的情况。


7. Java 代码完整注释

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        // dummy 是结果链表的虚拟头节点。
        // 它只用于统一连接操作,不属于最终结果。
        ListNode dummy = new ListNode(-1);

        // tail 始终指向结果链表当前最后一个节点。
        ListNode tail = dummy;

        // carry 保存上一位相加产生、需要加到当前位的进位。
        int carry = 0;

        // 只要任一链表还有节点,或还有最终进位,就继续计算。
        while (l1 != null || l2 != null || carry != 0) {
            // l1 已走完时,当前位按 0 处理。
            int x = (l1 == null) ? 0 : l1.val;

            // l2 已走完时,当前位按 0 处理。
            int y = (l2 == null) ? 0 : l2.val;

            // 当前位总和 = 两个当前位 + 低一位传来的进位。
            int sum = x + y + carry;

            // 当前结果节点保存个位。
            tail.next = new ListNode(sum % 10);

            // tail 移动到刚创建的结果节点。
            tail = tail.next;

            // 十位成为下一轮的进位。
            carry = sum / 10;

            // 还存在节点的链表向后移动一位。
            if (l1 != null) {
                l1 = l1.next;
            }

            if (l2 != null) {
                l2 = l2.next;
            }
        }

        // dummy 是辅助节点,真正的结果头节点是 dummy.next。
        return dummy.next;
    }
}

8. 易错点清单

易错点错误后果正确处理
一条链表结束就停止另一条链表的高位丢失空位按 0 处理
循环不判断 carry最终最高位进位丢失`while (l1 != null
sum % 10sum / 10 写反当前结果位和进位错误% 10 写当前位,/ 10carry
未判断 l1l2 是否为 null 就访问 .val空指针异常三元表达式补 0
返回 dummy答案多出虚拟节点 -1返回 dummy.next
不移动 tail后续节点不能正确追加每轮创建节点后 tail = tail.next

写完代码后,至少用下面三类情况手动检查一次:

普通情况:342 + 465 = 807
长度不同:942 + 65 = 1007
最终进位:99 + 1 = 100

对应链表结果应分别是:

7 -> 0 -> 8
7 -> 0 -> 0 -> 1
0 -> 0 -> 1

9. 复杂度

假设两条链表长度分别为 mn

时间复杂度:

O(max(m, n))

每一位只处理一次,最多额外处理一次最终进位。

额外空间复杂度:

O(1)

这里不把题目要求返回的结果链表算入额外空间。除结果链表外,只使用了 dummytailcarry 等常数个变量。

一句话记忆:

当前位 = x + y + carry;结果节点存 sum % 10;下一轮进位是 sum / 10;空位补 0,最终进位也不能漏。

更多推荐