1. 链表专题下篇:核心算法精解
作为C++算法练习者,LeetCode Hot100的链表专题是必须攻克的硬骨头。上篇我们解决了基础链表操作后,下篇将直面五个更具挑战性的问题:相交链表、回文链表、两数相加、两两交换节点和随机链表复制。这些问题不仅考察指针操作的熟练度,更考验对特殊边界条件的处理能力。
我在刷题过程中发现,链表问题的核心难点往往不在于算法本身,而在于指针移动时的细节控制。比如处理两数相加时的进位传递,或是复制随机链表时的映射关系维护,稍有不慎就会陷入死循环或内存泄漏的泥潭。本文将结合我的踩坑经验,详解每个问题的实现要点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 相交链表问题深度剖析
2.1 双指针法的精妙之处
相交链表(LeetCode 160)要求找出两个单链表相交的起始节点。最直观的解法是用哈希表存储节点地址,但空间复杂度为O(n)。更优解是利用双指针走相同路程的数学特性:
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;
}
这个解法之所以高效,是因为两个指针分别遍历"链表A+链表B"和"链表B+链表A"的组合,最终必然在相交点相遇或同时到达NULL。时间复杂度O(m+n),空间复杂度O(1)。
关键细节:循环终止条件应同时处理相交和不相交两种情况。我曾因漏判p1/p2同时为NULL的情况导致死循环。
2.2 边界条件实战测试
实际编码时需要特别注意:
- 一个链表为空的情况
- 两个链表长度相差极大时的性能
- 相交点在头节点或尾节点的特殊情况
验证代码时应构造如下测试用例:
- 常规相交(Y型交叉)
- 完全重叠的链表
- 不相交的平行链表
- 一个链表是另一个的子序列
3. 回文链表的三种解法对比
3.1 空间换时间的哈希法
最直观的思路是将链表值存入数组,然后用双指针判断回文。但这样需要O(n)额外空间:
cpp复制bool isPalindrome(ListNode* head) {
vector<int> vals;
while (head) {
vals.push_back(head->val);
head = head->next;
}
int left = 0, right = vals.size()-1;
while (left < right) {
if (vals[left++] != vals[right--])
return false;
}
return true;
}
3.2 快慢指针的优化解
更优解法使用快慢指针找到中点,反转后半部分后比较:
cpp复制bool isPalindrome(ListNode* head) {
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;
}
这个版本空间复杂度降至O(1),但需要修改原链表结构(虽然可以在比较后恢复)。我在实际实现时经常犯的错误是反转的起始点选择不当,导致奇数长度链表处理出错。
3.3 递归解法的思维训练
递归解法虽然不推荐(空间复杂度O(n)),但能很好训练链表思维:
cpp复制ListNode* front;
bool check(ListNode* curr) {
if (curr) {
if (!check(curr->next)) return false;
if (curr->val != front->val) return false;
front = front->next;
}
return true;
}
bool isPalindrome(ListNode* head) {
front = head;
return check(head);
}
4. 两数相加的进位处理艺术
4.1 模拟竖式加法的实现
两数相加(LeetCode 2)要求将两个逆序存储的数字链表相加。核心在于处理不同长度链表和进位:
cpp复制ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode dummy(0), *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;
l1 = l1 ? l1->next : nullptr;
l2 = l2 ? l2->next : nullptr;
}
return dummy.next;
}
易错点:循环条件必须包含carry判断,否则最高位进位会被遗漏。我曾因此提交失败3次。
4.2 内存管理的注意事项
由于需要创建新节点,必须注意:
- 使用dummy节点简化头节点处理
- 在工程代码中应添加new失败的处理
- 理论上应该释放输入链表内存(但LeetCode环境下可忽略)
5. 两两交换节点的指针操作
5.1 迭代法的四步舞曲
两两交换节点(LeetCode 24)需要精确控制指针修改顺序:
cpp复制ListNode* swapPairs(ListNode* head) {
ListNode dummy(0), *prev = &dummy, *curr = head;
dummy.next = head;
while (curr && curr->next) {
ListNode *first = curr, *second = curr->next;
// 四步指针修改
prev->next = second;
first->next = second->next;
second->next = first;
// 移动指针
prev = first;
curr = first->next;
}
return dummy.next;
}
这个"四步舞曲"必须严格按顺序执行,否则会导致指针丢失。我建议在纸上画出每一步的指针变化。
5.2 递归解法的简洁之美
递归解法虽然空间复杂度O(n),但代码极其简洁:
cpp复制ListNode* swapPairs(ListNode* head) {
if (!head || !head->next) return head;
ListNode *newHead = head->next;
head->next = swapPairs(newHead->next);
newHead->next = head;
return newHead;
}
6. 随机链表复制的映射技巧
6.1 哈希表维护新旧节点关系
随机链表复制(LeetCode 138)的难点在于random指针的复制。第一遍遍历用哈希表建立映射:
cpp复制Node* copyRandomList(Node* head) {
unordered_map<Node*, Node*> oldToNew;
Node *curr = head;
while (curr) {
oldToNew[curr] = new Node(curr->val);
curr = curr->next;
}
curr = head;
while (curr) {
oldToNew[curr]->next = oldToNew[curr->next];
oldToNew[curr]->random = oldToNew[curr->random];
curr = curr->next;
}
return oldToNew[head];
}
6.2 空间复杂度O(1)的巧妙解法
更优解法将新节点插入原节点之后,建立隐式映射关系:
cpp复制Node* copyRandomList(Node* head) {
if (!head) return nullptr;
// 第一步:插入新节点
Node *curr = head;
while (curr) {
Node *newNode = new Node(curr->val);
newNode->next = curr->next;
curr->next = newNode;
curr = newNode->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;
}
这个解法需要非常谨慎的指针操作,我在第一次实现时因为第三步分离不当导致原链表被破坏。建议在关键步骤添加断言检查。
7. 链表专题的通用解题技巧
经过这五个问题的训练,我总结出链表算法的通用技巧:
- dummy节点:处理头节点可能变化的场景
- 画图分析:在纸上画出指针变化过程
- 多指针协同:如快慢指针、前后指针等
- 递归思维:将问题分解为子问题
- 边界检查:空链表、单节点、头尾操作等特殊情况
在LeetCode上提交前,务必测试以下case:
- 空输入
- 单节点链表
- 两个节点的链表
- 超大链表(测试时间限制)
- 特殊结构的链表(如环形、交叉等)
链表操作是C++程序员的基本功,这些题目虽然看似简单,但能暴露指针操作中的各种隐患。建议每个题目至少实现三种解法,并比较它们的优劣。我在面试中经常被要求在白板上写出无bug的链表代码,平时的严格训练这时就显现出价值了。
