每日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直接指向结果头节点
相关推荐
今天背单词了吗9801 小时前
算法学习笔记:19.牛顿迭代法——从原理到实战,涵盖 LeetCode 与考研 408 例题
笔记·学习·算法·牛顿迭代法
晓13132 小时前
JavaScript加强篇——第四章 日期对象与DOM节点(基础)
开发语言·前端·javascript
jdlxx_dongfangxing2 小时前
进制转换算法详解及应用
算法
烛阴3 小时前
JavaScript函数参数完全指南:从基础到高级技巧,一网打尽!
前端·javascript
why技术3 小时前
也是出息了,业务代码里面也用上算法了。
java·后端·算法
2501_922895583 小时前
字符函数和字符串函数(下)- 暴力匹配算法
算法
chao_7894 小时前
frame 与新窗口切换操作【selenium 】
前端·javascript·css·selenium·测试工具·自动化·html
天蓝色的鱼鱼4 小时前
从零实现浏览器摄像头控制与视频录制:基于原生 JavaScript 的完整指南
前端·javascript
IT信息技术学习圈4 小时前
算法核心知识复习:排序算法对比 + 递归与递推深度解析(根据GESP四级题目总结)
算法·排序算法
愚润求学4 小时前
【动态规划】01背包问题
c++·算法·leetcode·动态规划