ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

链表相加算法解析与Java实现

链表相加算法解析与Java实现

1. 链表相加算法解析与实现

两数相加是LeetCode上经典的链表操作题目,编号为第2题。这道题考察了对链表数据结构的理解、指针操作以及数学进位处理能力。题目要求我们将两个用链表逆序表示的非负整数相加,并返回同样逆序存储的和的链表。

提示:链表逆序存储意味着链表的第一个节点表示数字的最低位(个位),这与我们日常书写数字的顺序相反,但恰好符合加法运算从低位到高位的计算顺序。

1.1 问题描述与示例分析

题目给出两个非空的链表,每个节点存储一位数字,且数字以逆序方式存储。例如:

  • 链表[2,4,3]表示数字342
  • 链表[5,6,4]表示数字465

我们需要实现一个函数,将这两个链表表示的数字相加,并返回表示和的链表。按照上面的例子,342 + 465 = 807,所以应该返回[7,0,8]。

题目还给出了三个测试用例:

  1. 输入:[2,4,3]和[5,6,4],输出应为[7,0,8]
  2. 输入:[0]和[0],输出应为[0]
  3. 输入:[9,9,9,9,9,9,9]和[9,9,9,9],输出应为[8,9,9,9,0,0,0,1]

1.2 链表数据结构定义

在Java中,链表节点通常定义为:

static class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } }

每个ListNode包含两个属性:

  • val:当前节点存储的数字(0-9)
  • next:指向下一个节点的指针

2. 算法思路与实现细节

2.1 基本思路

解决这个问题的关键在于模拟手工加法的过程:

  1. 从最低位(链表头)开始相加
  2. 处理进位(如果两数相加大于等于10,则向高位进1)
  3. 处理链表长度不一致的情况(短链表的高位视为0)
  4. 最后检查是否还有进位需要处理

2.2 核心算法实现

下面是完整的Java实现代码:

public static ListNode addTwoNumbers(ListNode l1, ListNode l2) { int carry = 0; // 进位标志 ListNode result = new ListNode(0); // 结果链表的哑节点 ListNode sum = result; // 当前节点指针 while (l1 != null || l2 != null || carry != 0) { int n1 = (l1 != null) ? l1.val : 0; // 获取l1当前位的值,如果l1已经遍历完则为0 int n2 = (l2 != null) ? l2.val : 0; // 获取l2当前位的值,如果l2已经遍历完则为0 sum.next = new ListNode((n1 + n2 + carry) % 10); // 计算当前位的和 sum = sum.next; carry = (n1 + n2 + carry) / 10; // 计算进位 if (l1 != null) l1 = l1.next; // 移动l1指针 if (l2 != null) l2 = l2.next; // 移动l2指针 } return result.next; // 返回哑节点的下一个节点,即真正的结果链表头 }

2.3 关键点解析

  1. 哑节点的使用:我们创建一个val为0的哑节点作为结果链表的起始点,这样可以简化代码,避免处理空链表的特殊情况。最后返回的是result.next。

  2. 循环条件:while (l1 != null || l2 != null || carry != 0)这个条件确保了:

    • 只要l1或l2还有节点未处理,就继续循环
    • 即使l1和l2都处理完了,如果还有进位,也需要继续处理
  3. 进位处理:

    • (n1 + n2 + carry) % 10计算当前位的值
    • (n1 + n2 + carry) / 10计算进位值
  4. 链表长度不一致的处理:当其中一个链表已经遍历完时,使用三元运算符将其值视为0,这样就能继续与另一个链表的剩余部分相加。

3. 复杂度分析与边界条件

3.1 时间复杂度分析

算法的时间复杂度是O(max(m,n)),其中m和n分别是两个链表的长度。这是因为我们需要遍历两个链表中较长的那个的所有节点。

3.2 空间复杂度分析

空间复杂度也是O(max(m,n)),因为我们需要创建一个新的链表来存储结果,最坏情况下结果链表的长度会比输入链表中最长的那个多1(因为有进位)。

3.3 边界条件考虑

编写这类算法时,需要考虑以下边界条件:

  1. 两个链表长度不一致
  2. 最高位相加后有进位(如999+1=1000)
  3. 其中一个链表为空(根据题目描述,这种情况不会出现)
  4. 两个链表都只有一个节点且相加无进位(如2+3=5)
  5. 两个链表都只有一个节点且相加有进位(如5+5=10)

4. 测试用例验证

让我们详细分析题目提供的三个测试用例:

4.1 测试用例1:342 + 465 = 807

输入:

  • l1 = [2,4,3](表示342)
  • l2 = [5,6,4](表示465)

执行过程:

  1. 2+5=7,无进位 → [7]
  2. 4+6=10,写入0,进位1 → [7,0]
  3. 3+4+1=8,无进位 → [7,0,8] 输出:[7,0,8](表示807)

4.2 测试用例2:0 + 0 = 0

输入:

  • l1 = [0]
  • l2 = [0]

执行过程:

  1. 0+0=0,无进位 → [0] 输出:[0]

4.3 测试用例3:9999999 + 9999 = 10009998

输入:

  • l1 = [9,9,9,9,9,9,9](表示9999999)
  • l2 = [9,9,9,9](表示9999)

执行过程:

  1. 9+9=18 → [8],进位1
  2. 9+9+1=19 → [8,9],进位1
  3. 9+9+1=19 → [8,9,9],进位1
  4. 9+9+1=19 → [8,9,9,9],进位1
  5. 9+0+1=10 → [8,9,9,9,0],进位1
  6. 9+0+1=10 → [8,9,9,9,0,0],进位1
  7. 9+0+1=10 → [8,9,9,9,0,0,0],进位1
  8. 0+0+1=1 → [8,9,9,9,0,0,0,1] 输出:[8,9,9,9,0,0,0,1](表示10009998)

5. 常见问题与调试技巧

5.1 常见错误

  1. 忘记处理最后的进位:例如计算5+5时,结果应该是[0,1],但如果循环条件不检查carry,可能会只返回[0]。

  2. 链表指针移动错误:在移动l1和l2指针时,需要先检查是否为null,否则会抛出NullPointerException。

  3. 结果链表构建错误:容易犯的错误是忘记更新当前节点指针,导致所有节点都链接到第一个节点上。

5.2 调试技巧

  1. 打印中间结果:在循环中加入打印语句,输出当前位的计算结果和进位值。

  2. 可视化链表:可以编写一个辅助方法打印链表,方便查看中间结果。

  3. 单元测试:除了题目提供的测试用例外,还应该自己设计一些边界情况的测试用例。

5.3 性能优化

虽然这个算法已经是最优解,但仍有几点可以注意:

  1. 避免不必要的对象创建:在实际情况中,如果允许修改输入链表,可以考虑复用节点来减少内存分配。

  2. 并行计算:对于特别长的链表,可以考虑并行计算不同区段的和,但会增加实现复杂度。

6. 算法扩展与变种

6.1 数字正序存储的情况

如果链表是正序存储数字(即第一个节点是最高位),该如何处理?这种情况下,我们可以:

  1. 先将两个链表反转,然后使用本算法
  2. 或者使用栈来反转数字的顺序
  3. 也可以递归地从后向前计算

6.2 多个数字相加

如果有多个链表需要相加,可以扩展本算法,使用一个变量来累加所有链表当前位的和,然后计算当前位值和进位。

6.3 不同进制的情况

如果不是十进制相加,而是其他进制(如二进制、十六进制),只需要修改进位计算的部分即可:

int base = 16; // 以十六进制为例 sum.next = new ListNode((n1 + n2 + carry) % base); carry = (n1 + n2 + carry) / base;

7. 实际应用场景

这种链表相加的算法在实际中有多种应用:

  1. 大整数运算:当数字太大无法用基本数据类型表示时,可以用链表或数组来存储,并使用类似的算法进行运算。

  2. 高精度计算:科学计算中需要高精度时,这种逐位计算的方法非常有用。

  3. 数字货币:区块链技术中的一些计算也涉及大数运算。

8. 不同语言的实现对比

虽然我们使用Java实现了这个算法,但了解其他语言的实现方式也很有帮助:

8.1 Python实现

def addTwoNumbers(l1, l2): dummy = ListNode(0) current = dummy carry = 0 while l1 or l2 or carry: val1 = l1.val if l1 else 0 val2 = l2.val if l2 else 0 total = val1 + val2 + carry carry = total // 10 current.next = ListNode(total % 10) current = current.next l1 = l1.next if l1 else None l2 = l2.next if l2 else None return dummy.next

8.2 C++实现

ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { ListNode* dummy = new ListNode(0); ListNode* current = dummy; int carry = 0; while (l1 || l2 || carry) { int val1 = l1 ? l1->val : 0; int val2 = l2 ? l2->val : 0; int total = val1 + val2 + carry; carry = total / 10; current->next = new ListNode(total % 10); current = current->next; l1 = l1 ? l1->next : nullptr; l2 = l2 ? l2->next : nullptr; } return dummy->next; }

不同语言的实现思路基本相同,主要区别在于语法细节和指针/引用的处理方式。

9. 个人实现心得

在实际实现这个算法时,我总结了以下几点经验:

  1. 哑节点的使用:一开始我尝试不使用哑节点,结果发现需要处理很多特殊情况。使用哑节点后代码简洁了很多。

  2. 循环条件的设置:最初我只检查了l1和l2是否为null,忽略了最后的进位,导致一些测试用例失败。后来加上carry != 0的条件后才完全正确。

  3. 代码可读性:将n1和n2的提取单独写成一行,而不是直接写在计算表达式中,虽然多了一行代码,但可读性大大提高。

  4. 测试的重要性:自己编写测试用例时,应该包括各种边界情况,如只有进位、链表长度相差很大等情况。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进