每日LeetCode : 两数相加--链表操作与进位的经典处理

题目描述

给你两个非空链表,表示两个非负整数。链表中每个节点存储一位数字,且数字以逆序 方式存储(例如:2-->4-->3 表示整数342)。现在,请将两个数相加,并以相同形式返回表示和的链表。

示例:

ini 复制代码
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807,返回链表 7-->0-->8

解法1:基础迭代法(模拟竖式计算)

核心思想:模拟人工竖式计算的过程,逐位相加并处理进位。

javascript 复制代码
function addTwoNumbers(l1, l2) {
    const dummy = new ListNode(0); // 哑节点简化操作
    let curr = dummy;
    let carry = 0; // 进位值
    
    while (l1 || l2 || carry) {
        // 获取当前位的值(节点不存在则为0)
        const val1 = l1 ? l1.val : 0;
        const val2 = l2 ? l2.val : 0;
        
        // 计算当前位的和(包括进位)
        const sum = val1 + val2 + carry;
        carry = Math.floor(sum / 10); // 更新进位
        curr.next = new ListNode(sum % 10); // 创建新节点
        
        // 移动指针
        curr = curr.next;
        if (l1) l1 = l1.next;
        if (l2) l2 = l2.next;
    }
    
    return dummy.next; // 返回真正的头节点
}

解法说明

  1. 初始化 :创建哑节点dummy简化链表操作,然后用carry记录进位值(初始为0),curr指向当前结果链表的末尾。

  2. 循环处理 :同时遍历l1l2,直到两个链表都结束且无进位后,再计算当前位的和(节点值+进位),在更新进位值carry = sum / 10(取整)后,创建新节点存储sum % 10的结果。

  3. 指针移动 :将curr移动到新创建的节点,l1l2也移动到各自的下一个节点。

  4. 返回结果 :循环结束返回dummy.next(跳过哑节点)

时间复杂度 :O(max(m,n))
空间复杂度:O(max(m,n))

解法2:递归解法(函数式思维)

核心思想:将加法过程分解为递归步骤,每层递归处理一位数字。

javascript 复制代码
function addTwoNumbers(l1, l2, carry = 0) {
    // 递归终止条件:无节点且无进位
    if (!l1 && !l2 && carry === 0) return null;
    
    // 计算当前位的和
    const val1 = l1 ? l1.val : 0;
    const val2 = l2 ? l2.val : 0;
    const sum = val1 + val2 + carry;
    
    // 创建当前节点
    const node = new ListNode(sum % 10);
    
    // 递归处理下一位
    node.next = addTwoNumbers(
        l1 ? l1.next : null,
        l2 ? l2.next : null,
        Math.floor(sum / 10)
    );
    
    return node;
}

解法说明

  1. 递归终止条件:先判断两个链表为空,且无进位的情况下,返回null。
  2. 当前位计算 :获取当前节点值,若存在则将节点的值赋值给val1/val2,否则则为0,随后进行计算(值1+值2+进位)。
  3. 创建节点 :创建一个节点用于存储sum % 10的结果
  4. 递归连接 :通过递归处理下一位,并将结果连接到node.next
  5. 返回节点:返回当前创建的节点

时间复杂度 :O(max(m,n))
空间复杂度:O(max(m,n))

解法3:原地修改法(空间优化)

核心思想:复用较长的链表节点,减少新节点的创建。

javascript 复制代码
function addTwoNumbers(l1, l2) {
    let p1 = l1, p2 = l2;
    let carry = 0;
    let lastNode = null;
    
    while (p1 || p2 || carry) {
        // 获取当前值
        const val1 = p1 ? p1.val : 0;
        const val2 = p2 ? p2.val : 0;
        const sum = val1 + val2 + carry;
        
        // 复用p1或p2的节点
        if (p1) {
            p1.val = sum % 10;
            lastNode = p1;
            p1 = p1.next;
        } else if (p2) {
            p2.val = sum % 10;
            lastNode = p2;
            p2 = p2.next;
        } else {
            // 处理最后进位
            lastNode.next = new ListNode(carry);
            carry = 0; // 进位已处理
            break;
        }
        
        carry = Math.floor(sum / 10);
    }
    
    return l1; // 或l2,取决于哪个更长
}

优点

  1. 复用已有节点,减少内存分配。
  2. 处理最后进位时创建新节点。
  3. 优先复用l1的节点,当l1结束时使用l2

总结

1. 方法对比:

解法 优势 劣势 适用场景
迭代法 逻辑清晰,效率稳定 需要额外空间 通用场景,面试首选
递归法 代码简洁,数学思维 栈空间开销 函数式编程,短链表
原地修改法 空间效率高 修改输入,逻辑复杂 内存敏感,允许修改输入

2. 逆序存储的优势

链表的逆序存储(个位在头部)带来天然对齐的优势:

  • 不需要考虑数字位数对齐问题
  • 可以直接从头部开始逐位相加
  • 进位自然向链表尾部传播

3. 哑节点技巧

为什么使用哑节点?

graph LR A[dummy] --> B[节点1] B --> C[节点2] C --> D[...]
  • 统一操作:避免对头节点的特殊处理
  • 简化逻辑 :始终有curr.next = newNode
  • 安全返回dummy.next直接指向结果头节点
相关推荐
程序员Xu32 分钟前
【LeetCode热题100道笔记】二叉树的右视图
笔记·算法·leetcode
笑脸惹桃花1 小时前
50系显卡训练深度学习YOLO等算法报错的解决方法
深度学习·算法·yolo·torch·cuda
阿维的博客日记1 小时前
LeetCode 48 - 旋转图像算法详解(全网最优雅的Java算法
算法·leetcode
gnip2 小时前
Jst执行上下文栈和变量对象
前端·javascript
GEO_YScsn2 小时前
Rust 的生命周期与借用检查:安全性深度保障的基石
网络·算法
程序员Xu2 小时前
【LeetCode热题100道笔记】二叉搜索树中第 K 小的元素
笔记·算法·leetcode
拉不动的猪2 小时前
简单回顾下Weakmap在vue中为何不能去作为循环数据源,以及替代方案
前端·javascript·vue.js
How_doyou_do2 小时前
数据传输优化-异步不阻塞处理增强首屏体验
开发语言·前端·javascript
DT——3 小时前
前端登录鉴权详解
前端·javascript
THMAIL3 小时前
机器学习从入门到精通 - 数据预处理实战秘籍:清洗、转换与特征工程入门
人工智能·python·算法·机器学习·数据挖掘·逻辑回归