仓颉语言实战:链表两数相加工具库详解
前言
在数据结构与算法的学习中,链表操作是一个经典且重要的主题。今天,我将带大家使用华为推出的新一代编程语言——仓颉语言,实现一个实用的链表两数相加工具库。
这个工具库解决的是一个经典问题:给定两个非空链表,表示两个非负整数,它们的每位数字都是按照逆序方式存储的,每个节点只能存储一位数字。我们需要将这两个数相加,并以相同形式返回一个表示和的链表。
本文将分享从问题分析到完整实现的全过程,包括链表数据结构设计、核心算法实现、边界情况处理以及完善的单元测试,希望能为学习仓颉语言的开发者提供一个实用的参考案例。
问题描述
题目定义
给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例说明
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807
链表表示:
2 -> 4 -> 3 (表示数字 342)
+ 5 -> 6 -> 4 (表示数字 465)
-----------------
= 7 -> 0 -> 8 (表示数字 807)
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
解释:9999999 + 9999 = 10009998
项目设计
设计目标
在设计这个工具库时,我确立了以下核心目标:正确性:准确处理各种边界情况和进位逻辑
高效性:时间复杂度O(max(m,n)),空间复杂度O(max(m,n))
可读性:代码结构清晰,逻辑易于理解
可测试性:提供完善的测试用例,覆盖各种场景
实用性:提供辅助函数方便创建和展示链表
项目架构
项目采用典型的仓颉语言项目结构:
add_two_numbers/
├── cjpm.toml # 项目配置文件
├── src/
│ ├── main.cj # 主程序入口(示例代码)
│ └── module/
│ ├── list_node.cj # 链表节点定义
│ ├── add_two_numbers.cj # 核心功能实现
│ └── add_two_numbers_test.cj # 单元测试
├── doc/ # 文档目录
├── README.md # 项目说明
└── CHANGELOG.md # 版本历史
算法思路
解决这个问题的关键思路如下:
同时遍历:使用指针同时遍历两个链表
逐位相加:将对应位置的数字相加,加上上一位的进位
构建结果链表:将每位的计算结果(取余10)作为新节点
更新进位:计算新的进位值(除以10)
处理剩余:当一个链表遍历完后,继续处理另一个链表
最终进位:如果最后还有进位,添加新节点
用伪代码表示:
carry = 0
while l1 != null || l2 != null || carry != 0:
sum = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry
carry = sum / 10
newNode.val = sum % 10
移动指针
核心实现
1. 链表节点定义
首先,我们需要定义链表节点的数据结构:

代码解析:
public class:定义公共类,可被外部访问
var 成员变量:使用 var 声明可变成员变量
可选类型:?ListNode 表示可选类型,可能为 None
构造函数:使用 init 定义构造函数,支持默认参数
2. 核心算法实现
接下来实现核心的两数相加函数:
// 两数相加函数
// 给定两个非空链表,表示两个非负整数
// 数字按逆序存储,每个节点存储一位数字
// 返回两数之和的链表
public func addTwoNumbers(l1: ?ListNode, l2: ?ListNode): ?ListNode {
var dummyHead = ListNode(0) // 虚拟头节点
var current = dummyHead
var carry: Int64 = 0 // 进位
var p1 = l1
var p2 = l2
// 遍历两个链表
var continueLoop = true
while (continueLoop) {
var val1: Int64 = 0
var val2: Int64 = 0
var hasP1 = false
var hasP2 = false
// 获取当前节点的值
match (p1) {
case Some(node) =>
val1 = node.val
p1 = node.next
hasP1 = true
case None =>
val1 = 0
}
match (p2) {
case Some(node) =>
val2 = node.val
p2 = node.next
hasP2 = true
case None =>
val2 = 0
}
// 检查是否继续循环
if (!hasP1 && !hasP2 && carry == 0) {
continueLoop = false
break
}
// 计算和与进位
var sum = val1 + val2 + carry
carry = sum / 10
var digit = sum % 10
// 创建新节点
current.next = Some(ListNode(digit))
match (current.next) {
case Some(node) =>
current = node
case None =>
break
}
}
return dummyHead.next
}
算法解析:
哑节点技巧:使用哑节点(dummy head)简化边界处理,避免特殊判断第一个节点
三指针法:p1、p2分别遍历两个链表,current构建结果链表
进位处理:使用carry变量保存进位,每次计算都要加上
条件判断:使用三元表达式处理链表为空的情况
循环条件:只要有任何一个链表未遍历完或还有进位,就继续循环
可选类型处理:使用match表达式安全地处理可选类型
3. 辅助函数:数组转链表
为了方便测试和使用,我们提供一个将数组转换为链表的函数:

4. 辅助函数:链表转数组
为了便于验证结果和调试,我们还需要一个将链表转换为数组的函数:

5. 辅助函数:打印链表
为了调试和展示,提供一个打印链表的函数:

