Leetcode 112 两数相加 II

1 题目

445. 两数相加 II

给你两个 非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。

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

示例1:

复制代码
输入:l1 = [7,2,4,3], l2 = [5,6,4]
输出:[7,8,0,7]

示例2:

复制代码
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[8,0,7]

示例3:

复制代码
输入:l1 = [0], l2 = [0]
输出:[0]

提示:

  • 链表的长度范围为 [1, 100]
  • 0 <= node.val <= 9
  • 输入数据保证链表代表的数字无前导 0

2 代码实现

c++

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        stack<int> s1 , s2 ;

        while (l1 != nullptr){
            s1.push (l1 -> val );
            l1 = l1 -> next ;
        }
        while (l2 != nullptr){
            s2.push (l2 -> val);
            l2 = l2 -> next ;
        }
        int carry = 0 ;
        ListNode* head = nullptr ;

        while (!s1.empty() || !s2.empty() || carry != 0 ){
            int num1 = 0 , num2 = 0 ;
            if (!s1.empty()){
                num1 = s1.top ();
                s1.pop();
            }
            if(!s2.empty()){
                num2 = s2.top();
                s2.pop();
            }

            int sum = num1 + num2 + carry ;
            carry = sum / 10 ;
            int curVal = sum % 10 ;

            ListNode* newNode = new ListNode(curVal);
            newNode -> next = head ;
            head =newNode ;
        }
        return head ;
    }
};

js

javascript 复制代码
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    const s1 =[] ,s2 = [];
    while(l1){
        s1.push(l1.val);
        l1 = l1.next ;
    }
    while (l2){
        s2.push(l2.val);
        l2 = l2.next ;
    }

    let carry = 0 ;
    let head = null ;
    while (s1.length > 0 || s2.length > 0 || carry > 0){
        const num1 = s1.length > 0 ? s1.pop() : 0 ;
        const num2 = s2.length > 0 ? s2.pop() : 0 ;

        const sum = num1 + num2 + carry ;
        carry = Math.floor(sum / 10 );
        const curVal = sum % 10 ;

        const newNode = new ListNode (curVal);
        newNode.next = head ;
        head = newNode ;
    }
    return head ;
};

思考

我觉得有点难啊,怎么搞的,不清楚啊,和上次做的题目的唯一区别就头结点是低位 / 高位 ;

怎么搞的。

Leetcode 111 两数相加-CSDN博客 不知道怎么写了。

题解

你觉得这道题难,核心原因是数字最高位在链表头 ,无法像「两数相加 I」那样从低位(链表尾)开始逐位相加,而链表又不能直接反向遍历,所以需要先解决低位访问的问题,栈是处理这个问题的最优选择(先进后出特性完美匹配 "从尾到头取数")。

解题核心思路

  1. 栈存储数字 :将两个链表的所有节点值分别压入两个栈,此时栈顶为数字的最低位,实现 "从低位到高位" 的访问;
  2. 逐位相加 :从两个栈顶同时取数,计算当前位和(含进位),当前位值为 和%10,新进位为 和/10
  3. 头插法建链表 :由于相加结果是从低位到高位计算的,需要用头插法将每个计算结果节点插入新链表头部,保证最终链表头为最高位;
  4. 处理剩余进位:若所有数相加后仍有进位(如 999 + 1 = 1000),需在链表头部补充进位节点。

完整 C++ 代码实现

cpp 复制代码
#include <iostream>
#include <stack>
using namespace std;

// 链表节点定义(LeetCode 原题定义)
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        stack<int> s1, s2;
        // 步骤1:将两个链表的数字分别压入栈,栈顶为最低位
        while (l1 != nullptr) {
            s1.push(l1->val);
            l1 = l1->next;
        }
        while (l2 != nullptr) {
            s2.push(l2->val);
            l2 = l2->next;
        }

        int carry = 0; // 进位,初始为0
        ListNode* head = nullptr; // 结果链表头结点,初始为空

        // 步骤2:逐位相加,直到栈空且无进位
        while (!s1.empty() || !s2.empty() || carry != 0) {
            int num1 = 0, num2 = 0;
            if (!s1.empty()) {
                num1 = s1.top();
                s1.pop();
            }
            if (!s2.empty()) {
                num2 = s2.top();
                s2.pop();
            }

            int sum = num1 + num2 + carry; // 当前位总和(含进位)
            carry = sum / 10; // 新的进位
            int curVal = sum % 10; // 当前位的数值

            // 步骤3:头插法创建新节点,保证高位在前
            ListNode* newNode = new ListNode(curVal);
            newNode->next = head;
            head = newNode;
        }

        return head;
    }
};

