随机链表的复制
题目如下图所示:

思路:将复制的节点连接到其对应的拷贝节点之后
- 拷贝链表的每一个节点,拷贝的节点先链接到被拷贝节点的后面
- 复制随机指针的链接:拷贝节点的随机指针指向被拷贝节点随机指针的下一个位置
- 拆解链表,把拷贝的链表从原链表中拆解出来
在解答这道题目时,我们可以通过多次运行测试,发现代码中需要改进的环节,并逐步完善。
cpp
struct Node* copyRandomList(struct Node* head) {
if(head == NULL)
{
return NULL;
}
struct Node* cur = head;
// 只拷贝了next和val的值
while (cur) {
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
copy->next = cur->next;
cur->next = copy;
cur = cur->next->next;
}
// 拷贝了原链表中random的指向
struct Node* pcur = head;
struct Node* pcopy = head->next;
while (pcur) {
if(pcur->random == NULL)
{
pcopy->random = NULL;
}
else
{
pcopy->random = pcur->random->next;
}
if (pcur->next->next == NULL) {
break;
}
pcur = pcur->next->next;
pcopy = pcopy->next->next;
}
// 建立新链表
struct Node* phead = head->next;
struct Node* ptail = head->next;
struct Node* p = head->next;//作为ptail的前一个节点,用于链表之间的连接
while (ptail->next)
{
ptail = ptail->next->next;
p->next = ptail;
p = p->next;
}
return phead;
}
环形链表I
题目如下图所示:

思路:快慢指针
定义快慢指针fast,slow, 如果链表确实有环,fast指针一定会在环内追上slow指针。
cpp
bool hasCycle(struct ListNode *head) {
struct ListNode *slow = head;
struct ListNode *fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
return true;
}
}
return false;
}
这个代码非常简单,但引申出一系列问题
问题1:为什么一定会相遇,有没有可能会错过,永远追不上
假设slow进环时,slow和fast之间的距离是N,fast追击slow的过程如下:
- N
- N-1
- N-2
- ...
- 2
- 1
- 0 -->追上了
每追击一次,距离缩小1,距离为0时便是追到了,即相遇了
问题2:slow一次走一步,fast走2步,3步,4步...可以吗
假设slow进环时,slow和fast之间的距离是N,fast走3步时,fast追击slow的过程如下:
假设1:
- N
- N-2
- N-4
- ...
- 4
- 2
- 0 -->追上了
假设2:
- N
- N-2
- N-4
- ...
- 3
- 1
- -1 -->错过了
如果错过当前机会,系统将进入下一轮循环。这将导致前述两种假设情况继续存在,难以区分。接下来,我们通过数学表达式来阐明这一概念。
- L:链表初始位置到环的开始位置的距离
- C:环的长度
- N:fast到slow的距离
fast走的距离:L + x * C + C - N;
slow走的距离:L
3L = L + x * C + C - N;
解得:2L = ( x - 1 ) C - N;
- 2L:一定是偶数
若C是偶数,N是奇数时不成立,N是偶数时成立
最终答案为否定,建议采用fast优先于slow的方案。
环形链表II
题目如下图所示:

思路:快慢指针
我们可以运用数学原理解决这个问题:让一个指针从相遇点出发,另一个从链表头开始,两者相遇的位置即为循环的起点。
第一次相遇时:
slow走的路程:L + N;
fast走的路程:L + C * X + N;
2L + 2N = L + C * X + N;
解得:L = C * X - N;也就是L = X - N,即相遇点到循环起点的距离 == 链表头到循环起点的距离
cpp
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow = head;
struct ListNode *fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
struct ListNode *meet = slow;
while(head != meet)
{
meet = meet->next;
head = head->next;
}
return meet;
}
}
return NULL;
}
总结 📝
本文通过 随机链表的复制 、环形链表 I 和 环形链表 II 三道经典题目,深入分析了链表操作中的不同解法效率:
随机链表的复制解法对比 🎯
| 思路 | 方法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|---|
| 思路1 | 原地复制 + 拆分法 | O(n)O(n) | O(1)O(1) | 三步操作,空间最优,推荐使用 ✅ |
环形链表 I 解法对比 🔍
| 思路 | 方法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|---|
| 思路1 | 快慢指针 | O(n)O(n) | O(1)O(1) | 最优解,简洁高效 ⚡ |
环形链表 II 解法对比 🎪
| 思路 | 方法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|---|
| 思路1 | 快慢指针 + 数学推导 | O(n)O(n) | O(1)O(1) | 最优解,找到环的入口节点 🎯 |
核心收获 💡
-
原地复制法解决随机链表:通过三步操作(复制节点、处理 random、拆分链表),实现深度拷贝 🔄
-
快慢指针判环:利用速度差判断链表是否有环,空间 O(1)O(1) 🏃♂️🏃♀️
-
快慢指针找环入口:相遇后重置指针,同步走找到环的入口节点 🚪
-
边界处理:链表为空的处理、随机指针为空的处理、无环情况的处理 ⚠️
-
面试技巧:掌握经典题目的最优解,展示对链表操作的熟练度 💪
下期预告 🔜
下一期我们将继续**【数据结构系列05】** ,进入 栈和队列经典问题,一起来看看三道经典题目:
-
[括号匹配问题]:如何判断字符串中的括号是否有效匹配? 🔄
-
[用队列实现栈]:如何仅用队列结构模拟栈的先进后出特性? 📚
-
[用栈实现队列]:如何用栈结构模拟队列的先进先出行为? 🚶♂️🚶♀️
如果你对链表操作还有疑问,欢迎在评论区留言讨论!如果觉得有帮助,别忘了点赞、收藏、关注,我们下期见! 🎉👋
