一、题目是啥?一句话说清
给定一个单链表,找到并返回链表的中间节点。如果有两个中间节点(链表长度为偶数),返回第二个中间节点。
示例:
-
输入:1 → 2 → 3 → 4 → 5
-
输出:节点3
-
输入:1 → 2 → 3 → 4 → 5 → 6
-
输出:节点4(第二个中间节点)
二、解题核心
使用快慢指针法:快指针每次走两步,慢指针每次走一步。当快指针到达链表末尾时,慢指针正好在中间位置。
这就像两个人赛跑,一个人跑得快(每次两步),一个人跑得慢(每次一步)。当快的人跑到终点时,慢的人正好在中间位置。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 快慢指针的速度差
- 是什么:快指针每次移动两个节点,慢指针每次移动一个节点。
- 为什么重要:这种速度差确保了当快指针到达末尾时,慢指针正好在中间位置。
2. 循环终止条件
- 是什么:循环继续的条件是快指针不为空且快指针的下一个节点不为空。
- 为什么重要:这确保了快指针可以安全地移动两步,避免空指针异常。
3. 处理偶数长度链表
- 是什么:当链表长度为偶数时,有两个中间节点,题目要求返回第二个中间节点。
- 为什么重要:快慢指针法自然满足这个要求,因为当快指针无法继续移动时,慢指针会停在第二个中间节点。
四、看图理解流程(通俗理解版本)
情况1:奇数长度链表 1 → 2 → 3 → 4 → 5
- 初始状态:快指针和慢指针都指向节点1。
- 第一步:
- 快指针移动两步:从1到3
- 慢指针移动一步:从1到2
- 第二步:
- 快指针移动两步:从3到5
- 慢指针移动一步:从2到3
- 结束:快指针到达末尾(5),慢指针在中间节点3。
情况2:偶数长度链表 1 → 2 → 3 → 4 → 5 → 6
- 初始状态:快指针和慢指针都指向节点1。
- 第一步:
- 快指针移动两步:从1到3
- 慢指针移动一步:从1到2
- 第二步:
- 快指针移动两步:从3到5
- 慢指针移动一步:从2到3
- 第三步:
- 快指针移动两步:从5到null(因为5后面是6,再后面是null)
- 慢指针移动一步:从3到4
- 结束:快指针到达末尾(null),慢指针在第二个中间节点4。
五、C++ 代码实现(附详细注释)
#include <iostream>
using namespace std;
// 链表节点定义
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) {}
};
class Solution {
public:
ListNode* middleNode(ListNode* head) {
// 快慢指针都从头节点开始
ListNode* slow = head;
ListNode* fast = head;
// 循环条件:快指针不为空且快指针的下一个节点不为空
// 这样确保快指针可以安全地移动两步
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next; // 慢指针移动一步
fast = fast->next->next; // 快指针移动两步
}
// 循环结束时,慢指针指向中间节点
return slow;
}
};
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head != nullptr) {
cout << head->val << " ";
head = head->next;
}
cout << endl;
}
// 测试代码
int main() {
// 测试用例1:奇数长度链表 1->2->3->4->5
ListNode* head1 = new ListNode(1);
head1->next = new ListNode(2);
head1->next->next = new ListNode(3);
head1->next->next->next = new ListNode(4);
head1->next->next->next->next = new ListNode(5);
Solution solution;
ListNode* middle1 = solution.middleNode(head1);
cout << "奇数长度链表中间节点: " << middle1->val << endl; // 输出:3
// 测试用例2:偶数长度链表 1->2->3->4->5->6
ListNode* head2 = new ListNode(1);
head2->next = new ListNode(2);
head2->next->next = new ListNode(3);
head2->next->next->next = new ListNode(4);
head2->next->next->next->next = new ListNode(5);
head2->next->next->next->next->next = new ListNode(6);
ListNode* middle2 = solution.middleNode(head2);
cout << "偶数长度链表中间节点: " << middle2->val << endl; // 输出:4
// 释放内存(简单示例)
// 实际应用中需要更完整的释放
return 0;
}
六、时间空间复杂度
- 时间复杂度:O(n),其中n是链表长度。快指针遍历了整个链表(每次两步),但总时间仍然是O(n)。
- 空间复杂度:O(1),只使用了两个指针,没有使用额外空间。
七、注意事项
- 空链表处理:如果链表为空,函数会返回nullptr,这是正确的,因为空链表没有中间节点。
- 单节点链表:如果链表只有一个节点,快指针无法移动(fast->next为null),直接返回头节点。
- 指针移动顺序:先移动快指针,再移动慢指针,或者反过来都可以,因为每次循环都会移动两个指针。
- 循环条件:条件
fast != nullptr && fast->next != nullptr确保了快指针可以安全地移动两步。如果只检查fast != nullptr,当fast在最后一个节点时,fast->next->next可能会导致空指针异常。 - 偶数长度处理:题目要求有两个中间节点时返回第二个,快慢指针法自然满足这个要求,不需要特殊处理。
算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解
【算法】【链表】148.排序链表--通俗讲解
【算法】【链表】160.相交链表--通俗讲解
【算法】【链表】203.移除链表元素--通俗讲解
【算法】【链表】206.反转链表--通俗讲解
【算法】234.回文链表--通俗讲解
【算法】【链表】237.删除链表中的节点--通俗讲解
【算法】【链表】328.奇偶链表--通俗讲解
【算法】【链表】给单链表加一--通俗讲解
【算法】【链表】382.链表随机节点--通俗讲解
【算法】426.将二叉搜索树转化为排序的双向链表--通俗讲解
【算法】430.扁平化多级双向链表--通俗讲解
关注公众号,获取更多底层机制/ 算法通俗讲解干货!