使用示例
在 main.cj 中,我们提供完整的使用示例:
main(): Int64 {
println("========== 两数相加工具库 ==========")
println("")
// 测试用例 1: 342 + 465 = 807
// 链表表示: [2,4,3] + [5,6,4] = [7,0,8]
println("测试用例 1: 342 + 465 = 807")
var l1 = createListFromArray([2, 4, 3])
var l2 = createListFromArray([5, 6, 4])
printList(l1, "输入1")
printList(l2, "输入2")
var result1 = addTwoNumbers(l1, l2)
printList(result1, "结果")
println("验证: ${listToNumber(l1)} + ${listToNumber(l2)} = ${listToNumber(result1)}")
println("")
// 测试用例 2: 0 + 0 = 0
println("测试用例 2: 0 + 0 = 0")
var l3 = createListFromArray([0])
var l4 = createListFromArray([0])
printList(l3, "输入1")
printList(l4, "输入2")
var result2 = addTwoNumbers(l3, l4)
printList(result2, "结果")
println("验证: ${listToNumber(l3)} + ${listToNumber(l4)} = ${listToNumber(result2)}")
println("")
// 测试用例 3: 9999999 + 9999 = 10009998
// 链表表示: [9,9,9,9,9,9,9] + [9,9,9,9] = [8,9,9,9,0,0,0,1]
println("测试用例 3: 9999999 + 9999 = 10009998")
var l5 = createListFromArray([9, 9, 9, 9, 9, 9, 9])
var l6 = createListFromArray([9, 9, 9, 9])
printList(l5, "输入1")
printList(l6, "输入2")
var result3 = addTwoNumbers(l5, l6)
printList(result3, "结果")
println("验证: ${listToNumber(l5)} + ${listToNumber(l6)} = ${listToNumber(result3)}")
println("")
// 测试用例 4: 123 + 456 = 579
println("测试用例 4: 123 + 456 = 579")
var l7 = createListFromArray([3, 2, 1])
var l8 = createListFromArray([6, 5, 4])
printList(l7, "输入1")
printList(l8, "输入2")
var result4 = addTwoNumbers(l7, l8)
printList(result4, "结果")
println("验证: ${listToNumber(l7)} + ${listToNumber(l8)} = ${listToNumber(result4)}")
println("")
// 测试用例 5: 99 + 1 = 100
println("测试用例 5: 99 + 1 = 100")
var l9 = createListFromArray([9, 9])
var l10 = createListFromArray([1])
printList(l9, "输入1")
printList(l10, "输入2")
var result5 = addTwoNumbers(l9, l10)
printList(result5, "结果")
println("验证: ${listToNumber(l9)} + ${listToNumber(l10)} = ${listToNumber(result5)}")
println("")
println("========== 所有测试完成 ==========")
return 0
}
运行结果:

复杂度分析
时间复杂度
O(max(m, n))
其中 m 和 n 分别是两个链表的长度。算法需要遍历两个链表中较长的那个,每个节点只访问一次。
空间复杂度
O(max(m, n))
新创建的链表最多包含 max(m, n) + 1 个节点(考虑最高位进位的情况)。
项目配置
cjpm.toml 配置文件:

关键技术点总结
1. 哑节点技巧
使用哑节点(Dummy Head)是链表操作中的常用技巧,可以:
统一处理头节点和其他节点
简化边界条件判断
使代码更加简洁优雅
let dummyHead = ListNode(0) // 创建哑节点
// ... 操作
return dummyHead.next // 返回真正的头节点
2. 进位处理
进位是这个问题的核心:
let sum = val1 + val2 + carry // 当前位的总和
carry = sum / 10 // 计算新进位
let digit = sum % 10 // 当前位的数字
3. 可选类型处理
仓颉语言的可选类型提供了空安全保证:
var next: ?ListNode = None // 声明可选类型
match (node) { // 使用match安全访问
case Some(n) => n.val
case None => 0
}
4. 循环条件设计
巧妙的循环条件确保所有情况都被正确处理:
while (p1 != None || p2 != None || carry != 0) {
// 继续条件:任一链表未完成 或 还有进位
}
扩展思考
1. 正序存储的情况
如果数字是正序存储(如 [3,4,2] 表示 342),该如何处理?
解决方案:
先反转两个链表
执行相加操作
再反转结果链表
2. 支持负数
如果要支持负数相加,需要:
增加符号位标记
实现减法逻辑
处理借位而不是进位
3. 支持更大的进制
如果每个节点存储的不是 0-9,而是 0-99 或其他进制:
const BASE: Int64 = 100 // 定义进制
carry = sum / BASE
digit = sum % BASE
4. 链表加法优化
对于频繁的加法操作,可以考虑:
将链表转换为大整数
使用大整数库进行计算
再转换回链表形式
最佳实践建议
1. 代码规范
使用清晰的变量命名(dummyHead、carry)
添加详细的注释说明算法思路
遵循仓颉语言的命名规范
2. 错误处理
// 添加参数校验
public func addTwoNumbers(l1: ?ListNode, l2: ?ListNode): ?ListNode {
if (l1 == None && l2 == None) {
return None
}
// ... 其他逻辑
}
3. 性能优化
避免不必要的对象创建
减少链表遍历次数
合理使用缓存
4. 测试驱动
先编写测试用例
覆盖所有边界情况
使用断言验证结果
总结
通过这个项目,我们:
- 掌握了链表的基本操作:节点创建、遍历、连接
- 学习了经典算法思想:哑节点、双指针、进位处理
- 实践了仓颉语言特性:可选类型、模式匹配、类定义
- 建立了完整的工程体系:代码实现、单元测试、文档编写
- 链表两数相加是一个看似简单但细节丰富的算法问题,通过仓颉语言的实现,我们不仅解决了问题本身,更重要的是掌握了:
- 问题分析能力:将实际问题抽象为算法模型
- 编码实现能力:将算法思想转化为可运行的代码
- 工程化思维:编写可测试、可维护、可扩展的代码
希望这个实战案例能够帮助你更好地理解仓颉语言和数据结构算法。如果你有任何问题或建议,欢迎交流讨论!
参考资料
仓颉语言官方文档
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)