一个链表的尾节点的next指针反而指向其他节点(包括自己),就构成了一个带环链表。对带环链表问题的求解,往往涉及环的入口点和环的周长。本文着重介绍单向带环链表中求环的周长和环的入口的若干解法。

带环链表问题剖析_带环链表

判断链表是否带环

假设一个链表有环,则该链表一定包含一段闭合回路,遍历链表的指针进入该回路后就会陷入不断循环。已知的是,在一个闭合回路中,若两个点的运动相对速度合适,则两个点一定会在回路中的某个位置相遇(重合),所以可以利用追及运动证明环的存在。

为了分析方便,这里将带环链表进行抽象,整个链表包括非环部分和环。为了实现追及运动,分别定义快(fast)、慢(slow)指针遍历链表,fast指针一次走两步,slow指针一次走一步,可见当slow指针位于非环部分的中间时,fast开始进环;当slow指针开始进环时,fast指针已经绕环走了数圈,此时fast指针开始在环内追及slow指针。

在这种情形下,两指针一定会在环内的某一点(meet)相遇,下面给出逻辑证明。

带环链表问题剖析_环的入口_02

证明一:

slow指针一次走一步,fast指针一次走两步,两指针一定会在环内相遇

假设slow开始进环时,fastslow之间的距离为N,因为可能存在slow刚开始入环即与fast相遇的情况,固有

带环链表问题剖析_环的入口_03

fastslow相对速度为 1,即fast追及slow,两者之间的距离递减1,所以N一定会在数次运动后减为0,此时两指针相遇。

带环链表问题剖析_带环链表_04

还有没有其他可能的情况,比如slow一次走1步,fast一次走3步,两者一定会相遇吗?

问题一:

slow一次走一步,fast一次走三步,两指针一定会相遇吗?

此时slowfast相对速度为2,假设slow开始进环时fastslow的距离为N,在fast追及slow的过程中,二者之间的距离递减 2。通过证明一可知,N减至 0,二者相遇,对于相对速度为 2 的情形,N 是否能减至 0 需要考虑N的奇偶性

  • N为偶数 N递减 2,此时N一定会递减至0,两指针相遇
  • N为奇数 N的变化情况大致为N, N - 2, N - 4, . . ., 3, 1, -1当N为-1时,意味着两指针错过,fast领先slow一步,此时进入新一轮追及,且N变为 C - 1,其中C为环的周长。可见,在新一轮追及中,N能否递减至 0 取决于 C
  • C - 1为偶数 此时同N为偶数的情况,这一轮追及两指针一定会相遇
  • C - 1为奇数 此时同N为奇数的情况,快慢指针不断错过、进入新的追及,处于无限递归状态,两指针一定不会相遇

可见,slow 一次走一步,fast 一次走三步,两指针不一定会相遇

带环链表问题剖析_环的入口_05

问题二:

slow一次走一步,fast一次走四步,两指针一定会相遇吗?

此时slow与fast的相对速度为 3,为了证明 N 能否递减至 0,需要分三种情况

  • N % 3 == 0
  • N % 3 == 1
  • N % 3 == 2

每个情况的分析与证明一和问题一相似,可见,这种情形两指针不一定相遇。

实际上,只有快慢指针的相对速度为 1 时,才能保证两指针一定会相遇。


求环的周长

在进行上文的操作使快、慢指针相遇后,其中一个指针从相遇点出发绕环一周,直到再次与另一指针相遇,经过的路径即为环的周长

带环链表问题剖析_单向链表_06

求环的入口点

解法一:

设带环链表的非环部分长为L,快慢指针的相遇点距环的入口点长为X,快慢指针相遇前快指针已经绕环走了k圈,且

带环链表问题剖析_环的入口_07

带环链表问题剖析_单向链表_08

则相遇时有:

带环链表问题剖析_单向链表_09

可得: L = k*C - X,即L = (k - 1)*C  + C - X

得出结论:一个指针从相遇点出发,另一个指针从head位置出发,两指针一定会在环的入口点相遇。根据这个结论,即可找到环的入口点。

struct ListNode *detectCycle(struct ListNode *head)
{
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            struct ListNode* meet = slow;
            struct ListNode* cur = head;
            while(cur != meet)
            {
                cur = cur->next;
                meet = meet->next;
            }
            return cur;
        }
    }
    return NULL;
}

在观察上述等式时,要清醒地注意两指针是在环内循环运动的,指针之间的相对距离不会叠加。

解法二:

在找到快慢指针的相遇点meet后,可以先记录meet的下一个位置,再将链表在meet位置断开,可以观察到,此时环形链表变成了两个相交链表相交链表的交点即为要求的环的入口点

带环链表问题剖析_带环链表_10

此时问题转化为了找相交链表的交点的问题,用双指针求解即可。这里附上求相交链表求交点的代码,不再进行详细的证明:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
    if(!headA || !headB)//其中一个为空,一定不相交 {
        return NULL;
    }
    struct ListNode* pA = headA;
    struct ListNode* pB = headB;

    while(pA != pB)//pA和pB都为空或在某节点相遇
    {
        pA = (pA == NULL ? headB : pA->next);
        pB = (pB == NULL ? headA : pB->next);
    }
    return pA;
}