// 辅助函数:创建链表(用于测试)
ListNode* createList(int arr[], int n) {
    if (n == 0) return nullptr;
    ListNode* head = new ListNode(arr[0]);
    ListNode* cur = head;
    for (int i = 1; i < n; i++) {
        cur->next = new ListNode(arr[i]);
        cur = cur->next;
    }
    return head;
}

// 辅助函数:打印链表(用于测试)
void printList(ListNode* head) {
    ListNode* cur = head;
    while (cur != nullptr) {
        cout << cur->val;
        if (cur->next != nullptr) cout << " -> ";
        cur = cur->next;
    }
    cout << endl;
}

// 测试主函数
int main() {
    // 示例1:l1 = [7,2,4,3], l2 = [5,6,4],输出[7,8,0,7]
    int arr1[] = {7,2,4,3};
    int arr2[] = {5,6,4};
    ListNode* l1 = createList(arr1, 4);
    ListNode* l2 = createList(arr2, 3);

    Solution sol;
    ListNode* res = sol.addTwoNumbers(l1, l2);
    cout << "相加结果:";
    printList(res);

    return 0;
}

关键细节解析

1. 栈的作用

将链表 7->2->4->3 压入栈后,栈内元素为 [7,2,4,3](栈底到栈顶),栈顶是 3(最低位) ,出栈顺序为 3→4→2→7,刚好匹配加法 "从低位到高位" 的计算顺序。

2. 头插法的必要性

假设相加过程得到的位依次是 7→0→8→7(低位到高位),如果用普通尾插法,链表会是 7->0->8->7(低位在前),不符合题目要求;头插法 会依次将节点插入头部,最终得到 7->8->0->7(高位在前),完美匹配结果要求。

3. 循环条件的完整性

循环条件为 !s1.empty() || !s2.empty() || carry != 0,覆盖三种情况:

  • 栈 s1 还有数未加;
  • 栈 s2 还有数未加;
  • 所有数都加完,但仍有进位(如 999 + 1 = 1000,最后需要补充进位 1)。
4. 与「两数相加 I」的核心区别
特性 两数相加 I 两数相加 II
数字存储顺序 低位在链表头 高位在链表头
遍历顺序 从链表头直接遍历(低位→高位) 借助栈反向遍历(低位→高位)
链表构建方式 尾插法 头插法

代码运行结果

以示例 1 为例,输入 l1 = [7,2,4,3]l2 = [5,6,4],运行代码后输出:

bash 复制代码
相加结果:7 -> 8 -> 0 -> 7

完全匹配题目要求。

复杂度分析

  • 时间复杂度:O(m+n),其中 、 分别是两个链表的长度,需遍历两个链表压栈(O(m+n)),再遍历栈相加(O(m+n)),总时间为线性。
  • 空间复杂度:O(m+n),主要为两个栈的存储空间,最坏情况下存储所有节点值。

测试用例扩展

  1. 示例 2:l1 = [2,4,3]l2 = [5,6,4] → 输出 [8,0,7]
  2. 示例 3:l1 = [0]l2 = [0] → 输出 [0]
  3. 有进位:l1 = [9,9]l2 = [1] → 输出 [1,0,0]

只需修改主函数中的数组和长度,即可测试上述用例。

js实现

其实我不知js应该如何写这个语法,感觉有点难啊。js怎么写栈,做这个题目效率奇低。。

两数相加 II(JavaScript 实现)

你觉得这道题用 JavaScript 实现难,核心是对JS 中栈的模拟链表节点的创建 / 操作 不熟悉,再加上需要结合头插法建链表的逻辑,所以会觉得效率低、无从下手。

其实 JS 中用数组就能完美模拟栈(先进后出),链表操作也有固定范式,下面我会从核心思路JS 栈的实现完整代码关键细节四个部分讲清楚,保证易懂且效率达标。

一、核心解题思路(和 C++ 完全一致)

这道题和「两数相加 I」的核心区别是数字高位在链表头,无法直接从低位开始相加,所以核心解决思路不变:

  1. 栈存数字 :将两个链表的节点值分别存入两个 "栈",利用栈先进后出 特性,让栈顶为数字最低位,实现从低位到高位的访问;
  2. 逐位相加:从两个栈顶取数,计算当前位和(含进位),当前位值 = 和 % 10,新进位 = 和 / 10(取整);
  3. 头插法建链表 :相加结果是从低位到高位计算的,用头插法将每个结果节点插入新链表头部,保证最终链表高位在前
  4. 处理剩余进位:所有数相加完若还有进位(如 999 + 1 = 1000),需在链表头部补充进位节点。

二、JavaScript 中栈的实现(极简高效)

