两数相加链表题深度拆解

题目回顾

LeetCode 2 两数相加:逆序存储数字的两条单链表,逐位相加,进位向后传递,返回新链表。

一、先逐行解析这段 Java 代码的链表操作逻辑

java

复制代码
// 虚拟头指针head(最终返回结果),res工作游标,不断向后追加节点
ListNode head=null,res=null;
int add=0; // 进位
while(l1!=null||l2!=null){
    // 空链表位补0
    int n1 = l1!=null?l1.val:0; 
    int n2 = l2!=null?l2.val:0;
    int sum=n1+n2+add;
    // 第一次创建头节点
    if(head==null)
        head=res=new ListNode(sum%10);
    else {
        // 当前游标res的next挂新节点,游标后移
        res.next=new ListNode(sum %10);
        res=res.next;
    }
    add=sum/10; // 更新进位
    // 两条输入链表游标后移
    if(l1!=null) l1=l1.next;
    if(l2!=null) l2=l2.next;
}
// 最高位进位单独补节点
if(add>0){
    res.next=new ListNode(add);
}
return head;

核心链表操作点

  1. head结果链表头引用,全程固定指向第一个节点,用于最后返回整条链表;
  2. res工作游标引用,始终指向当前链表最后一个节点,专门用来追加新节点;
  3. res.next = new ListNode():在尾部挂新节点;res = res.next:游标后移;
  4. 所有ListNode对象全部存堆内存 ,局部变量head/res/l1/l2只是栈上的引用地址,类似 C 的指针。

二、Java 链表 vs C 语言链表

1. 存储结构完全一致

C 结构体链表:

复制代码
struct ListNode {
    int val;
    struct ListNode *next;
};

Java 类链表:

java

复制代码
public class ListNode {
    int val;
    ListNode next;
}

结构一一对应:

  • val:数据域,普通整型;
  • next:指针 / 引用域,存下一个节点的内存地址; 逻辑操作完全互通:遍历、尾插、头插、删除、游标移动思路一模一样。

2. 游标遍历逻辑完全相同

C 写法等价逻辑:

c

复制代码
struct ListNode *head = NULL, *res = NULL;
int add = 0;
while(l1 || l2){
    int n1 = l1 ? l1->val : 0;
    int n2 = l2 ? l2->val : 0;
    int sum = n1 + n2 + add;
    struct ListNode *newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
    newNode->val = sum % 10;
    newNode->next = NULL;
    if(head == NULL){
        head = res = newNode;
    }else{
        res->next = newNode;
        res = res->next;
    }
    add = sum / 10;
    if(l1) l1 = l1->next;
    if(l2) l2 = l2->next;
}
if(add > 0){
    struct ListNode *newNode = malloc(...);
    newNode->val = add;
    res->next = newNode;
}
return head;

遍历、尾插、双游标、进位处理思路完全一样,算法逻辑无差别。

3. 空判断逻辑一致

C:l1 == NULL Java:l1 == null 语义完全等价:当前游标没有指向任何节点。

三、Java 链表让人觉得 "奇怪" 的核心差异

差异 1:C 是裸指针,Java 是安全引用,无手动内存管理

C 语言痛点
  1. 节点必须手动malloc堆分配;
  2. 使用完必须free释放,否则内存泄漏;
  3. 野指针风险:指针指向已释放内存,程序直接崩溃;
  4. 可以指针强转、任意地址读写,不安全。
Java 设计
  1. new ListNode() 自动在堆创建对象,GC 自动回收,不用手动释放;
  2. 不存在野指针:引用只能指向合法对象或null,访问null.val只会抛NullPointerException,不会程序崩溃;
  3. 禁止直接操作内存地址,屏蔽指针运算,只能.访问成员。

你代码里 new ListNode(sum%10) 等价 C 的malloc,但不用关心释放,这是最直观的区别。

差异 2:变量本质不同:C 指针变量 / Java 引用变量

C 语言

c

复制代码
struct ListNode *res; // 栈上存内存地址数值
res->next = newNode; // 解引用,修改堆节点内部next字段
res = res->next;     // 修改栈内指针变量存的地址值
Java

java

复制代码
ListNode res; // 栈上存对象引用(逻辑地址,不暴露真实内存)
res.next = new ListNode(); // 通过引用找到堆对象,修改对象内部next属性
res = res.next; // 把栈中res引用指向新的堆对象

行为完全一致,但底层实现不同: C 直接操作物理内存地址;Java 引用是 JVM 封装后的句柄,屏蔽底层内存。

差异 3:C 结构体是值 / 指针两套逻辑,Java 类永远引用传递

  1. C 如果直接写struct ListNode a; 是栈上实体对象,赋值是值拷贝; 链表必须全程用struct ListNode*指针,否则链表断裂;
  2. Java 中ListNode是引用类型,不存在栈上实体节点,所有节点一律在堆,所有变量都是引用,不存在值拷贝混淆问题。

举例容易踩坑的对比:

c

复制代码
// C 错误写法,值拷贝,链表断
struct ListNode res = *head; 
res.next = newNode;

// Java 不存在这种写法,ListNode变量只能存引用,不会拷贝整个节点
ListNode res = head; // 仅拷贝引用地址,共享同一个堆节点

这是很多 C 转 Java 写链表最容易疑惑的点:Java 永远不会拷贝整个节点对象,只有引用传递。

差异 4:构造器封装节点创建,C 靠 malloc 手动赋值

C 创建节点需要三步:malloc、赋值 val、置空 next; Java ListNode 提供三个构造函数,一行new ListNode(val, next)直接构建完整节点,封装简化代码。

java

复制代码
// 自带构造,不用手动赋值next
new ListNode(sum % 10, null);

差异 5:内存回收机制不同

  • C:忘记 free → 内存泄漏;重复 free → 程序崩溃;
  • Java:无手动释放,对象无任何引用指向时,GC 自动标记清除; 本题执行完毕后head方法结束失效,整条链表自动被回收。

四、这段代码的 Java 链表细节深挖

1. head 和 res 两个引用,为什么能维持整条链表?

java

复制代码
if(head==null)
    head=res=new ListNode(sum%10);

赋值逻辑: new 创建堆节点 A; res = Ahead = A,两个栈引用同时指向同一个堆节点 A ; 后续循环: res.next = 新节点B → 节点 A 的 next 字段存 B 的引用; res = res.next → res 引用改为指向 B,head 依旧固定指向 A(链表头部)。

核心:head 永远保留链表起点,res 只做移动游标,二者互不干扰 。 C 语言中两个ListNode*指针也是完全相同逻辑。

2. 为什么不用虚拟头节点(dummy)?

标准优化写法会创建哑节点 ListNode dummy = new ListNode(0); ListNode cur = dummy;,最后返回dummy.next,消除if(head==null)判断。 你的代码不用 dummy,通过head==null区分首次创建节点,逻辑更省一个节点,但多一层判断,两种写法都是工业标准。

3. l1 = l1.next 到底做了什么?

l1 是输入链表的游标引用,l1 = l1.next 只是修改栈局部变量 l1 的指向 ,不会修改原链表任何堆内节点数据。 Java 引用只是 "地址别名",重新赋值引用不会改动原有对象,和 C 指针l1 = l1->next行为一致。

4. 循环结束后单独处理进位 add>0

链表遍历完两条输入,但最高位还有进位,需要尾部追加新节点; 此时res正好停在链表最后一个节点,res.next = new ListNode(add)直接尾插,利用游标特性,和 C 逻辑无区别。

五、Java 链表容易产生 "奇怪感" 的总结

  1. 习惯 C 手动 malloc/free,Java 自动 GC,看不到内存操作,感觉 "黑盒";
  2. C 区分结构体值和指针,Java 只有引用类型,无值拷贝混淆;
  3. C 能打印指针地址、指针运算,Java 屏蔽底层内存,只能用.访问属性;
  4. C 空指针直接崩溃,Java 空引用抛 NPE,异常可控;
  5. Java 构造函数封装节点创建,C 需要手动分配 + 赋值,步骤繁琐。

六、通用链表核心思想

  1. 单链表本质:节点 = 数据 + 下一个节点地址;
  2. 双游标思想:一个头指针固定起点,一个游标遍历 / 尾插;
  3. 遍历终止条件:游标 == null(C NULL);
  4. 尾插标准流程:cur.next = 新节点; cur = cur.next;
  5. 所有链表算法(反转、合并、快慢指针、两数相加)算法逻辑不分语言,仅内存操作语法不同。

七、补充优化版

java

复制代码
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        int carry = 0;
        while(l1 != null || l2 != null || carry > 0){
            int n1 = l1 == null ? 0 : l1.val;
            int n2 = l2 == null ? 0 : l2.val;
            int sum = n1 + n2 + carry;
            cur.next = new ListNode(sum % 10);
            cur = cur.next;
            carry = sum / 10;
            if(l1 != null) l1 = l1.next;
            if(l2 != null) l2 = l2.next;
        }
        return dummy.next;
    }
}

哑节点统一所有插入逻辑,不用判断头节点是否为空,是 Java 链表刷题最常用写法。