C++ 算法题中链表的操作技巧总结 链表 模拟 力扣 2. 两数相加 题解 每日一题
文章目录
- 算法题中链表常用的技巧和操作总结
- 常用技巧
- 链表核心操作
- 题目描述
- 为什么这道题值得你花几分钟看懂?
- 题目解析
- 算法原理
- 代码实现
- 繁琐版本代码
- 简洁版本代码
- 代码解析
- 时间复杂度与空间复杂度分析
- 总结
- 下题预告


算法题中链表常用的技巧和操作总结
常用技巧
1. 画图梳理思路
这不仅是链表问题的核心技巧,更是解决所有算法题的通用思路(遇事不决,画图解决)。链表的节点关系依赖指针串联,抽象的指针移动很容易让我们混淆。动手画出节点、指针以及每一步的操作过程,能快速拆解复杂逻辑,把混乱的思路直观化,帮你精准定位问题关键点。
2. 引入虚拟头节点
算法题中给出的链表大多是无哨兵节点的单链表,操作头节点时需要单独处理空指针、边界节点等特殊情况,容易遗漏细节。引入虚拟头节点(哨兵节点)后,无需单独判断头节点边界,无论是删除、插入还是遍历操作,都能统一逻辑,极大降低代码复杂度,让操作更简洁高效。
3. 合理分配空间,灵活定义节点
解题时不必局限于原链表的节点结构,可根据需求大胆定义新节点或辅助指针。适当的空间开销能简化逻辑,避免在原链表上反复修改导致的指针混乱,尤其在合并、拆分链表等场景中,灵活定义节点能让思路更清晰,代码更易维护。
4. 快慢双指针法
这是链表问题的“万能工具”之一,核心思路是让两个指针以不同速度遍历链表。它能高效解决多种经典问题:判断链表是否有环、找到环形链表的入口节点、寻找链表的中间节点、倒数第n个节点等,用这种方法能将时间复杂度优化至O(n),避免暴力遍历的低效问题。
链表核心操作
算法题对链表的考察,本质上都是围绕以下三个核心操作的组合与延伸:
1. 创建新节点
这是链表操作的基础,根据题目给定的值初始化节点,并将节点的指针域置空(避免野指针)。创建节点时需注意内存分配合理性,确保后续指针操作的安全性。
2. 尾插法
将新节点插入链表尾部,核心是找到当前链表的尾节点,再将尾节点的指针指向新节点。尾插法能保持链表元素的插入顺序,常用于构建有序链表或合并多个链表时的元素拼接。
3. 头插法
将新节点插入链表头部(或虚拟头节点之后),核心是让新节点的指针指向当前头节点,再更新头节点为新节点。头插法操作高效(时间复杂度O(1)),常用于链表反转、逆序构建链表等场景。
题目描述
题目链接:力扣 2. 两数相加
题目描述:

示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807
提示:
每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
为什么这道题值得你花几分钟看懂?
这道题是链表操作的“入门必修课”,是理解链表遍历、节点创建和进位处理的经典案例。花上几分钟时间,我们能够收获:
-
掌握链表的基本操作范式:包括虚拟头节点的使用、指针移动、新节点创建等核心技巧,这些技巧在几乎所有链表问题中都会用到。
-
理解模拟加法的完整流程:从低位到高位的加法运算、进位处理、不同长度数字的对齐方式,这些逻辑可迁移到字符串加法、大数运算等问题中。
-
学习代码简化的思维方式:从冗长的分支判断到简洁的统一逻辑,掌握如何通过抽象共性条件来优化代码结构,提升可读性和可维护性。
这道题看似简单,但能让我们建立对链表操作的基本认知又或是回顾链表的操作,为后续解决更复杂的链表问题(如反转、合并、环检测等)打下坚实基础。
题目解析
这道题的核心是模拟我们日常做加法的过程,但有两个关键特点需要注意:
-
数字的存储方式:链表中的数字是逆序存储的,例如
342存储为2 -> 4 -> 3。这恰好方便我们从低位(个位)开始相加,与加法的运算顺序完全一致。 -
进位处理:两个数字相加可能产生进位(例如
6 + 8 = 14,需要向高位进 1),且进位可能持续传递(例如999 + 1 = 1000)。
因此,解题的基本思路是:
- 同时遍历两个链表,逐位相加对应节点的值
- 记录每一步的进位,并参与到下一位的计算中
- 处理两个链表长度不同的情况
- 处理最后可能剩余的进位
算法原理
模拟加法过程
我们可以按照以下步骤模拟两数相加的过程:
-
初始化:创建一个虚拟头节点
head(简化边界处理),以及用于遍历链表的指针和记录进位的变量。