JS 中没有原生的 Stack 类,但数组的 push()(入栈)和 pop()(出栈,取栈顶元素) 方法完全符合栈的先进后出特性,是模拟栈的最优选择,时间复杂度均为 O (1),效率和原生栈一致。

  • 入栈:stack.push(val) → 向数组末尾添加元素(栈顶);
  • 出栈 / 取栈顶:先通过 stack[stack.length - 1] 取栈顶值,再用 stack.pop() 移除栈顶元素;
  • 判断栈空:stack.length === 0

三、JavaScript 完整实现(含注释 + 测试用例)

3.1 完整代码(兼容 LeetCode 提交)

LeetCode 中 JS 提交时,链表节点会被自动定义,直接写核心逻辑即可,代码如下:

javascript 复制代码
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    // 用数组模拟两个栈,存储链表节点值
    const s1 = [], s2 = [];
    // 步骤1:将l1所有节点值压入栈s1
    while (l1) {
        s1.push(l1.val);
        l1 = l1.next;
    }
    // 步骤1:将l2所有节点值压入栈s2
    while (l2) {
        s2.push(l2.val);
        l2 = l2.next;
    }

    let carry = 0; // 进位,初始为0
    let head = null; // 结果链表头节点,初始为null

    // 步骤2:逐位相加,直到栈空且无进位(核心循环)
    while (s1.length > 0 || s2.length > 0 || carry > 0) {
        // 取栈顶值,栈空则为0
        const num1 = s1.length > 0 ? s1.pop() : 0;
        const num2 = s2.length > 0 ? s2.pop() : 0;
        
        const sum = num1 + num2 + carry; // 当前位总和(含进位)
        carry = Math.floor(sum / 10); // 计算新进位(取整,避免小数)
        const curVal = sum % 10; // 当前位的最终值

        // 步骤3:头插法创建新节点,保证高位在前
        const newNode = new ListNode(curVal);
        newNode.next = head; // 新节点的next指向当前头节点
        head = newNode; // 更新头节点为新节点
    }

    return head;
};

3.2 本地测试代码(可直接运行)

如果想在本地浏览器 / Node.js 中测试,需要手动实现链表创建链表打印辅助函数,完整可运行代码如下:

javascript 复制代码
// 链表节点构造函数
function ListNode(val, next) {
    this.val = (val === undefined ? 0 : val);
    this.next = (next === undefined ? null : next);
}

// 核心解题函数
var addTwoNumbers = function(l1, l2) {
    const s1 = [], s2 = [];
    while (l1) {
        s1.push(l1.val);
        l1 = l1.next;
    }
    while (l2) {
        s2.push(l2.val);
        l2 = l2.next;
    }

    let carry = 0;
    let head = null;
    while (s1.length > 0 || s2.length > 0 || carry > 0) {
        const num1 = s1.length > 0 ? s1.pop() : 0;
        const num2 = s2.length > 0 ? s2.pop() : 0;
        const sum = num1 + num2 + carry;
        carry = Math.floor(sum / 10);
        const curVal = sum % 10;

        const newNode = new ListNode(curVal);
        newNode.next = head;
        head = newNode;
    }
    return head;
};

// 辅助函数:根据数组创建链表(如 [7,2,4,3] → 7→2→4→3)
function createList(arr) {
    if (arr.length === 0) return null;
    const head = new ListNode(arr[0]);
    let cur = head;
    for (let i = 1; i < arr.length; i++) {
        cur.next = new ListNode(arr[i]);
        cur = cur.next;
    }
    return head;
}

// 辅助函数:打印链表(如 7→8→0→7)
function printList(head) {
    const res = [];
    let cur = head;
    while (cur) {
        res.push(cur.val);
        cur = cur.next;
    }
    console.log(res.join(' -> '));
}

// 测试用例1:l1=[7,2,4,3], l2=[5,6,4] → 输出7->8->0->7
const l1 = createList([7,2,4,3]);
const l2 = createList([5,6,4]);
printList(addTwoNumbers(l1, l2));

// 测试用例2:l1=[2,4,3], l2=[5,6,4] → 输出8->0->7
// const l1 = createList([2,4,3]);
// const l2 = createList([5,6,4]);
// printList(addTwoNumbers(l1, l2));

// 测试用例3:l1=[0], l2=[0] → 输出0
// const l1 = createList([0]);
// const l2 = createList([0]);
// printList(addTwoNumbers(l1, l2));

// 测试用例4:l1=[9,9], l2=[1] → 输出1->0->0
// const l1 = createList([9,9]);
// const l2 = createList([1]);
// printList(addTwoNumbers(l1, l2));

四、JS 实现的关键细节解析(避坑 + 理解)

4.1 栈的操作细节

JS 数组模拟栈时,不要用 shift ()/unshift () (这两个方法是操作数组头部,时间复杂度 O (n),会降低效率),只用 push()(入栈)和 pop()(出栈),这是保证效率的关键。

4.2 进位的处理

