环形链表面试题详解

文章介绍了如何使用快慢指针技巧判断链表中是否存在环,并在环存在时找到环的起始节点。通过对比快指针和慢指针的移动,确保在环中相遇,从而确定链表的环结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

A. 环形链表1

给你一个链表的头节点 head ,判断链表中是否有环.

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况.

如果链表中存在环 ,则返回 true 。 否则,返回 false .
OJ链表

// 判断链表是否有环  
bool hasCycle(ListNode *head) {  
    if (head == NULL || head->next == NULL) {  
        // 空链表或只有一个节点的链表不可能有环  
        return false;  
    }  
  
    ListNode *slow = head;  
    ListNode *fast = head->next;  
  
    // 快慢指针开始移动,直到它们相遇或快指针到达链表尾部  
    while (slow != fast) {  
        // 如果快指针到达链表尾部,说明没有环  
        if (fast == NULL || fast->next == NULL) {  
            return false;  
        }  
        slow = slow->next; // 慢指针每次移动一个节点  
        fast = fast->next->next; // 快指针每次移动两个节点  
    }  
  
    // 如果快慢指针相遇,说明有环  
    return true;  
}  

【思路】
快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表其实位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾

【扩展问题】

  • 为什么快指针每次走两步,慢指针走一步可以?

假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。

  • 快指针一次走3步,走4步,…n步行吗?

假设:快指针每次走三步,慢指针每次走一步, 此时快指针肯定先进环。假设慢指针进环的时候,快指针的位置如图所示
在这里插入图片描述
此时按照上述方法来环绕移动
当快指针每次走三步,慢指针每次走一步时,它们之间的相对速度差是两步。这意味着快指针相对于慢指针每次都会拉近两个节点的距离。但是,如果环的大小(即环中节点的数量)是快指针和慢指针速度差的倍数(即环的大小是2的倍数),那么快指针可能会“套圈”慢指针而不相遇。具体来说,如果环的大小是2的倍数(比如4、6、8等),那么快指针会在到达环的起点之前追上慢指针,但会在慢指针之后再次到达起点,从而不会相遇。

举个例子,假设环的大小是4个节点,快指针每次走三步,慢指针每次走一步。当慢指针走完一圈回到起点时,快指针已经走了三圈,即12个节点,并回到了起点之后两个节点的位置。因此,它们不会在同一位置相遇。

然而,如果环的大小不是快指针和慢指针速度差的倍数,那么快指针最终还是会与慢指针相遇。这是因为在这种情况下,快指针和慢指针的相对位置会在环中不断发生变化,直到它们在某个节点相遇。

但为什么快指针走两步,慢指针走一步可以确保相遇呢?这是因为无论环的大小是多少,快指针都会在环中追上慢指针。具体来说,假设环的大小为n,那么当慢指针走了x圈时(x为正整数),快指针会走了2x圈。由于它们都在环中移动,所以它们最终会在某个节点相遇。

因此,虽然快指针每次走三步或更多步在理论上是可行的(只要处理好空指针和尾部的情况),但在实践中,快指针每次走两步,慢指针走一步是一种更简单、更可靠的方法来检测链表中的环。
所以只有快指针走两步,慢指针走一步才可以,因为换的最小长度是一,即使套圈了两个也在相同的位置.

B. 环形链表2

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null.

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况.

不允许修改链表。
OJ链表

typedef struct ListNode {  
    int val;  
    struct ListNode *next;  
} ListNode;  
  
// 函数声明  
ListNode *detectCycle(ListNode *head);  
  
ListNode *detectCycle(ListNode *head) {  
    if (!head || !head->next) {  
        // 空链表或只有一个节点的链表没有环  
        return NULL;  
    }  
  
    ListNode *slow = head;  
    ListNode *fast = head;  
  
    // 第一步:检测环是否存在  
    while (fast && fast->next) {  
        slow = slow->next;  
        fast = fast->next->next;  
  
        // 如果快慢指针相遇,说明有环  
        if (slow == fast) {  
            break;  
        }  
    }  
  
    // 如果fast或fast->next为NULL,说明没有环  
    if (!fast || !fast->next) {  
        return NULL;  
    }  
  
    // 第二步:找到环的起始节点  
    slow = head;  
    while (slow != fast) {  
        slow = slow->next;  
        fast = fast->next;  
    }  
  
    // slow(或fast)现在指向环的起始节点  
    return slow;  
} 

让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。

证明:

  • 定义:
  • 假设环的起始节点为 tail(在环中,我们不知道它的确切位置,但它是环的一部分)。 假设从头节点 head 到环的起始节点tail 的距离为 a 个节点。 假设环的长度(即环中节点的数量)为 b 个节点。 当快慢指针相遇时,假设慢指针走了 s步,那么快指针就走了 2s 步(因为快指针每次移动两步).
  • 快慢指针相遇时的情况:
  • 当快慢指针相遇时,慢指针 slow 已经走了 s 步,其中 s = a + nb(n 是一个非负整数,表示慢指针在环中绕了多少圈)。 同时,快指针 fast 走了 2s 步,即 2s = a + mb(m是另一个非负整数,并且由于快指针走得快,所以 m 会比 n 大).
  • 找出 m 和 n 的关系:
  • 由于快指针走的是慢指针的两倍,所以 2s = s + kb(其中 k 是慢指针在环中多绕的圈数)。 这意味着 s = kb。 所以,当快慢指针相遇时,慢指针在环中绕了 k 圈.
  • 两个指针从不同起点同时移动:
  • 现在,我们有一个指针从头节点 head 开始,另一个从快慢指针相遇点开始。 当从头节点开始的指针走了 a步到达环的起始节点 tail 时,从相遇点开始的指针也在环中走了 k*b 步(因为它之前已经走了这么多步)。 由于环的长度是 b,从相遇点开始的指针现在也在环的起始节点 tail。
  • 结论:
  • 因此,两个指针最终会在环的起始节点 tail 相遇。

伪代码描述:
// 假设 slow 和 fast 已经在环中相遇
slow = head // 重置 slow 到链表头
while (slow != fast) { // 当两个指针没有相遇时
slow = slow->next // 两者同时移动
fast = fast->next
}

// 此时 slow(和 fast)指向环的起始节点
这个逻辑利用了环的性质,确保两个指针最终会在环的起始节点相遇。

在这里插入图片描述
那么看到这就接近尾声了哦,劳动节假期也要接近尾声了,呜呜呜,那么咱们下期再见咯
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值