1. 链表OJ题解概述
作为一名长期奋战在数据结构教学一线的讲师,我深知链表是数据结构中最基础也最考验编程功底的环节。今天我将通过7道精选OJ题目,带大家深入理解链表的常见操作技巧。这些题目覆盖了快慢指针、链表反转、环检测等核心知识点,每道题都配有详细图解和代码实现。
链表操作的精髓在于指针的灵活运用。在实际工程中,链表广泛应用于内存管理、文件系统等领域。掌握这些基础算法不仅能帮助通过面试,更能培养严谨的编程思维。建议读者在阅读时准备好纸笔,跟着图解一步步推演指针变化。
2. 快慢指针的经典应用
2.1 寻找链表中点(LeetCode 876)
问题场景:在数据流处理中,经常需要快速定位链表的中间节点,比如合并两个有序链表时。
算法思路:
- 快指针每次走两步,慢指针每次走一步
- 当快指针到达末尾时,慢指针正好在中点
- 对于偶数节点,返回第二个中间节点
关键细节:
c复制struct ListNode* middleNode(struct ListNode* head) {
struct ListNode *fast = head, *slow = head;
while (fast && fast->next) { // 注意终止条件
fast = fast->next->next; // 快指针步进2
slow = slow->next; // 慢指针步进1
}
return slow;
}
复杂度分析:
- 时间复杂度:O(n),只需遍历一次
- 空间复杂度:O(1),仅使用常数空间
实际调试中发现,fast->next的判断不能省略,否则可能访问空指针的next成员
2.2 倒数第k个节点(面试题 02.02)
工程应用:操作系统调度算法中经常需要访问链表末尾元素。
优化技巧:
- 快指针先走k步
- 然后快慢指针同步前进
- 快指针到末尾时,慢指针即指向目标
c复制int kthToLast(struct ListNode* head, int k) {
struct ListNode *fast = head, *slow = head;
while (k--) fast = fast->next; // 快指针先行k步
while (fast) { // 同步前进
fast = fast->next;
slow = slow->next;
}
return slow->val;
}
边界处理:
- k大于链表长度时,初始循环可能越界
- 实际编码时应增加长度校验
3. 链表高级操作
3.1 回文链表判断(牛客网)
算法步骤:
- 快慢指针找中点
- 反转后半部分链表
- 前后半部逐个比较
c复制bool chkPalindrome(ListNode* A) {
struct ListNode* mid = middleNode(A);
struct ListNode* rmid = reverseList(mid);
while (A && rmid) {
if (A->val != rmid->val)
return false;
A = A->next;
rmid = rmid->next;
}
return true;
}
反转链表的实现技巧:
c复制struct ListNode* reverseList(struct ListNode* head) {
struct ListNode *cur = head, *newhead = NULL;
while (cur) {
struct ListNode *next = cur->next;
cur->next = newhead; // 头插法
newhead = cur;
cur = next;
}
return newhead;
}
3.2 相交链表(LeetCode 160)
解题关键:
- 先遍历得到两个链表的长度
- 长链表指针先走长度差步
- 然后同步前进直到相遇
c复制struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
// 计算长度差
int lenA = 1, lenB = 1;
struct ListNode *pA = headA, *pB = headB;
while (pA->next) { pA = pA->next; lenA++; }
while (pB->next) { pB = pB->next; lenB++; }
if (pA != pB) return NULL; // 尾节点不同
// 对齐起点
int gap = abs(lenA - lenB);
struct ListNode *longList = lenA > lenB ? headA : headB;
struct ListNode *shortList = lenA > lenB ? headB : headA;
while (gap--) longList = longList->next;
// 同步前进找交点
while (longList != shortList) {
longList = longList->next;
shortList = shortList->next;
}
return longList;
}
4. 环形链表专题
4.1 环检测(LeetCode 141)
Floyd判圈算法:
- 快指针每次两步,慢指针每次一步
- 若相遇则有环,否则无环
c复制bool hasCycle(struct ListNode *head) {
struct ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (fast == slow) return true;
}
return false;
}
4.2 环入口定位(LeetCode 142)
数学证明:
- 设头节点到入口距离为a
- 入口到相遇点距离为b
- 相遇点到入口距离为c
- 快指针路程 = a + n(b+c) + b
- 慢指针路程 = a + b
- 由2(a+b)=a+(n+1)b+nc得a=c
c复制struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *meet = determineCycle(head);
if (!meet) return NULL;
while (head != meet) {
head = head->next;
meet = meet->next;
}
return head;
}
5. 复杂链表复制(LeetCode 138)
三步法解决方案:
- 创建镜像节点并插入原节点后
- 设置random指针
- 分离新旧链表
c复制struct Node* copyRandomList(struct Node* head) {
if (!head) return NULL;
// 第一步:创建镜像节点
struct Node *cur = head;
while (cur) {
struct Node *copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
copy->next = cur->next;
cur->next = copy;
cur = copy->next;
}
// 第二步:设置random
cur = head;
while (cur) {
if (cur->random)
cur->next->random = cur->random->next;
cur = cur->next->next;
}
// 第三步:分离链表
cur = head;
struct Node *newHead = head->next;
while (cur->next) {
struct Node *temp = cur->next;
cur->next = cur->next->next;
cur = temp;
}
return newHead;
}
6. 链表操作实战技巧
6.1 调试技巧
- 画图辅助理解指针变化
- 使用printf打印关键节点地址
- 添加边界条件检查
6.2 常见错误
- 忘记处理空指针
- 指针操作顺序错误
- 内存泄漏(特别是带malloc的代码)
6.3 性能优化
- 减少不必要的遍历
- 合理利用哨兵节点
- 注意缓存局部性
在实际项目开发中,我经常使用这些链表技巧来处理日志分析、任务调度等场景。比如在实现一个简单的任务队列时,快慢指针算法可以帮助快速定位需要优先处理的任务节点。