-
遍历链表:同时遍历两个输入链表,直到两个链表都遍历完毕且没有剩余进位。
-
逐位计算:
- 取出当前节点的值(若链表已遍历完毕,则取值为 0)
- 计算当前位的总和:
当前位之和 = l1的值 + l2的值 + 进位 - 计算当前位的结果(
总和 % 10)和新的进位(总和 / 10) - 创建新节点存储当前位的结果,并移动指针
-
处理剩余进位:若遍历结束后仍有进位,需创建新节点存储。
代码实现
繁琐版本的问题
我在最初的实现过程中将可能会分的情况都分开处理:
- 两个链表都有节点的情况
- 只有 l1 有节点的情况
- 只有 l2 有节点的情况
- 处理剩余进位的情况
这种方式虽然直观,但存在大量重复代码,且逻辑分散,容易出错。
优化过程
通过观察我们可以发现,各种情况的核心计算逻辑是一致的(都是“当前值 + 进位”),差异仅在于是否从链表中取值。因此可以:
- 用一个循环条件涵盖所有需要处理的情况(
cur1 || cur2 || t) - 在循环内部统一处理取值逻辑(有节点则取值并移动指针,否则取 0)
- 统一计算当前位结果和进位,无需分情况判断
这种方式将分散的逻辑集中,大大减少了代码量,同时保持了逻辑的清晰性。
繁琐版本代码
/*** 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) {ListNode* head = new ListNode(-1); // 虚拟头节点ListNode* cur1 = l1;ListNode* cur2 = l2;ListNode* tmp = head; // 用于构建结果链表的指针int t = 0; // 进位// 处理两个链表都有节点的部分while (cur1 != nullptr && cur2 != nullptr) {int sum = cur1->val + cur2->val + t;if (sum >= 10) {t = sum / 10;sum %= 10;} else {t = 0;}tmp->next = new ListNode(sum);tmp = tmp->next;cur1 = cur1->next;cur2 = cur2->next;}// 处理 l1 剩余的节点while (cur1 != nullptr) {int sum = cur1->val + t;if (sum >= 10) {t = sum / 10;sum %= 10;} else {t = 0;}tmp->next = new ListNode(sum);tmp = tmp->next;cur1 = cur1->next;}// 处理 l2 剩余的节点while (cur2 != nullptr) {int sum = cur2->val + t;if (sum >= 10) {t = sum / 10;sum %= 10;} else {t = 0;}tmp->next = new ListNode(sum);tmp = tmp->next;cur2 = cur2->next;}// 处理最后剩余的进位if (t != 0) {tmp->next = new ListNode(t);tmp = tmp->next;}return head->next;}
};
简洁版本代码
/*** 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) {ListNode* head = new ListNode(-1); // 虚拟头节点ListNode* cur1 = l1;ListNode* cur2 = l2;ListNode* tmp = head; // 用于构建结果链表的指针int t = 0; // 进位// 循环条件:只要有一个链表未结束,或仍有进位,就继续处理while (cur1 || cur2 || t) {// 累加当前节点的值(若节点存在)if (cur1) {t += cur1->val;cur1 = cur1->next; // 移动指针}if (cur2) {t += cur2->val;cur2 = cur2->next; // 移动指针}// 创建当前位的节点(存储 t 的个位)tmp->next = new ListNode(t % 10);tmp = tmp->next; // 移动指针// 更新进位(t 的十位)t /= 10;}// 面试时注意释放虚拟头节点的内存ListNode* result = head->next;delete head;return result;}
};
代码解析
-
虚拟头节点:
new ListNode(-1)创建一个虚拟头节点,避免了处理头节点为空的特殊情况,简化了代码逻辑。 -
循环条件:
while (cur1 || cur2 || t)确保所有情况都被处理:cur1不为空:l1 还有未处理的节点cur2不为空:l2 还有未处理的节点t不为 0:存在未处理的进位
-
取值逻辑:
- 若链表指针不为空,则累加其值并移动指针
- 若指针为空,则相当于累加 0(不做处理)
-
结果计算:
- 当前位的结果为
t % 10(取个位) - 新的进位为
t / 10(取十位)
- 当前位的结果为
-
内存管理:
- 笔试时可以直接返回
head->next,无需释放虚拟头节点 - 面试时需要手动释放虚拟头节点的内存(
delete head),体现对内存管理的重视
- 笔试时可以直接返回
时间复杂度与空间复杂度分析
-
时间复杂度:O(max(m, n)),其中 m 和 n 分别是两个链表的长度。循环执行次数取决于较长的链表长度和可能的进位,最多为 max(m, n) + 1(处理最后一个进位)。
-
空间复杂度:O(max(m, n)),结果链表的长度最多为 max(m, n) + 1(当有进位时),因此需要额外的 O(max(m, n)) 空间存储结果节点。
总结
-
核心思路:模拟加法运算过程,从低位到高位逐位计算,同时处理进位,利用链表逆序存储的特点简化运算顺序。
-
关键技巧:
- 使用虚拟头节点简化边界处理
- 统一循环条件涵盖所有情况(链表未结束或有进位)
- 抽象共性逻辑,减少分支判断
-
面试注意:
- 清晰讲解模拟加法的过程,包括进位处理
- 说明虚拟头节点的作用
- 注意内存释放,体现良好的编程习惯
这道题虽然简单,但包含了链表操作的核心技巧,掌握这些技巧对于解决更复杂的链表问题至关重要。
下题预告
接下来,我们将继续深耕 链表操作!下一道题,我们就聚焦经典进阶题 24. 两两交换链表中的节点,不仅帮大家搞定这道题的迭代与递归两种核心解题思路,更会借着这道题,和大家一起深入拆解链表节点交换的关键技巧(比如虚拟头节点的进阶使用、多指针联动、边界条件处理等),进一步强化链表操作的实战能力,为后续解决更复杂的链表重组问题奠基~
感兴趣的朋友记得持续关注,咱们一起攻克链表这块“高频考点”!
Doro 带着小花🌸来啦!🌸奖励🌸看到这里的你!如果这篇内容帮你理清了两数相加的解题逻辑,或是让你对链表操作的代码优化有了更清晰的认识,别忘了点赞支持呀!把它收藏起来,以后复习这类问题时翻出来,就能快速回忆起关键细节~关注这个博主,他会持续更新算法系列内容,有什么疑问随时讨论!

