支持作者新书,点击京东购买《Yocto项目实战教程:高效定制嵌入式Linux系统》
链表作为数据结构的重要基础,几乎是各类算法题中的高频考点。掌握链表的常用操作与典型题型,有助于深刻理解指针操作、递归与迭代思想,并提升综合解决问题的能力。本文系统梳理了链表常见的典型题型,配合完整代码与解题思路,涵盖从单链表、双向链表到循环链表的多样操作,帮助读者建立系统的链表算法知识体系。
一、链表基本操作与核心概念
链表分为单向链表、双向链表和循环链表。最常用的是单向链表,其节点结构如下:
struct ListNode {
int val;
ListNode* next;
ListNode(int x): val(x), next(nullptr) {}
};
核心操作包括:
- 插入、删除、查找
- 反转链表
- 查找中间节点、倒数第k个节点
- 合并、分割、去重
二、链表高频题型与详细解析
1. 反转单链表
题目描述
给定一个单链表的头节点 head
,请反转该链表,并返回反转后的头节点。
核心思路
采用迭代的方式,依次将当前节点的 next 指向前一个节点。
完整代码
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* next_temp = curr->next;
curr->next = prev;
prev = curr;
curr = next_temp;
}
return prev;
}
要点讲解
- 只需 O(1) 额外空间,原地修改链表指针。
- 常考变种:递归实现反转链表。
2. 链表中环的检测与入口节点
题目描述
判断链表是否有环;如有,返回入环的第一个节点。
解题思路
- 使用快慢指针(Floyd判圈法)。
- 第一次相遇后,快指针回到头节点,二者以相同速度前进,再次相遇即为入环点。
完整代码
ListNode* detectCycle(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) { // 有环
ListNode* p = head;
while (p != slow) {
p = p->next;
slow = slow->next;
}
return p;
}
}
return nullptr;
}
要点讲解
- 掌握快慢指针的巧妙应用。
- 理解入环点距离的数学推导。
3. 删除链表中倒数第 k 个节点
题目描述
给定单链表的头节点 head
,删除链表中的倒数第 k 个节点,并返回头节点。
解题思路
- 使用双指针法,第一个指针先前进 k 步,然后两个指针同步前进。
- 前指针到达末尾时,后指针即为待删除节点的前驱。
完整代码
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode dummy(0);
dummy.next = head;
ListNode* first = &dummy;
ListNode* second = &dummy;
for (int i = 0; i <= n; ++i)
first = first->next;
while (first) {
first = first->next;
second = second->next;
}
second->next = second->next->next;
return dummy.next;
}
要点讲解
- 使用虚拟头节点,统一删除逻辑,避免头节点特判。
4. 合并两个有序链表
题目描述
将两个升序链表合并为一个新的升序链表,并返回新链表的头节点。
解题思路
- 采用双指针遍历,逐步比较插入。
完整代码
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode dummy(0);
ListNode* curr = &dummy;
while (l1 && l2) {
if (l1->val < l2->val) {
curr->next = l1;
l1 = l1->next;
} else {
curr->next = l2;
l2 = l2->next;
}
curr = curr->next;
}
curr->next = l1 ? l1 : l2;
return dummy.next;
}
要点讲解
- 可递归实现,但迭代效率更高且无栈溢出风险。
5. 找到链表的中间节点
题目描述
给定单链表头节点,返回中间节点(偶数时返回第二个)。
解题思路
- 依旧使用快慢指针,快指针每次走两步,慢指针每次走一步。
完整代码
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
要点讲解
- 通过快慢指针一次遍历找中点。
6. 两两交换链表中的节点
题目描述
每两个相邻节点交换,返回新链表头。
解题思路
- 画图辅助理解,使用递归或迭代实现。
完整代码
ListNode* swapPairs(ListNode* head) {
if (!head || !head->next) return head;
ListNode* newHead = head->next;
head->next = swapPairs(newHead->next);
newHead->next = head;
return newHead;
}
要点讲解
- 递归写法简洁明了,亦可用迭代。
7. 判断回文链表
题目描述
判断链表是否是回文结构。
解题思路
- 找到中间节点,将后半段反转,对比前后两段。
完整代码
bool isPalindrome(ListNode* head) {
if (!head || !head->next) return true;
ListNode* slow = head;
ListNode* fast = head;
while (fast->next && fast->next->next) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* second = reverseList(slow->next);
ListNode* p1 = head, *p2 = second;
while (p2) {
if (p1->val != p2->val) return false;
p1 = p1->next; p2 = p2->next;
}
return true;
}
注:
reverseList
参考前文反转链表实现。
8. 链表求交点
题目描述
给定两个单链表,找出它们的第一个公共节点(可能有环)。
解题思路
- 不带环:双指针法,走到头就切到另一条链表头,最终相遇。
- 带环:需先判断环并找入环点。
完整代码
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
if (!headA || !headB) return nullptr;
ListNode* pA = headA;
ListNode* pB = headB;
while (pA != pB) {
pA = pA ? pA->next : headB;
pB = pB ? pB->next : headA;
}
return pA;
}
要点讲解
- 充分利用指针的错位遍历,空间复杂度 O(1)。
9. 链表排序(归并排序)
题目描述
对链表进行升序排序。
解题思路
- 链表适合归并排序(拆分+合并)。
- 拆分可用快慢指针找中点。
完整代码
ListNode* sortList(ListNode* head) {
if (!head || !head->next) return head;
// 找到中点
ListNode* slow = head, *fast = head->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* mid = slow->next;
slow->next = nullptr;
ListNode* left = sortList(head);
ListNode* right = sortList(mid);
return mergeTwoLists(left, right);
}
注:
mergeTwoLists
见前文。
10. 移除链表元素
题目描述
移除所有值等于给定值的节点。
完整代码
ListNode* removeElements(ListNode* head, int val) {
ListNode dummy(0);
dummy.next = head;
ListNode* curr = &dummy;
while (curr->next) {
if (curr->next->val == val)
curr->next = curr->next->next;
else
curr = curr->next;
}
return dummy.next;
}
要点讲解
- 虚拟头节点极大简化头部节点删除逻辑。
三、经典链表题目设计与知识点小结
题型 | 主要考察知识点 | 典型代码逻辑 |
---|---|---|
反转链表 | 指针操作,递归迭代 | prev、curr指针移动 |
检测环/入口 | 快慢指针,数学归纳 | Floyd判圈法 |
删除倒数第k个节点 | 双指针,虚拟头结点 | 前后指针错位遍历 |
合并有序链表 | 归并思想,指针比较 | 新链表串联 |
找中点 | 快慢指针 | 一次遍历 |
两两交换节点 | 指针操作,递归思维 | 局部反转 |
判断回文 | 链表反转,比较 | 快慢指针+原地反转 |
求交点 | 指针错位遍历 | 双链表同步遍历 |
链表排序 | 归并排序 | 快慢指针分割+合并 |
移除指定元素 | 虚拟头节点,遍历 | 指针跳过目标节点 |
四、刷题建议与思考提升
- 画图理解:链表题建议先画指针示意图,梳理操作流程。
- 掌握虚拟头节点用法:几乎所有涉及节点删除/插入题型均可用 dummy 节点简化逻辑。
- 快慢指针、双指针思想:多体会其妙用,解决位置相关问题尤其有效。
- 递归与迭代切换:链表天然适合递归,但实际开发中多用迭代以节省栈空间。
- 掌握链表高阶题型:如带随机指针的复制、k个一组翻转等,有更高提升空间。
五、结语
链表题目虽以指针操作为主,但背后考察的是细致的思维与对数据结构本质的把握。每一道题都值得深入研究和多次实践,只有扎实掌握指针的每一次变动和链表的每一次重组,才能在实际开发与算法挑战中游刃有余。
支持作者新书,点击京东购买《Yocto项目实战教程:高效定制嵌入式Linux系统》