题目
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。注意不允许修改链表。
进阶:你能用 O(1)
(即,常量)内存解决此问题吗?
解法1:哈希去重
由于需要返回开始入环的第一个节点,可以考虑通过哈希表保存已访问过的节点,如果下一个访问的节点已经在哈希表中,那么表示已经链表成环,且该节点为链表开始入环的第一个节点。
代码如下:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
unordered_set<ListNode*> buffer{};
while (head != nullptr) {
if (buffer.find(head) != buffer.end()) {
return head;
}
buffer.insert(head);
head = head->next;
}
return nullptr;
}
};
时间复杂度和空间复杂度均为O(n)
,执行结果如下:
解法2:快慢指针
解法1中空间复杂度为O(n)
,在题目中进阶要求空间复杂度为O(1)
。此时仍然可应用快慢指针处理,初始化两个指针,一个快指针,每次走两步,一个慢指针,每次走一步。如果成环,那么快指针肯定会追到慢指针。如下图所示:
如果链表共有 m + n 个节点,其中m为链表头部到链表环入口的节点个数,n为链表环的节点个数。由于快指针每次走两个节点,慢指针每次走一个节点,当两个指针相遇时,快指针走过的节点个数为慢指针走过的节点个数的2倍,同时快指针走过的节点个数比慢指针多x倍的链表环的节点个数。两个条件分别如下:
// f为快指针走过的节点个数,s为慢指针走过的节点个数,x为快指针走过的节点个数比慢指针多x倍的链表环的节点个数
f = 2s; // 条件1:快指针走过的节点个数为慢指针走过的节点个数的2倍
f = s + xn; // 条件2:快指针走过的节点个数比慢指针多x倍的链表环的节点个数
通过两个条件可得:s = xn;即慢指针在相遇时走过的节点个数刚好为链表环节点个数的倍数。如果一个节点走到链表开始入环的第一个节点位置时,那么其走过的节点个数肯定为 m + xn,而慢指针已经走过了xn个节点,那么只需要再走m个节点肯定可以到链表开始入环的节点啦!
此时如果再初始化一个慢指针并指向头结点,每次走一个节点,让其与另外一个慢指针都走m个节点,那么这两个指针肯定在链表开始入环的节点相遇。
代码如下:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow_node = head;
ListNode* fast_node = head;
while ((fast_node != nullptr) && (fast_node->next != nullptr)) {
fast_node = fast_node->next->next;
slow_node = slow_node->next;
if (fast_node == slow_node) {
break;
}
}
// 需要先判断链表是否成环,如果不成环直接返回空指针
if ((fast_node == nullptr) || (fast_node->next == nullptr)) {
return nullptr;
}
// 如果成环,让快指针重新回到头节点,并且每次只移动一个节点,让其和慢指针同时移动,下一次相遇时肯定是链表开始入环的节点
fast_node = head;
while (fast_node != slow_node) {
fast_node = fast_node->next;
slow_node = slow_node->next;
}
return slow_node;
}
};
时间复杂度为O(n)
,空间复杂度为O(1)
执行结果如下: