力扣解题-2. 两数相加

0 阅读7分钟

力扣解题-2. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

image.png

输入:l1 = [2,4,3], l2 = [5,6,4]

输出:[7,0,8]

解释:342 + 465 = 807.

示例 2:

输入:l1 = [0], l2 = [0]

输出:[0]

示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]

输出:[8,9,9,9,0,0,0,1]

提示:

每个链表中的节点数在范围 [1, 100] 内

0 <= Node.val <= 9

题目数据保证列表表示的数字不含前导零

Related Topics

递归、链表、数学


第一次解答

解题思路

核心方法:模拟手工加法(迭代版),通过哑节点(dummyHead)简化结果链表的头节点处理,逐位计算两个链表对应节点的和并处理进位,循环直到两个链表遍历完毕且无进位,时间复杂度O(max(n,m))、空间复杂度O(max(n,m)),是本题的经典最优解法。

核心逻辑拆解

链表逆序存储数字的特性,恰好匹配手工加法“从个位开始计算”的逻辑:

  1. 哑节点初始化:创建dummyHead(哑节点)作为结果链表的虚拟头节点,current指针指向哑节点,用于构建结果链表;
  2. 进位初始化carry表示加法进位(初始为0,因为个位相加无初始进位);
  3. 循环计算(核心)
    • 循环条件:l1 != null || l2 != null || carry != 0(只要有链表未遍历完,或仍有进位,就需要继续计算);
    • 取当前位数值:val1为l1当前节点值(l1为空则取0),val2为l2当前节点值(l2为空则取0);
    • 计算当前位总和:sum = val1 + val2 + carry
    • 更新进位:carry = sum / 10(总和≥10时进位为1,否则为0);
    • 计算当前位结果:digit = sum % 10(取余数作为当前位数字);
    • 构建结果节点:current.next指向新创建的digit节点,current移动到下一个节点;
    • 移动链表指针:l1、l2非空时分别向后移动;
  4. 返回结果:返回dummyHead.next(跳过哑节点,得到结果链表的真实头节点)。
具体步骤(以示例1 l1=[2,4,3]、l2=[5,6,4]为例)
步骤l1值l2值carrysumdigit结果链表(current指向)说明
125077dummyHead→7个位相加:2+5+0=7,无进位
24601007→0十位相加:4+6+0=10,进位1
3341880→8百位相加:3+4+1=8,无进位
4nullnull0---循环终止
最终结果链表:7→0→8,与示例一致。
性能说明
  • 时间复杂度:O(max(n,m))(n、m分别为两个链表的长度,最多遍历较长链表的长度+1次,处理最后一位进位);
  • 空间复杂度:O(max(n,m))(结果链表的长度最多为max(n,m)+1,如示例3的7位+4位=8位);
  • 优势:
    1. 哑节点避免了头节点的特殊处理(无需判断结果链表是否为空);
    2. 循环条件覆盖了“链表长度不一致”和“最后一位进位”的边界场景;
    3. 逐位计算符合数学加法逻辑,无冗余操作,执行效率极高。
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(0);
        ListNode current=dummyHead;
        int carry=0;
        while( l1!=null|| l2!=null || carry!=0){
            int val1=(l1!=null)?l1.val:0;
            int val2=(l2!=null)?l2.val:0;
            int sum=val1+val2+carry;
            carry=sum/10;
            int digit=sum%10;
            current.next=new ListNode(digit);
            current=current.next;
            if(l1!=null){
                l1=l1.next;
            }
            if(l2!=null) {
                l2 = l2.next;
            }
        }
         return dummyHead.next;
     }

示例解答

解题思路

解法1:递归版(逻辑等价,代码更简洁)

核心方法:递归模拟加法,将迭代的循环逻辑转化为递归调用,每一层递归处理一位数字的相加和进位传递,逻辑更简洁但递归深度受限于链表长度(最多101层,无栈溢出风险)。

核心逻辑

递归的核心是“处理当前位 + 递归处理剩余位 + 传递进位”:

  1. 递归终止条件:l1为空 && l2为空 && carry=0,返回null;
  2. 计算当前位数值:val1=l1.val(空则0),val2=l2.val(空则0);
  3. 计算sum和carry,创建当前位结果节点;
  4. 递归调用处理下一位(l1.next/l2.next,传递新的carry),作为当前节点的next;
  5. 返回当前节点。
代码实现
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    return add(l1, l2, 0);
}

// 辅助递归函数:l1、l2为当前处理节点,carry为进位
private ListNode add(ListNode l1, ListNode l2, int carry) {
    // 递归终止:无节点且无进位
    if (l1 == null && l2 == null && carry == 0) {
        return null;
    }
    // 取当前位值
    int val1 = l1 != null ? l1.val : 0;
    int val2 = l2 != null ? l2.val : 0;
    // 计算当前位和进位
    int sum = val1 + val2 + carry;
    int newCarry = sum / 10;
    int digit = sum % 10;
    // 创建当前节点,递归处理下一位
    ListNode node = new ListNode(digit);
    node.next = add(
        l1 != null ? l1.next : null,
        l2 != null ? l2.next : null,
        newCarry
    );
    return node;
}
优势与适用场景
  • 时间复杂度:O(max(n,m)),与迭代版一致;
  • 空间复杂度:O(max(n,m))(递归调用栈的深度=结果链表长度);
  • 优势:代码更简洁,符合“分治”的编程思维,无需手动管理指针移动;
  • 注意事项:链表长度≤100,递归深度≤101,不会触发栈溢出(Java默认栈深度约1000)。
解法2:空间优化版(复用原链表节点,进阶思路)

核心方法:复用较长的原链表节点存储结果,避免创建新节点,减少内存分配开销,空间复杂度仍为O(max(n,m))(最坏情况仍需创建最后一位进位节点),但实际内存占用更低。

代码实现
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;
    int carry = 0;
    ListNode p1 = l1, p2 = l2;
    
    // 先遍历两个链表都有的部分,复用节点
    while (p1 != null && p2 != null) {
        int sum = p1.val + p2.val + carry;
        carry = sum / 10;
        p1.val = sum % 10; // 复用l1的节点存储结果
        curr.next = p1;
        curr = curr.next;
        p1 = p1.next;
        p2 = p2.next;
    }
    
    // 处理l1剩余节点
    while (p1 != null) {
        int sum = p1.val + carry;
        carry = sum / 10;
        p1.val = sum % 10;
        curr.next = p1;
        curr = curr.next;
        p1 = p1.next;
    }
    
    // 处理l2剩余节点
    while (p2 != null) {
        int sum = p2.val + carry;
        carry = sum / 10;
        p2.val = sum % 10;
        curr.next = p2;
        curr = curr.next;
        p2 = p2.next;
    }
    
    // 处理最后的进位
    if (carry > 0) {
        curr.next = new ListNode(carry);
    }
    
    return dummy.next;
}
优势说明
  • 减少新节点创建:复用原链表的节点,仅在最后有进位时创建新节点;
  • 性能优化:减少对象创建和内存分配的开销,执行效率略高于迭代版;
  • 局限性:修改了原链表的节点值,若需要保留原链表数据则不适用。

总结

  1. 迭代版(第一次解答):逻辑清晰、无副作用(不修改原链表),O(max(n,m))时间+O(max(n,m))空间,是工程首选的经典解法;
  2. 递归版:代码简洁、符合分治思维,性能与迭代版一致,适合偏好递归风格的场景;
  3. 空间优化版:复用原链表节点,减少内存分配,适合对内存占用要求较高的场景(需注意原链表数据被修改);
  4. 关键技巧:
    • 核心思想:模拟手工加法,逐位计算+进位传递,利用链表逆序存储的特性匹配加法顺序;
    • 边界处理:哑节点简化头节点处理,循环条件覆盖“链表长度不一致”和“最后一位进位”;
    • 性能优化:优先选择迭代版,避免递归的栈开销;需复用内存时选择空间优化版。