链表经典算法题详解教程
链表经典算法题详解教程
目录
- 链表基础知识
- 删除重复节点
- 返回倒数第k个节点
- 寻找链表中间节点
- 反转链表
- 环形链表检测
- 回文链表判断
- 合并两个有序链表
链表基础知识
ListNode 结构定义
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) {}
};
常用技巧
- 双指针法:快慢指针、前后指针
- 哑节点(Dummy Node):简化边界处理
- 哈希表:用于检测重复或环
1. 删除重复节点
问题描述
删除链表中重复出现的节点,保留每个值只出现一次。
解题思路
使用哈希表记录已经出现过的节点值,遍历链表时跳过重复节点。
代码实现
class Solution {
public:ListNode* removeDuplicateNodes(ListNode* head) {// 边界情况:空链表或单节点if(head == nullptr || head->next == nullptr)return head;unordered_map<int, int> mp; // 哈希表记录出现过的值ListNode* cur = head;ListNode* next = head->next;mp[cur->val] = 1; // 标记头节点while(next) {if(mp.find(next->val) == mp.end()) {// 未出现过的值,保留该节点mp[next->val] = 1;cur->next = next;cur = next;next = next->next;} else {// 重复值,跳过该节点next = next->next;}}cur->next = next; // 处理尾节点return head;}
};
复杂度分析
- 时间复杂度:O(n),遍历链表一次
- 空间复杂度:O(n),哈希表存储节点值
2. 返回倒数第k个节点
问题描述
返回链表的倒数第 cnt 个节点。
解题思路
使用快慢双指针:
- 快指针先走 cnt 步
- 快慢指针同时移动,直到快指针到达末尾
- 此时慢指针指向倒数第 cnt 个节点
代码实现
class Solution {
public:ListNode* trainingPlan(ListNode* head, int cnt) {ListNode *fast = head;ListNode *slow = head;// 快指针先走 cnt 步while(cnt--) {if(fast == nullptr)return nullptr;fast = fast->next;}// 快慢指针同时移动while(fast) {fast = fast->next;slow = slow->next;}return slow;}
};
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
3. 寻找链表中间节点
问题描述
返回链表的中间节点。若有两个中间节点,返回第二个。
解题思路
使用快慢指针:
- 快指针每次走2步
- 慢指针每次走1步
- 当快指针到达末尾时,慢指针正好在中间
代码实现
class Solution {
public:ListNode* middleNode(ListNode* head) {ListNode *fast = head;ListNode *slow = head;while(fast && fast->next) {fast = fast->next->next; // 快指针走2步slow = slow->next; // 慢指针走1步}return slow;}
};
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
4. 反转链表
问题描述
反转一个单链表。
解题思路
使用三指针法:
- pre:前一个节点
- cur:当前节点
- next:下一个节点
通过改变指针方向实现反转。
代码实现
class Solution {
public:ListNode* trainningPlan(ListNode* head) {// 边界情况if(head == nullptr || head->next == nullptr)return head;ListNode* pre = nullptr;ListNode* cur = head;ListNode* next = head->next;while(next) {cur->next = pre; // 反转指针pre = cur; // 前移cur = next;next = next->next;}cur->next = pre; // 处理最后一个节点return cur; // cur 是新的头节点}
};
图解过程
原链表: 1 -> 2 -> 3 -> 4 -> null
步骤1: null <- 1 2 -> 3 -> 4 -> null
步骤2: null <- 1 <- 2 3 -> 4 -> null
步骤3: null <- 1 <- 2 <- 3 4 -> null
步骤4: null <- 1 <- 2 <- 3 <- 4
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
5. 环形链表检测
问题描述
检测链表中是否有环,并找到环的入口节点。
解题思路
使用Floyd判圈算法(快慢指针):
- 快慢指针同时出发,快指针每次走2步,慢指针走1步
- 如果有环,两指针必定相遇
- 相遇后,将快指针重置到头节点
- 两指针以相同速度前进,再次相遇点即为环的入口
代码实现(修正版)
class Solution {
public:ListNode* detectCycle(ListNode *head) {ListNode *fast = head;ListNode *slow = head;// 第一阶段:检测是否有环while(fast && fast->next) {slow = slow->next;fast = fast->next->next;if(slow == fast) {// 第二阶段:找到环的入口fast = head;while(slow != fast) {slow = slow->next;fast = fast->next;}return slow; // 返回环的入口节点}}return nullptr; // 无环}
};
原理说明
设链表头到环入口距离为 a,环入口到相遇点距离为 b,相遇点到环入口距离为 c。
- 慢指针走的距离:a + b
- 快指针走的距离:a + b + c + b = a + 2b + c
- 因为快指针速度是慢指针的2倍:2(a + b) = a + 2b + c
- 化简得:a = c
因此,从头节点和相遇点同时出发,相遇点即为环入口。
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
6. 回文链表判断
问题描述
判断链表是否为回文结构。
解题思路
- 使用快慢指针找到链表中点
- 反转后半部分链表
- 比较前半部分和反转后的后半部分
代码实现
class Solution {
public:bool isPalindrome(ListNode* head) {// 1. 找到中间节点ListNode* fast = head;ListNode* slow = head;while(fast && fast->next) {fast = fast->next->next;slow = slow->next;}// 2. 反转后半部分链表ListNode* pre = nullptr;ListNode* cur = slow;ListNode* next = slow->next;while(next) {cur->next = pre;pre = cur;cur = next;next = next->next;}cur->next = pre;// 3. 比较前后两部分while(cur && head) {if(cur->val == head->val) {cur = cur->next;head = head->next;} else {return false;}}return true;}
};
示例
链表: 1 -> 2 -> 3 -> 2 -> 1
中点: 3
后半部分反转: 1 -> 2 -> 3 <- 2 <- 1
比较: 1==1, 2==2, 结果为 true
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
7. 合并两个有序链表
问题描述
将两个升序链表合并为一个新的升序链表。
方法一:迭代法
解题思路
使用哑节点简化操作,依次比较两个链表的节点值,将较小的节点连接到结果链表。
代码实现
class Solution {
public:ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {// 边界情况if (list1 == nullptr || list2 == nullptr)return list1 == nullptr ? list2 : list1;ListNode dummy(0); // 哑节点ListNode* list3 = &dummy; // 游标指针ListNode* res = list3; // 记录结果链表头while (list1 && list2) {if (list1->val < list2->val) {list3->next = list1;list1 = list1->next;} else {list3->next = list2;list2 = list2->next;}list3 = list3->next;}// 连接剩余节点list3->next = (list2 == nullptr) ? list1 : list2;return res->next;}
};
复杂度分析
- 时间复杂度:O(m + n)
- 空间复杂度:O(1)
方法二:递归法
解题思路
递归比较两个链表的头节点,将较小的节点与剩余部分的合并结果连接。
代码实现
class Solution {
public:ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {// 递归终止条件if (list1 == nullptr || list2 == nullptr)return list1 == nullptr ? list2 : list1;if(list1->val < list2->val) {list1->next = mergeTwoLists(list1->next, list2);return list1;} else {list2->next = mergeTwoLists(list1, list2->next);return list2;}}
};
复杂度分析
- 时间复杂度:O(m + n)
- 空间复杂度:O(m + n),递归栈空间
总结
链表题目常用技巧
技巧 | 适用场景 | 代表题目 |
---|---|---|
快慢指针 | 寻找中点、检测环、倒数第k个节点 | 中间节点、环形链表 |
哈希表 | 检测重复、记录访问 | 删除重复节点 |
双指针 | 反转、合并 | 反转链表、合并链表 |
哑节点 | 简化边界处理 | 合并链表 |
递归 | 分解子问题 | 合并链表、反转链表 |
注意事项
- 边界条件:空链表、单节点链表
- 指针操作:避免断链、空指针访问
- 空间优化:尽量使用 O(1) 空间
- 代码规范:变量命名清晰,逻辑简洁
练习建议
- 先理解算法原理,画图模拟过程
- 手写代码,注意边界条件
- 多做类似题目,总结规律
- 尝试优化空间复杂度