【算法】—— 链表常见算法3
1. 相交链表
1.1 问题描述
给你两个单链表的头节点headA 和headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回NULL 。
图示两个链表在节点c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
1.2 实现思路
? 首先想到的是暴力求解,但是我们不这样实现,通过遍历链表得到两个链表的长度,判断尾结点是否相等,若是相等则为相交链表,若是不相等则不相交,对于相交链表,让长的链表先走够差值,再让两个链表一起走,相遇的位置就是相交的链表处
- 分别遍历两个链表,记录出两个链表的长度为7和5,比较两个链表尾结点相等,链表相交
- 先遍历长的链表,遍历的结点个数是两个链表长度的差
- 两个链表一起遍历,相遇位置就是相交的链表
1.3 代码实现
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
if (headA == NULL || headB == NULL)
{
return NULL;
}
struct ListNode *curA = headA, *curB = headB;
int lenA = 0;
while (curA->next != NULL)
{
curA = curA->next;
lenA++;
}
int lenB = 0;
while (curB->next != NULL)
{
curB = curB->next;
lenB++;
}
if (curA != curB)
{
return NULL;
}
struct ListNode *longList = headA, *shortList = headB;
if (lenA < lenB)
{
longList = headB;
shortList = headA;
}
int sub = abs(lenA - lenB);
while (sub--)
{
longList = longList->next;
}
while (longList != shortList)
{
longList = longList->next;
shortList = shortList->next;
}
return longList;
}
2. 环形链表I
2.1 问题描述
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
2.2 实现思路
带环链表不能直接遍历,否则会进入死循环,所以我们使用快慢指针来判断,快指针走两步慢指针走一步,若是链表带环则两个指针总会相遇,若是不带环,则快指针一定先到NULL
n圈之后:快慢指针相遇
为什么快慢指针最后能够相遇?
- 快指针先进入环中,慢指针再次进入环中,快慢指针之间就会有距离,此时快指针追赶慢指针。
- 快指针走2步,慢指针走一步,两个指针每走一次它们的距离就会缩小一个,直到完全相遇。
快指针走3步,慢指针走1步,是否也能相遇?
不一定。
- 快慢指针进入环时的距离为N,快慢指针每走一次它们的差距就会缩小2个。
- 若是N是奇数,则追赶上时快指针会跨过慢指针,两者距离为-1,快指针相对于慢指针的距离就成了环长减一。
- 若是环长减一也是偶数,则再追一圈就追上了,若环长减一为奇数,则永远也追不上。
即:N为奇数且环长为偶数时不会相遇
2.3 代码实现
struct ListNode
{
int val;
struct ListNode *next;
};
bool hasCycle(struct ListNode *head)
{
struct ListNode *first = head, *slow = head;
while (first != NULL && first->next != NULL)
{
first = first->next->next;
slow = slow->next;
if (slow == first)
{
return true;
}
}
return false;
}
3. 环形链表II
3.1 问题描述
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
3.2 实现思路
方法一(公式法)
? 快指针走2步,慢指针走1步
? 假设入环前的长度为L,环的长度为C,入环点到相遇点的距离为X
-
因为:slow 入环后不可能走过一圈,因为fast 和slow 的距离不可能超过一圈 所以:slow 走过的距离为:
L
+
X
L + X
L+X -
因为:fast 入环后不一定只转了一圈,若是L很长,圈很小,slow 入环前fast 就已经转了很多圈,假设fast 在slow 入环之前转了n 圈(
n
>
=
1
n >= 1
n>=1) 所以:fast 走过的距离为:
L
+
n
×
C
+
X
L + n\times C + X
L+n×C+X -
因为:快指针走2步,慢指针走一步,即:
f
a
s
t
的距离
=
2
×
s
l
o
w
的距离
fast的距离 = 2 \times slow的距离
fast的距离=2×slow的距离 所以:
2
×
(
L
+
X
)
=
L
+
n
×
C
+
X
2 \times (L + X) = L+n \times C + X
2×(L+X)=L+n×C+X 化简为:
L
+
X
=
n
×
C
L+X = n \times C
L+X=n×C 化简为:
L
=
n
×
C
?
X
L = n \times C - X
L=n×C?X 分配律:
L
=
(
n
?
1
)
×
C
+
C
?
X
L = (n-1) \times C + C - X
L=(n?1)×C+C?X 加括号:
L
=
[
(
n
?
1
)
×
C
]
+
[
C
?
X
]
L = [(n-1) \times C] + [C - X]
L=[(n?1)×C]+[C?X]
L 的意义是从开始位置到入环点的距离。(n-1)*C 的意义是转了
n
?
1
n-1
n?1圈C-X 的意义是从相遇点顺时针到入环点的位置
结论:指针A从头开始走,B从相遇点开始走,第一个相遇的位置一定是入环点。
实现步骤:
- 找到链表的相遇点
- 定义A、B指针分别从头和相遇点开始出发,相遇的地方即是入环点
代码实现
struct ListNode
{
int val;
struct ListNode *next;
};
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode *slow = head, *fast = head;
while (fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
{
struct ListNode *A = head, *B = fast;
while (A != B)
{
A = A->next;
B = B->next;
}
return A;
}
}
return NULL;
}
方法二(转换为相交问题)
- 找到相遇点,将相遇点断开,形成相交的两个链表
- 分别以相遇点的下一个结点和开始结点作为两个链表的头结点,找链表的交点
- 还原链表并返回入环点
代码实现
相交链表的函数(在本文第一个算法处实现)
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB);
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode *slow = head, *fast = head;
while (fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
{
struct ListNode* next = fast->next;
fast->next = NULL;
struct ListNode* meet = getIntersectionNode(head, next);
fast->next = next;
return meet;
}
}
return NULL;
}
4. 复制带随机指针的链表
4.1 问题描述
? 给你一个长度为n 的链表,每个节点包含一个额外增加的随机指针random ,该指针可以指向链表中的任何节点或空节点。
? 构造这个链表的深拷贝。 深拷贝的链表应与原链表的数据域完全一致,随机指针的指向与原链表的随机指针的指向一一对应,但是不能直到原链表的结点中
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
4.2 实现思路
最容易实现的是暴力求解的方法:
- 复制原链表结点
- 更新
random ,遍历链表,找到每个结点的random 在原链表中指向第几个结点,然后使新链表的random 指向自身的第几个结点 - 时间复杂度是
O
(
n
2
)
O(n^2)
O(n2),容易理解但是效率低,我们不实现这种方法
我们使用原地拷贝的方法对如下链表实现深拷贝:
- 复制每个结点,将其连接在源链表的每个结点的后面
展开为下图:
- 更新
random ,通过源结点的random 找到复制结点的random 指向,源节点的下一个结点就是该节点的复制结点
展开如下图:
- 将拷贝链表解下来,形成新链表。将新链表返回
代码实现
struct Node
{
int val;
struct Node *next;
struct Node *random;
};
struct Node* copyRandomList(struct Node* head)
{
if (head == NULL)
{
return head;
}
struct Node* cur = head;
struct Node* copy = NULL;
while (cur != NULL)
{
copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
copy->next = cur->next;
cur->next = copy;
cur = copy->next;
}
cur = head;
while (cur != NULL)
{
copy = cur->next;
if (cur->random != NULL)
{
copy->random = cur->random->next;
}
else
{
copy->random = NULL;
}
cur = copy->next;
}
struct Node* copyHead = head->next;
cur = head;
while (cur->next->next != NULL)
{
copy = cur->next;
cur->next = copy->next;
copy->next = copy->next->next;
cur = cur->next;
}
cur->next = NULL;
return copyHead;
}
|