📌 题目链接:160. 相交链表 - 力扣(LeetCode)
🔍 难度:简单 | 🏷️ 标签:链表、双指针、哈希表
⏱️ 目标时间复杂度:O(m + n)
💾 空间复杂度:O(1)(最优解)
✅ 本篇核心算法 :双指针技巧(Two Pointers)
🎯 面试重点:如何在 O(1) 空间下解决链表相交问题?为什么双指针能"对齐"路径长度?
🔥 适用场景:链表结构对比、路径匹配、环检测的前置思维训练
🧩 题目分析
给定两个单链表 headA 和 headB,要求找出它们相交的起始节点 。若无交点,则返回 null。
📌 关键信息:
- 不存在环(保证链表是线性的)
- 节点值可能重复,但内存地址相同才表示相交
- 必须保持原始结构(不能修改链表)
💡 示例中强调:值为 1 的节点不相交,因为它们在内存中是不同对象!只有当两个指针指向同一个节点时才算相交!
❗️ 注意:不是"值相同"就是相交,而是"引用相同"才是相交!
🔍 核心算法及代码讲解
✅ 方法一:哈希集合(Hash Set)------空间换时间
cpp
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
unordered_set<ListNode*> visited;
ListNode* temp = headA;
while (temp != nullptr) {
visited.insert(temp); // 将 headA 中所有节点存入哈希表
temp = temp->next;
}
temp = headB;
while (temp != nullptr) {
if (visited.count(temp)) { // 若 headB 中某节点已在哈希表中,说明找到交点
return temp;
}
temp = temp->next;
}
return nullptr; // 未找到交点
}
};
🧠 原理说明:
- 使用
unordered_set<ListNode*>存储headA所有节点的指针(地址) - 遍历
headB,一旦发现某个节点已存在于哈希表中 → 即为交点 - 第一个匹配的节点即为最早相交的节点
📈 复杂度分析:
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(m + n),遍历两个链表各一次 |
| 空间复杂度 | O(m),存储 headA 的全部节点 |
❌ 缺点:使用了额外空间,不符合进阶要求(O(1) 空间)
✅ 方法二:双指针法(Two Pointers)------最优解!
这是本题最经典的解法,也是面试官最爱考察的「巧妙思维」。
cpp
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr) {
return nullptr; // 至少有一个为空,则不可能相交
}
ListNode* pA = headA; // 指针 A,初始指向 headA
ListNode* pB = headB; // 指针 B,初始指向 headB
while (pA != pB) { // 当两个指针不相遇时继续循环
pA = pA == nullptr ? headB : pA->next; // pA 到尾部则跳转到 headB
pB = pB == nullptr ? headA : pB->next; // pB 到尾部则跳转到 headA
}
return pA; // 此时 pA == pB,可能是交点或都为 null
}
};
🛠️ 行注释详解:
cpp
ListNode* pA = headA; // 指针从 headA 开始
ListNode* pB = headB; // 指针从 headB 开始
while (pA != pB) { // 只要没相遇就继续走
pA = pA == nullptr ? headB : pA->next; // 如果 pA 走完 headA,就跳到 headB 继续走
pB = pB == nullptr ? headA : pB->next; // 如果 pB 走完 headB,就跳到 headA 继续走
}
return pA; // 最终要么是交点,要么都是 null(不相交)
🔍 为什么双指针能工作?------数学证明
设:
headA长度 = m = a + cheadB长度 = n = b + c- 其中 a: headA 不相交部分长度,b: headB 不相交部分长度,c: 相交部分长度
🔄 运行过程模拟:
| 步骤 | pA 路径 | pB 路径 |
|---|---|---|
| 1 | headA → ... → tailA | headB → ... → tailB |
| 2 | headB → ... → 交点 | headA → ... → 交点 |
👉 总共走了多少步?
- pA 走了:a + c + b
- pB 走了:b + c + a
✅ 两者总路程相同!因此会在同一时间到达交点
🎯 关键洞察:通过"绕路",让两个指针走过相同的总长度,从而自然对齐!
✅ 特殊情况处理:
- 若两链表不相交 → pA 和 pB 同时变为
nullptr→ 返回null - 若相交 → 在交点处相遇 → 返回该节点
🧭 解题思路(分步拆解)
-
边界判断
- 若任一链表为空 → 不可能相交 → 返回
nullptr
- 若任一链表为空 → 不可能相交 → 返回
-
初始化双指针
pA = headA,pB = headB
-
同步移动指针
- 每次移动一步,若当前指针为
nullptr,则跳转到另一个链表的头节点
- 每次移动一步,若当前指针为
-
判断是否相遇
- 当
pA == pB时停止循环 - 返回当前节点(可能是交点或
null)
- 当
-
逻辑闭环
- 不相交 → 两个指针都会走到
null并同时退出 - 相交 → 在交点处相遇并返回
- 不相交 → 两个指针都会走到
📊 算法分析
| 指标 | 哈希表法 | 双指针法 |
|---|---|---|
| 时间复杂度 | O(m + n) | O(m + n) |
| 空间复杂度 | O(m) | O(1) ✅ |
| 是否破坏结构 | 否 | 否 |
| 面试推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 思维难度 | ⭐⭐ | ⭐⭐⭐ |
💡 面试加分项:你能说出"双指针法的本质是让两个指针走相同的总距离"吗?这正是体现你理解深度的关键!
💻 代码(完整可运行版本)
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
// 方法一:哈希表(空间 O(m))
class Solution_Hash {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
unordered_set<ListNode*> visited;
ListNode* temp = headA;
while (temp != nullptr) {
visited.insert(temp);
temp = temp->next;
}
temp = headB;
while (temp != nullptr) {
if (visited.count(temp)) {
return temp;
}
temp = temp->next;
}
return nullptr;
}
};
// 方法二:双指针(空间 O(1))✅ 推荐
class Solution {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
if (headA == nullptr || headB == nullptr) {
return nullptr;
}
ListNode* pA = headA;
ListNode* pB = headB;
while (pA != pB) {
pA = pA == nullptr ? headB : pA->next;
pB = pB == nullptr ? headA : pB->next;
}
return pA;
}
};
// 测试函数
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 构造测试用例 1:[4,1,8,4,5] 和 [5,6,1,8,4,5],交点为 8
ListNode* headA = new ListNode(4);
headA->next = new ListNode(1);
headA->next->next = new ListNode(8);
headA->next->next->next = new ListNode(4);
headA->next->next->next->next = new ListNode(5);
ListNode* headB = new ListNode(5);
headB->next = new ListNode(6);
headB->next->next = new ListNode(1);
headB->next->next->next = headA->next->next; // 指向交点 8
Solution sol;
ListNode* result = sol.getIntersectionNode(headA, headB);
if (result) {
cout << "Intersected at '" << result->val << "'" << endl;
} else {
cout << "No intersection" << endl;
}
// 清理内存(实际面试中可忽略)
// ...
return 0;
}
🧪 测试用例 & 结果
| 输入 | 输出 | 说明 |
|---|---|---|
listA=[4,1,8,4,5], listB=[5,6,1,8,4,5] |
8 |
成功找到交点 |
listA=[1,9,1,2,4], listB=[3,2,4] |
2 |
交点为 2 |
listA=[2,6,4], listB=[1,5] |
null |
无交点 |
✅ 所有测试均通过,符合题目预期!
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第23题 ------ 17.反转链表(简单)
🔹 题目 :给你单链表的头节点
head,请将其反转,并返回反转后的链表。🔹 核心思路:使用三指针法(前驱、当前、后继)逐步翻转指针方向。
🔹 考点:链表操作、指针重定向、递归 vs 迭代。
🔹 难度:简单,但却是链表操作的基础,常考于字节、腾讯、阿里等大厂笔试。
💡 提示:不要用栈或数组辅助!原地反转才是硬核!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!
📌 特别提醒 :结合手写代码练习,形成肌肉记忆!
🚀 从今天开始,把每一道题变成你的"武器库"中的利器!