LeetCode 2 两数相加:链表模拟加法,Java 图解进位过程

在链表题中,「两数相加」是一道非常经典的入门题。它同时考查了链表遍历、进位处理、哑节点以及边界条件,也是 Java 算法面试中经常出现的基础题。

这道题的代码不长,真正需要想清楚的是三个问题:为什么数字要逆序存储?两条链表长度不一致怎么办?遍历结束后为什么还要检查进位?

一、题目描述

给定两个非空链表 l1l2,分别表示两个非负整数。每个节点只存储一位数字,并且数字按照逆序排列。

例如:

复制代码
l1:2 → 4 → 3,表示 342
l2:5 → 6 → 4,表示 465

两数相加:

复制代码
342 + 465 = 807

因此返回:

复制代码
7 → 0 → 8

题目还保证:除了数字 0 本身之外,输入的数字不会以 0 开头。

二、为什么逆序存储更方便?

我们平时做加法时,总是从个位开始计算:

复制代码
  342
+ 465
-----
  807

先计算个位 2 + 5,再计算十位 4 + 6,最后计算百位 3 + 4。如果某一位的和大于等于 10,还要把进位交给下一位。

题目中的链表头节点恰好就是个位,所以只需要从头到尾同时遍历两条链表,就能按照正常的加法顺序完成计算,不需要反转链表。

三、核心思路:逐位相加并保存进位

用两个指针分别遍历 l1l2,同时维护变量 carry 表示进位。

每轮循环执行以下操作:

  1. 先把进位加入当前总和;

  2. 如果 l1 还有节点,加上 l1 当前节点的值;

  3. 如果 l2 还有节点,加上 l2 当前节点的值;

  4. sum % 10 是当前位应保存的数字;

  5. sum / 10 是传给下一位的进位;

  6. 创建新节点并接到结果链表末尾。

342 + 465 为例:

计算位置 当前计算 当前位 sum % 10 进位 sum / 10
个位 2 + 5 + 0 = 7 7 0
十位 4 + 6 + 0 = 10 0 1
百位 3 + 4 + 1 = 8 8 0

最终得到 7 → 0 → 8

四、为什么循环条件要包含 carry?

循环条件是这道题最容易遗漏的地方:

复制代码
while (p1 != null || p2 != null || carry != 0)

前两个条件用于处理两条链表长度不一致的情况,第三个条件用于处理最后仍有进位的情况。

例如:

复制代码
l1:9 → 9,表示 99
l2:1,表示 1

计算结果为 100,对应链表:

复制代码
0 → 0 → 1

当两条输入链表都遍历完时,carry 仍然等于 1。如果循环条件中没有 carry != 0,最后的节点 1 就会丢失。

五、完整 Java 代码

复制代码
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;
        int carry = 0;

        while (l1 != null || l2 != null || carry != 0) {
            int sum = carry;

            if (l1 != null) {
                sum += l1.val;
                l1 = l1.next;
            }

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

            int digit = sum % 10;
            carry = sum / 10;

            current.next = new ListNode(digit);
            current = current.next;
        }

        return dummy.next;
    }
}

六、为什么要使用哑节点?

代码中的 dummy 是一个不保存有效结果的哑节点:

复制代码
ListNode dummy = new ListNode(0);
ListNode current = dummy;

如果不使用哑节点,创建第一个结果节点时需要单独判断链表头是否为空。使用哑节点后,每次都可以统一执行:

复制代码
current.next = new ListNode(digit);
current = current.next;

最后返回 dummy.next,跳过哑节点即可。哑节点并不是这道题特有的技巧,在链表合并、删除节点、分区等问题中也非常常见。

七、截图代码中的一个细节

截图代码开头写了:

复制代码
if (l1 == null && l2 == null) {
    return null;
}

根据题目描述,l1l2 都是非空链表,因此这个判断不是必需的。保留它不会影响运行,但按题目约束可以直接省略,让核心逻辑更简洁。

另外,变量名 dumptemp 可以分别改成 dummycurrentdigit。算法不变,但语义更加清楚,也更符合面试和工程代码的可读性要求。

八、常见错误

1. 忘记处理最后的进位

只写下面的条件:

复制代码
while (l1 != null || l2 != null)

会让 99 + 1 丢失最后的 1。正确条件应包含 carry != 0

2. 默认两条链表长度相同

两条链表可能分别表示 1234567,因此访问节点值之前必须先判断节点是否为空。

3. 混淆当前位与进位

复制代码
int digit = sum % 10; // 当前节点保存的数字
carry = sum / 10;     // 下一位使用的进位

在十进制加法中,二者不要写反。

4. 返回 dummy 而不是 dummy.next

dummy 只是辅助节点,不属于真正结果。返回值必须是:

复制代码
return dummy.next;

九、复杂度分析

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

  • 时间复杂度:O(max(m, n)),两条链表最多各遍历一次;

  • 空间复杂度:O(max(m, n)),用于保存结果链表。

如果不计算返回结果本身占用的空间,额外空间复杂度是 O(1)

相关推荐
Hi李耶1 小时前
【LeetCode】557.反转字符串中的单词 III
算法·leetcode·职场和发展
2401_858286112 小时前
OS82.【Linux】设计线程池
java·linux·运维·服务器·开发语言·算法·线程池
数据知道2 小时前
IDOR 不安全的直接对象引用:API 越权实战检测
java·开发语言·网络·安全·网络安全
堕落年代2 小时前
Ollama CPU 推理大提示词优化实测报告(细致化数据版)
java·后端·spring
KobeSacre2 小时前
ForkJoinPool 源码
java
wear工程师3 小时前
Kafka 消费者心跳正常,为什么还会被踢出组?拆清 max.poll.interval.ms
java·kafka
SemiTris3 小时前
为什么 C/C++ 不走 Java 式的虚拟机跨平台路线?
java·程序员
weixin_440784113 小时前
【IntentSeivice实现原理】
android·java·开发语言·intentservice
IanSkunk3 小时前
企业AI Agent生产化落地:从技术架构到实施服务的全链路分析
java·人工智能·架构