给出一个链表,如果其中有环,则需找出环的入口节点:即从头结点开始遍历,第一个被访问到的环中的节点。
如下图示,入口节点的值为 2。
方法一:双指针;分析距离
当一个链表有环时,快慢指针必然会进入到环中。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的(也就是套了一圈)。
当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。
设环的入口节点为
E
E
E,两指针相遇的节点为
C
C
C。设头结点
H
H
H 到
E
E
E 的距离为
L
L
L,
E
E
E 到
C
C
C 的距离为
P
1
P_1
P1,
C
C
C 到
E
E
E 的距离为
P
2
P_2
P2。
因为快指针的移动速度是慢指针的两倍,所以可得出:
2
∗
(
L
+
P
1
)
=
L
+
(
n
+
1
)
∗
P
1
+
n
∗
P
2
,
n
∈
N
+
2*(L+P_1) = L + (n+1)*P_1 + n*P_2,n \in \mathbb{N}^+
2∗(L+P1)=L+(n+1)∗P1+n∗P2,n∈N+
其中
n
n
n 是正整数,代表相遇时快指针已经完整绕环
n
n
n 圈了。将上式移项可得:
L
=
(
n
−
1
)
∗
P
1
+
n
∗
P
2
,
n
∈
N
+
L = (n-1)*P_1 + n*P_2,n \in \mathbb{N}^+
L=(n−1)∗P1+n∗P2,n∈N+
设有两个指针 h h h 和 c c c 分别从 H H H 和 C C C 同时出发,每次移动一个节点。当 h h h 到达 E E E 时, c c c 也必然到达 E E E。
于是有了一种解决方案:先用快慢指针找到节点 C C C。然后再用两个慢指针分别从 H H H 和 C C C 出发,两慢指针相遇的节点即为入口节点。
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
ListNode *fast = pHead, *slow = pHead;
while (fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
// 相遇了
if (fast == slow) {
ListNode *entry = pHead;
while (entry != slow) {
entry = entry->next;
slow = slow->next;
}
return entry;
}
}
return nullptr;
}
};
方法二:修改 next 指针
还有一种比较 hack 的做法,仅在 Linux 下用 C++ 验证过,不确定能否在其他操作系统及编程语言下实现。
上图描述了 32/64 位系统对内存地址的划分,不难发现,用户空间地址的最高位全部为 0。我们可利用这一点表示某个节点是否被访问过:
- 节点指针域的最高位为 0,表示该节点未被访问过。
- 节点指针域的最高位为 1,表示该节点已经被访问过了。
利用上述标记方法,可以用一个指针找出入口节点。下述代码可在 64 位系统上正确运行。
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
const uint64_t mask = 0x8000000000000000;
while (pHead != nullptr && pHead->next != nullptr) {
uint64_t &adr = *(uint64_t*)(&(pHead->next));
if (adr & mask) {
return pHead;
}
pHead = pHead->next;
adr |= mask;
}
return nullptr;
}
};