目录

题目描述

LCR 025. 两数相加 II

题意分析

给两条非空链表,每个节点存一个数位,最高位在链表头(也就是正常的书写顺序),把两个数相加并以同样的顺序返回结果链表。

竖式加法必须从个位算起,因为进位是从低位向高位传递的。而链表的头是最高位、只能从头往尾走,方向恰好与计算方向相反——这就是本题与「低位在前」版本的全部差异,也是难点所在。

题目额外要求不能修改输入链表,这条约束直接否掉了「把两条链都反转、算完再反转回来」这个最省事的做法(除非算完再把输入还原,写起来啰嗦且容易漏)。因此需要一种不改结构就能倒序访问的手段。

数位长度可能不等,短的那条要当作高位补零;结果长度可能比两者都长,例如 9 + 1 会进位出一个新的最高位。

数值范围提示也很关键:整数可能极长,不能先把链表转成 intlong 相加再拆位,必然溢出,只能逐位模拟。

解法:数学推导

核心思路

先想暴力:把两条链表的数字拼成整数相加。链表长度可达上百位,任何定长整型都放不下,方案直接作废。这提醒我们必须停留在「逐位处理」的层面。

逐位处理要求从低位开始,而链表只能从高位开始。要解决方向冲突,可选的手段有三类:反转链表(但题目不许改结构)、递归到底再回溯(等价于用系统栈)、或者显式地把数位压进栈里。栈是「后进先出」,把链表从头压到尾之后,弹栈顺序恰好就是从个位到最高位——这正是我们需要的顺序,而且完全没有触碰原链表的指针。

于是状态定义为:carry 表示已处理低位产生的进位,取值只可能是 0 或 1;dummy.next 始终指向当前已生成结果的最高位

每一轮把两个栈的栈顶(缺则补 0)与 carry 相加,得到的和 carry 中,carry % 10 是本位数字,carry / 10 是新的进位。这里有个小技巧:直接把和累加进 carry 变量,用完再整除 10,省掉一个临时变量。

结果的顺序问题同样要解决:我们是从个位往高位生成的,但输出要求高位在前。答案是头插法——每个新节点都插到 dummy 之后,后生成的高位自然排在前面,输出即为正确顺序,而且不需要最后再反转一次。

循环条件写成「两个栈还有元素,或者 carry 非零」,这样 999 + 1 这种最高位进位的情况会自然地多跑一轮生成开头的 1,无需任何特判。

解题步骤

  • 两条链分别压栈for (; l1 != null; l1 = l1.next) s1.push(l1.val);l2 同理。只读 val、只沿 next 走,输入链表一根指针都没改,满足「不修改输入」的要求。
  • 准备进位与哑结点carry = 0dummy = new ListNode()。哑结点让头插法有一个固定的插入锚点,省掉「结果为空时特殊处理」的分支。
  • 循环条件写三项析取!s1.isEmpty() || !s2.isEmpty() || carry != 0。前两项负责把两条链的数位取完,第三项负责最高位的进位,缺了它 5 + 5 会漏掉开头的 1
  • 累加当前位carry += (s1 空 ? 0 : s1.pop()) + (s2 空 ? 0 : s2.pop())。短链弹空后补 0,等价于在高位补零对齐,不需要预先把两条链补成等长。
  • 头插新节点并更新进位node = new ListNode(carry % 10, dummy.next); dummy.next = node; carry /= 10;。新节点的 next 直接接上此前生成的最高位,插完再更新 dummy.next。顺序不能反——先改 dummy.next 会让新节点接到自己身上。
  • 返回 dummy.next:它指向最终的最高位。

l1 = 7 → 2 → 4 → 3l2 = 5 → 6 → 4 走一遍,即 7243 + 564 = 7807。压栈后 s1 从栈顶到栈底是 3, 4, 2, 7s24, 6, 5

第一轮:carry = 0 + 3 + 4 = 7,头插节点 7,结果为 7carry = 0。第二轮:carry = 0 + 4 + 6 = 10,头插节点 0,结果为 0 → 7carry = 1。第三轮:carry = 1 + 2 + 5 = 8,头插节点 8,结果为 8 → 0 → 7carry = 0。第四轮:s2 已空补 0,carry = 0 + 7 + 0 = 7,头插节点 7,结果为 7 → 8 → 0 → 7carry = 0。此时两个栈都空且 carry 为 0,循环退出,返回 7 → 8 → 0 → 7,正是 7807。

再看最高位进位的用例 l1 = 5l2 = 5:第一轮 carry = 10,头插节点 0carry = 1;两个栈都空但 carry != 0,循环继续;第二轮 carry = 1,头插节点 1,结果为 1 → 0carry = 0,退出。返回 1 → 0,即 10,正确。

