1. 判断链表是否成环

通过快慢指针可以解决这个问题。

设置两个指针 ptrFast 和 ptrSlow ,Fast指针一次走两步,Slow指针一次走一步。可以想象,如果链表有环的话,那么Fast一定先进环Slow后进环(特殊情况,链表首尾相连成环)。

现在只考虑环内的视野,可以想象,当Slow也进入环内的时候,不管Fast此时在环内的任何位置,两个指针按照原来的步频继续前进,最终一定会相遇,因为步频相差1,即每移动一次两个指针距离缩短1,所以最终一定会相遇。

转换成代码就是让两个指针无限前进,如果Fast探测到NULL了说明无环,如果最终两个指针相遇则有环。

// 判断是否有环
int hasCircle(ListNode* head) {
    ListNode* ptrFast = head;
    ListNode* ptrSlow = head;

    do
    {
        if (ptrFast->next == NULL)
            return 0;
        else if (ptrFast->next->next == NULL)
            return 0;

        ptrFast = ptrFast->next->next;
        ptrSlow = ptrSlow->next;
    } while (ptrFast != ptrSlow);
    return 1;
}

2. 找出环入口

这里先来一个结论:当Slow进入环内,两个指针开始追赶,当两个指针相遇时Slow指针一定没有走完环的一圈,这个很好证明。

然后看下图:

设链表头到环入口距离为a,环入口到相遇点距离为b,相遇点到环入口距离为c,环长为L,假设两指针相遇时Fast已经在环内走了x圈+b的路程即xL+b。
所以可以得到Slow指针走过的路程为a+b,Fast指针走过的路程为a+xL+b。

Fast的速度为Slow的速度的两倍,所以现有:
(a + b) * 2 = a + xL + b
得 a + b = xL
得 a = (x-1)L + L - b
得 a = (x-1)L + c

最后这个式子是什么意思呢,即a的距离等于c的距离加若干圈环得长度。那这又是什么意思呢?用代码来说就是,现在再来两个指针,一个从链表头开始走,一个从相遇点顺着链表走,最终一定会在环入口相遇!

// 寻找环入口
// 输入有环链表
ListNode* findCircleEntry(ListNode* head) {
    ListNode* ptrFast = head;
    ListNode* ptrSlow = head;

    // 寻找相交节点
    do
    {
        ptrFast = ptrFast->next->next;
        ptrSlow = ptrSlow->next;
    } while (ptrFast != ptrSlow);

    // 寻找环入口
    // 这时指针不分快慢,每次都走一步
    // 一个指针到链表入口,一个指针停留在相交位置
    ptrSlow = head;
    while (ptrSlow != ptrFast) {
        ptrSlow = ptrSlow->next;
        ptrFast = ptrFast->next;
    }

    // 此时的相交节点即为环入口
    return ptrSlow;
}

3. 代码汇总

#include "LinkedList.h"

// 判断是否有环
int hasCircle(ListNode* head) {
    ListNode* ptrFast = head;
    ListNode* ptrSlow = head;

    do
    {
        if (ptrFast->next == NULL)
            return 0;
        else if (ptrFast->next->next == NULL)
            return 0;

        ptrFast = ptrFast->next->next;
        ptrSlow = ptrSlow->next;
    } while (ptrFast != ptrSlow);
    return 1;
}

// 寻找环入口
// 输入有环链表
ListNode* findCircleEntry(ListNode* head) {
    ListNode* ptrFast = head;
    ListNode* ptrSlow = head;

    // 寻找相交节点
    do
    {
        ptrFast = ptrFast->next->next;
        ptrSlow = ptrSlow->next;
    } while (ptrFast != ptrSlow);

    // 寻找环入口
    // 这时指针不分快慢,每次都走一步
    // 一个指针到链表入口,一个指针停留在相交位置
    ptrSlow = head;
    while (ptrSlow != ptrFast) {
        ptrSlow = ptrSlow->next;
        ptrFast = ptrFast->next;
    }

    // 此时的相交节点即为环入口
    return ptrSlow;
}

int main() {
    // 建立链表
    ListNode* head;
    ListInit(&head);
    ListPushBack(&head, 1);
    ListPushBack(&head, 2);
    ListPushBack(&head, 3);
    ListPushBack(&head, 4);
    ListPushBack(&head, 5);
    ListPushBack(&head, 6);
    ListPushBack(&head, 7);
    print_linkedList(head);
    printf("%d\n", hasCircle(head));

    // 成环
    ListNode* ptr = head;
    ListNode* tmp = NULL;
    while (ptr->next) {
        if (ptr->data == 3)
            tmp = ptr;
        ptr = ptr->next;
    }
    ptr->next = tmp;
    printf("%d\n", hasCircle(head));

    int res = findCircleEntry(head)->data;
    printf("相交节点:%d\n", res);

    system("pause");
    return 0;
}

4. 扩展问题:判断两个链表是否相交(链表可能带环)

这个问题可以分为上面6种情况,现在要做的就是区分这6种情况。

首先,判断两个链表是否带环,如果都不带环,则是1或2。
所以下面就是判断两个无环链表是否相交,方法很简单。先分别算出两个链表的长度,然后让长链表的指针先往前走两个链表长度之差的步数,可以想到对于情况2来说,这时候两个子链表已经等长了,所以现在让两个指针一起往前走,如果链表相交则指针必相交,如果链表不相交则指针也永远不会相交。

然后,如果一个链表无环一个链表有环,即情况3,可以想到两链表必不相交。

然后就是两个链表都带环的情况了,这时候找两个链表的环入口,如果环入口相同,即情况5,则链表相交。

最后就是区分情况4和情况6。分别记录两个不同的环入口,然后让一个指针从其中一个环入口开始往后走,如果是情况6则这个指针必会遇到另一个环入口,如果走了一圈还没遇到另一个环入口那就是情况4。

最后修改:2021 年 11 月 28 日
如果觉得我的文章对你有用,请随意赞赏