1. 项目概述
作为一名长期奋战在算法竞赛一线的C++开发者,我决定系统性地整理LeetCode Hot100中链表专题的解题笔记。链表作为数据结构中的常青树,在面试中出现的频率居高不下。本专题将聚焦五个经典问题:相交链表、回文链表、两数相加、两两交换链表节点以及随机链表的复制。这些题目不仅考察对链表基本操作的掌握程度,更是检验开发者对指针运用和边界条件处理能力的试金石。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心算法解析
2.1 相交链表判定(LeetCode 160)
判断两个链表是否相交的经典解法是双指针法。其核心思路是让两个指针分别遍历两个链表,当到达尾部时切换到另一个链表的头部继续遍历。如果存在交点,两个指针必定会在交点处相遇。
cpp复制ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *p1 = headA, *p2 = headB;
while (p1 != p2) {
p1 = p1 ? p1->next : headB;
p2 = p2 ? p2->next : headA;
}
return p1;
}
关键点:时间复杂度O(m+n),空间复杂度O(1)。注意处理不相交的情况,此时两个指针会同时到达nullptr。
2.2 回文链表验证(LeetCode 234)
验证链表是否为回文结构的优化解法包含三个步骤:
- 使用快慢指针找到链表中点
- 反转后半部分链表
- 比较前后两部分是否相同
cpp复制bool isPalindrome(ListNode* head) {
if (!head || !head->next) return true;
// 找中点
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
// 反转后半部分
ListNode *prev = nullptr, *curr = slow;
while (curr) {
ListNode *next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
// 比较前后部分
ListNode *p1 = head, *p2 = prev;
while (p2) {
if (p1->val != p2->val) return false;
p1 = p1->next;
p2 = p2->next;
}
return true;
}
注意事项:奇数个节点时,中间节点不需要参与比较。反转链表时要保存next指针,否则会丢失后续节点。
2.3 两数相加(LeetCode 2)
处理两个链表表示的数字相加时,需要注意:
- 从最低位开始逐位相加
- 处理进位情况
- 最后检查是否还有未处理的进位
cpp复制ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode dummy(0);
ListNode *curr = &dummy;
int carry = 0;
while (l1 || l2 || carry) {
int sum = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry;
carry = sum / 10;
curr->next = new ListNode(sum % 10);
curr = curr->next;
if (l1) l1 = l1->next;
if (l2) l2 = l2->next;
}
return dummy.next;
}
技巧:使用哑节点(dummy node)可以简化头节点处理逻辑。注意循环条件包含carry不为0的情况,防止最高位进位丢失。
3. 进阶链表操作
3.1 两两交换节点(LeetCode 24)
交换相邻节点需要仔细处理指针关系。推荐使用递归和迭代两种解法:
迭代法:
cpp复制ListNode* swapPairs(ListNode* head) {
ListNode dummy(0);
dummy.next = head;
ListNode *prev = &dummy;
while (prev->next && prev->next->next) {
ListNode *first = prev->next;
ListNode *second = first->next;
// 交换节点
first->next = second->next;
second->next = first;
prev->next = second;
// 移动prev指针
prev = first;
}
return dummy.next;
}
递归法:
cpp复制ListNode* swapPairs(ListNode* head) {
if (!head || !head->next) return head;
ListNode *first = head;
ListNode *second = head->next;
first->next = swapPairs(second->next);
second->next = first;
return second;
}
对比分析:迭代法空间复杂度O(1),递归法空间复杂度O(n)。面试时建议先给出迭代解法,再补充递归解法展示思维灵活性。
3.2 复制带随机指针的链表(LeetCode 138)
这道题的难点在于如何处理随机指针的复制。最优解法分为三步:
- 在原节点后插入复制节点
- 设置复制节点的random指针
- 分离两个链表
cpp复制Node* copyRandomList(Node* head) {
if (!head) return nullptr;
// 第一步:插入复制节点
Node *curr = head;
while (curr) {
Node *copy = new Node(curr->val);
copy->next = curr->next;
curr->next = copy;
curr = copy->next;
}
// 第二步:设置random指针
curr = head;
while (curr) {
if (curr->random) {
curr->next->random = curr->random->next;
}
curr = curr->next->next;
}
// 第三步:分离链表
Node *newHead = head->next;
curr = head;
while (curr) {
Node *temp = curr->next;
curr->next = temp->next;
if (temp->next) {
temp->next = temp->next->next;
}
curr = curr->next;
}
return newHead;
}
常见错误:分离链表时没有正确处理原链表的next指针,导致原链表结构被破坏。建议画图辅助理解指针变化过程。
4. 链表专题总结
通过这五个经典题目的系统训练,我总结了链表操作的几个核心要点:
-
指针操作四要素:
- 当前节点指针
- 前驱节点指针
- 后继节点指针
- 临时保存指针
-
边界条件检查清单:
- 空链表处理
- 单节点链表
- 头节点/尾节点特殊情况
- 奇数/偶数长度差异
-
调试技巧:
- 打印链表可视化(建议重载<<运算符)
- 使用gdb的print命令查看指针地址
- 在纸上画出指针变化过程
-
性能优化方向:
- 尽量减少不必要的遍历
- 合理使用哑节点简化逻辑
- 考虑递归转迭代降低空间复杂度
在实际面试中,建议先明确问题要求,和面试官确认边界条件,然后从暴力解法开始逐步优化。链表问题通常有多个解法,展示不同的解题思路能体现思维广度。
