经典链表问题——判断链表是否带环

一、题目描述

原题链接:https://leetcode.cn/problems/linked-list-cycle/

题目描述:

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

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

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

带环链表类似于以下结构:

经典链表问题——判断链表是否带环_第1张图片

  • 是否有环,实际上可以转换为链表的最后一个节点是否指向了链表其中的一个节点,如果有环,我们遍历一遍链表的话,会陷入死循环。那么我们该如何判断链表是否有环呢?

二、解题

解题思路:因为该题涉及部分数学知识,所以我们先讲如何做,再讲为什么。首先我们定义快慢指针,都指向头结点,让快指针每次走两步,慢指针每次走一步,如此不断迭代。如果最后快指针为NULL了,那么链表就是不带环的;如果slow最后追上了fast,那么链表就是带环的了。

为什么可以这么做?

其实可以把这里抽象成一个追及问题。因为链表是带环的,快指针又比慢指针走的快,所以它肯定会进入循环当中,等到慢指针也进入循环中后,他们再不断迭代后肯定会相遇了。

可能有人觉得不太对,如果数值巧一点,会不会一直都不会相遇呢?

这里先给出答案,要看快慢指针的差值,如果快指针每次走的步数—慢指针每次走的步数=1时,它们就一定能相遇,否则就要看具体的链表个数数值了。

推导过程:

我们先将该问题抽象成如下模型

经典链表问题——判断链表是否带环_第2张图片

当慢指针入环时,设此时快慢指针距离为N,如下 

经典链表问题——判断链表是否带环_第3张图片

 上文说了将其抽象成追及问题,当快指针每次走的步数—慢指针每次走的步数=1时,每迭代一次快慢指针之间的距离就减一,不断减到N=0时就相遇了。

如果快指针每次走的步数—慢指针每次走的步数!=1时

我们记此时慢指针slow与快指针fast的距离为N,慢指针每次走一步,快指针每次走三步,因此N每次减少2,于是就有:

N - 2
N - 4
N - 6
N - 8
.....
最终有两种情况:
 N = 1,然后再减2 -> -1
 N = 2,然后再减2 -> 0

  • 如果N最后减为0,则快指针等于慢指针,判断为true,如果N最后减为-1,那么需要再次追击。所以得出:当N为奇数时,第一次追不上;当N为偶数时,一定追得上。
  • 如果第一次追不上,最后fast会在slow的下一个位置,然后继续追击,那么,继续追击又是否追的上呢?其实就是重置了两指针之间的距离,再来一次追及而已。

我们假设环的长度为c,那么此时再次追击两个指针的距离就为:C - 1,令L = C - 1,则:

L - 2
L - 4
L - 6
L - 8
.....
最终有两种情况:
 L = 1,然后再减2 -> -1
 L = 2,然后再减2 -> 0

因此,这里又有两种情况,有了前面的推导,这里不难得出,当C为偶数时,则L为奇数,此时继续追不上,并且下一次也是一样,所以这里会陷入死循环;当C为奇数时,则L为偶数,此时是追的上的。因此,当C为偶数时,永远追不上,当C为奇数时,追的上。
所以,通过以上分析,快指针每次走三步,慢指针每次走一步不一定能判断是否为带环链表,可能会陷入死循环,尽管陷入死循环就说明带环。

以此类推,其他情况也可以按此思路推出来,不知道大家是否学会了呢?
 

代码实现

bool hasCycle(struct ListNode *head) {
    struct ListNode* fast = head, * slow = head;

    while (fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;

        if (fast == slow) return true;
    }

    return false;
}

环形链表的另一个问题

原题链接:https://leetcode.cn/problems/linked-list-cycle-ii/

题目描述:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。注意:不允许修改链表。

注意:与上题不同,本题不仅需要判断是否带环,还需要返回链表入环的第一个结点。

经典链表问题——判断链表是否带环_第4张图片

 解题思路:

还是先说做法再说思路

先以第一题的思路用快慢指针找到快指针和慢指针相遇的那个点,然后一个指针从该点开始走,一个指针从头开始走,两个指针每次走一步,最终两个指针会在入环的第一个节点相遇,然后返回这个节点。

推导:

假设头结点到入环点的距离为X,入环点到相遇点的距离为N,环的长度为C。设相遇点为pos

经典链表问题——判断链表是否带环_第5张图片

 

 

由此,我们可以计算一下当快慢指针相遇时,两指针各自走的路程。

快指针:X + nC + N;

 慢指针:X + N;

为什么是nC呢,因为如果环很小的话,当慢指针入环后,快指针会已经转了很多圈了。

而慢指针没有C是因为:慢指针入环后最多只会走C - 1步,不可能出现在环内步数超过一圈的情况,因此慢指针没有C

因为快指针每次走两步,是慢指针的两倍,所以路程也是慢指针的两倍

所以可得:X + nC + N = 2(X + N)——>,令C-N=L,X = (n - 1)C + L;

这个表达式的意思就是头结点到入环点的距离等于相遇点绕了n-1圈后再走L,因为是环所以其实c可以不用看,X=L,所以从pos位置和头结点开始每个指针各走一步,他们就会在入环点相遇。

代码实现:

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *slow = head, *fast = head;

    while (fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
		// 如果找到相遇的那个点,就开始找入环的第一个节点
        if (slow == fast)
        {
            struct ListNode *cur = slow;
            // 一个从头开始走,一个从当前节点开始走
            while (cur != head)
            {
                head = head->next;
                cur = cur->next;
            }
            // 最终相遇的那个点就是入环的第一个节点
            return cur;
        }
    }
	
	// 如果链表不带环,就返回NULL
    return NULL;
}

三、总结

你可能感兴趣的:(LeetCode刷题之路,链表,数据结构,c++,leetcode,算法)