链表求和算是我最早接触的一批算法题,当年刷到它的时候,我写了将近70行代码:先遍历两个链表取出所有数字塞进vector,逐位加完再new一串节点拼回去。当时跑通测试用例就再没回头看过,还觉得自己挺厉害。直到最近整理C++学习笔记,重新翻开这道“旧题”,才真正觉得不好意思——那版代码本质上是在用数组思维处理链表问题,完全没有吃透“逆序存储”这四个字给我们的便利。这篇就算一个迟到的重做记录:从旧代码的问题出发,把链表求和的迭代、递归和空间优化三套方案都重新推导一遍,也会把重做过程中踩过的一些边界条件坑一并写出来。如果你也在用C++刷链表题,这篇文章里的细节应该能帮你少走一半弯路。
1. 为什么一道“旧题”值得重写一遍:初版代码的三个问题
1.1 初版“能跑”代码长什么样
先说清楚我当时是怎么写的。整体思路是“先把链表变成数组,算完再把数组变回链表”。代码大概是下面这个样子:
cpp复制// 初版写法:不推荐,仅展示我当时的问题
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
vector<int> a, b;
while (l1) {
a.push_back(l1->val);
l1 = l1->next;
}
while (l2) {
b.push_back(l2->val);
l2 = l2->next;
}
int n = max(a.size(), b.size());
vector<int> res(n, 0);
int carry = 0;
for (int i = 0; i < n; i++) {
int sum = carry;
if (i < a.size()) sum += a[i];
if (i < b.size()) sum += b[i];
res[i] = sum % 10;
carry = sum / 10;
}
if (carry) res.push_back(carry);
ListNode* head = new ListNode(res[0]);
ListNode* cur = head;
for (int i = 1; i < res.size(); i++) {
cur->next = new ListNode(res[i]);
cur = cur->next;
}
return head;
}
如果只看“提交通过”这个标准,它没有任何问题。但等我对链表更熟悉之后回头读这段代码,心里就只剩一个念头:这真的是在写链表题吗?本质上它把链表遍历当成“读取数组的前置步骤”,把结果重新组织成链表时又重复造了一遍节点。整个过程里链表本身的结构特点、逐位推进的逻辑,全被消解掉了。
1.2 三个让我想删除它的理由
第一个问题是额外空间。这个方法需要两个O(n)的临时数组存原始值,再需要一个O(n)的结果数组,最后还要new出O(n)的节点。一个本来可以在常数级额外空间里做完的题,被我变成了三份线性空间的拷贝。
第二个问题是思路割裂。链表最大的特点就是“一个节点指向下一个节点”,天然支持你从头到尾逐位处理。我却先把所有节点拉平成数组,再用下标去模拟“当前位置”,等于放弃了这个数据结构自带的遍历能力。
第三个问题更实际:如果链表代表的是一个超过int范围的大数,比如有几十上百位,你把数值整体转成整数这条路就走不通。虽然我用的是vector而不是int,但我的算法结构仍然停留在“先集中取值、再统一计算”的思路上,一旦题目变形成“二进制链表求和”或者“正序存储求和”,这套结构就需要大改,完全没有复用性。
1.3 从数组思维切换到链表思维
“旧题新做”最有价值的地方,不是把代码写得更短,而是完成一次思维模型的切换:处理链表时,尽量让遍历顺序、计算顺序和结果构建顺序保持一致。这道题恰恰是练习这种思维的最佳载体,因为加法本身也是“从低位到高位、逐位相加、保留进位”,和链表从头部到尾部遍历的顺序完全重合。
也正因为如此,我后来养成了一个习惯:遇到链表题目先问自己一句,题目允许我可以从哪个方向遍历?如果需要从尾部开始,是反转链表用栈,还是直接递归?这个习惯让我后续刷C++结构体链表基本语法相关的题目时,明显没那么容易卡壳了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++链表与十进制进位的天然契合:读懂题目的隐藏信息
2.1 逆序存放不是故意刁难,恰恰是把优势递到你手里
很多初学者第一次看到题目说“数字按逆序存储”时,第一反应是“为什么要反着存?这不增加