JS 中除法 / 会返回小数(如 17/10 = 1.7),所以计算进位时必须用 Math.floor(sum / 10) 取整,否则进位会是小数,导致后续计算错误。

4.3 头插法的核心逻辑(必懂)

因为我们是从低位到高位 计算结果(比如先算个位,再算十位,最后算百位),而题目要求结果链表高位在前(百位→十位→个位),所以必须用头插法:

  1. 初始时结果链表头节点 head = null
  2. 每计算一个低位值,创建新节点,让新节点的 next 指向当前的 head
  3. 再将 head 更新为这个新节点;
  4. 最终 head 就是高位在前的结果链表头。

举例:计算 7243 + 564 = 7807,计算顺序是 3+4=7(个位)→4+6=10(十位,留 0 进 1)→2+5+1=8(百位)→7+0=7(千位),头插法过程:

  • 个位 7:newNode (7) → next=null → head=7
  • 十位 0:newNode (0) → next=7 → head=0→7
  • 百位 8:newNode (8) → next=0→7 → head=8→0→7
  • 千位 7:newNode (7) → next=8→0→7 → head=7→8→0→7(最终结果)

4.4 循环条件的完整性

循环条件 s1.length > 0 || s2.length > 0 || carry > 0 必须包含三个条件,缺一不可:

  1. s1.length > 0:s1 栈还有数未加;
  2. s2.length > 0:s2 栈还有数未加;
  3. carry > 0:所有数加完但还有进位(如 999+1=1000,最后需要补充进位 1)。

4.5 链表节点的判空

JS 中链表节点判空直接用 if (l1) 即可(等价于 if (l1 !== null && l1 !== undefined)),比写 l1 !== null 更简洁。

五、复杂度分析(JS 实现和 C++ 效率一致)

  • 时间复杂度:O (m + n),其中 m、n 分别是两个链表的长度。需要遍历两个链表压栈(O (m+n)),再遍历栈逐位相加(O (m+n)),总时间为线性,无额外耗时;
  • 空间复杂度:O (m + n),主要为两个栈的存储空间,最坏情况下需要存储两个链表的所有节点值。

六、和 C++ 实现的异同

特性 C++ 实现 JavaScript 实现
栈的实现 原生 stack<int> 数组 [](push/pop)
链表节点创建 new ListNode(curVal) new ListNode(curVal)
头插法逻辑 完全一致 完全一致
进位计算 整数除法 sum / 10 Math.floor(sum / 10)
判空方式 l1 != nullptr if (l1)
时间 / 空间复杂度 O(m+n) O(m+n)

简单说:核心逻辑完全相同,只是语法细节适配了各自语言,JS 实现的效率和 C++ 基本一致,不存在 "效率奇低" 的问题。

总结

用 JS 解这道题的核心要点就 3 个:

  1. 用数组模拟栈:只用 push/pop 方法,保证效率;
  2. 头插法建链表:解决 "低位计算、高位在前" 的问题;
  3. 处理好进位:用 Math.floor 取整,循环条件包含剩余进位。

你之前觉得难,只是对 JS 的栈模拟和链表操作不熟悉,多写两遍测试用例,熟悉这些固定范式后,就会发现这道题的 JS 实现其实很简洁。

3 小结

问题不过夜,是这样的,现在回头看看昨天晚上卡住的题目,无非就是核心数据结构栈操作而已,但是却不太会做,现在自己做也有点效率低,先收藏一下。

主要就是1.栈操作 , 2.头插法

算法题可以在调节写项目或者睡前 / 早上,这些时候状态最好头脑最清晰,睡前还可以反思一下。下次不这么乱七八糟磨洋工了。

相关推荐
long3161 小时前
KMP模式搜索算法
数据库·算法
_OP_CHEN2 小时前
【算法基础篇】(五十三)隔板法指南:从 “分球入盒” 到不定方程,组合计数的万能解题模板
算法·蓝桥杯·c/c++·组合数学·隔板法·acm/icpc
卡兰芙的微笑2 小时前
编译鸿蒙6.0release版本出错
学习
暴躁小师兄数据学院2 小时前
【WEB3.0零基础转行笔记】Golang编程篇-第3讲:GO语言中的运算符
笔记·golang·web3
近津薪荼2 小时前
优选算法——滑动窗口3(子数组)
c++·学习·算法
遨游xyz2 小时前
数据结构-栈
java·数据结构·算法
FPGA小迷弟2 小时前
基于FPGA实现HDMI接口,选型/核心技术
学习·fpga开发·verilog·fpga·modelsim
ghie90902 小时前
基于动态规划算法的混合动力汽车能量管理建模与计算
算法·汽车·动态规划
蓝海星梦2 小时前
GRPO 算法演进——裁剪机制篇
论文阅读·人工智能·深度学习·算法·自然语言处理·强化学习