代码实现

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        Deque<Integer> s1 = new ArrayDeque<>();
        Deque<Integer> s2 = new ArrayDeque<>();
        // 只读不改,压栈后弹出顺序即为从个位到最高位。
        for (; l1 != null; l1 = l1.next) {
            s1.push(l1.val);
        }
        for (; l2 != null; l2 = l2.next) {
            s2.push(l2.val);
        }
        int carry = 0;
        ListNode dummy = new ListNode();
        while (!s1.isEmpty() || !s2.isEmpty() || carry != 0) {
            carry += (s1.isEmpty() ? 0 : s1.pop()) + (s2.isEmpty() ? 0 : s2.pop());
            // 头插法:后算出的高位自动排到前面,省掉最后一次反转。
            ListNode node = new ListNode(carry % 10, dummy.next);
            dummy.next = node;
            carry /= 10;
        }
        return dummy.next;
    }
}
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
    stack1 := []int{}
    stack2 := []int{}
    // 只读不改,出栈顺序即为从个位到最高位。
    for l1 != nil {
        stack1 = append(stack1, l1.Val)
        l1 = l1.Next
    }
    for l2 != nil {
        stack2 = append(stack2, l2.Val)
        l2 = l2.Next
    }

    carry := 0
    dummy := &ListNode{}
    for len(stack1) > 0 || len(stack2) > 0 || carry > 0 {
        if len(stack1) > 0 {
            carry += stack1[len(stack1)-1]
            stack1 = stack1[:len(stack1)-1]
        }
        if len(stack2) > 0 {
            carry += stack2[len(stack2)-1]
            stack2 = stack2[:len(stack2)-1]
        }
        // 头插法:后算出的高位自动排到前面。
        node := &ListNode{Val: carry % 10, Next: dummy.Next}
        dummy.Next = node
        carry /= 10
    }
    return dummy.Next
}

复杂度分析

  • 时间复杂度:$O(m+n)$,mn 为两条链的长度。两次压栈各扫一遍,主循环轮数不超过 $\max(m,n)+1$,每轮只有常数次弹栈、取模与建节点。
  • 空间复杂度:$O(m+n)$,两个栈分别存下全部数位,这是「不修改输入又要倒序访问」付出的代价。返回的结果链表不计入额外空间。

关键点总结

  • 数位在链表里但计算方向相反时,栈是把「顺序访问」翻成「逆序访问」的标准工具,代价是 $O(n)$ 空间,换来的是输入链表零改动。
  • 用「carry 直接累加、再取模与整除」的写法,把本位与进位从同一个变量里取出,比另设 sum 更短且不易写错。
  • 循环条件必须把 carry != 0 算进去,否则最高位的进位会被静默丢掉,这是所有大数加法的共同陷阱。
  • 头插法让生成顺序与输出顺序自动对齐,凡是「从后往前算、要求从前往后输出」的链表题都可以这样省掉一次反转。
  • 短链弹空补 0 等价于高位补零,比预先把两条链补齐长度更省事,也避免了修改输入。
  • 面试视角:先问清楚能否修改输入链表——能改就答「反转两条链、复用低位在前的解法、最后反转回来」,$O(1)$ 额外空间;不能改就答栈解法。主动比较这两条路线并说明各自代价,比只写出一种更能体现取舍能力。

易错点总结

  • 循环条件漏掉 carry != 05 + 5 会返回 0 而不是 1 → 0,最高位进位被吞。
  • 头插时先改 dummy.next 再建节点:写成 dummy.next = node; node.next = dummy.next; 会让节点指向自己,7243 + 564 输出时死循环。
  • 改成尾插法却忘记最后反转7243 + 564 会输出 7 → 0 → 8 → 7,数位顺序整个颠倒。
  • 短链弹空时不补 0 而直接退出循环7243 + 564 只算了三位就停,返回 8 → 0 → 7,丢掉最高位的 7。
  • 把链表转成 long 相加:数位可长达上百位,99...9 + 1 会直接溢出,得到负数或截断结果。
  • 反转输入链表后不还原:题目要求不修改输入,判题在返回后校验原链表时会判错。
  • 进位写成 carry = carry / 10 之前就取了 carry % 10 存入变量却又用旧 carry:例如先 carry /= 10new ListNode(carry % 10)5 + 5 会生成数字 1 而不是 0,本位与进位对调。
  • Go 里循环条件写 carry > 0 之外还漏判栈空:若写成 for len(stack1) > 0 && len(stack2) > 0,长度不等时短链先耗尽,7243 + 564 只输出低三位。
  • Stack 或按值传参把链表复制一份再反转:能过但多了一份 $O(n)$ 拷贝且逻辑更绕,面试时说不清「为什么不直接压栈」。

相似题目

题目 难度 考察点
445. 两数相加 II 中等 与本题同题,可直接套用栈 + 头插的写法
2. 两数相加 中等 低位在前,方向天然一致,直接尾插逐位相加,不需要栈
369. 给单链表加一 中等 只加 1,可从右往左找第一个非 9 位,改动范围远小于通用加法
面试题 02.05. 链表求和 中等 同时考察正序与逆序两个版本,正序版即本题
61. 旋转链表 中等 同样需要「从尾部反推位置」,但靠成环再断开而非借助栈
143. 重排链表 中等 也需要逆序访问后半段,可用栈也可用反转,是本题空间取舍的另一个样本