链表练习题2:找链表中间结点、找链表倒数第K个结点、判断是否是带环链表、求带环链表的第一个入环结点(注意:以上所有习题都要用到快慢指针法)

目录

一、找链表中间结点

1.解题思路

(1)思路1

(2)思路2

(3)思路3:快慢指针法

思路3的详细描述

2.1.思路3的代码实现—链表是偶数个结点则下面代码可以找出链表第二个中间结点

代码分析

注意事项

2.2.拓展代码—若链表是偶数个结点则下面代码可以找出链表第一个中间结点

2.3.测试代码

二、找链表倒数第K个结点

1.思路1

1.1.思路步骤

1.2.代码

图形解析

代码

2.思路2

2.1.思路描述

图形解析

​编辑

代码

2.3代码分析

3.测试代码

4.总结

4.1.对思路1进行小结

4.2.求链表中间结点和求链表倒数第k个结点进行对比

三、判断是否是带环链表

1.解题思路

2.扩展问题

3.代码

四、求带环链表的第一个入环结点

1.思路1

1.1.思路1步骤

1.2.思路1证明 (对下面结论进行证明)

 1.3.思路1代码

2.思路2

2.1.思路描述

2.2.代码

2.3.对思路2进行总结


一、找链表中间结点

. - 力扣(LeetCode)

1.解题思路

(1)思路1

先遍历1遍整个链表后求出链表的长度len,然后再找链表中间结点。若链表的长度len是奇数的话,则需再去遍历第2遍链表但是循环的次数是len / 2次即只能访问到单链表的第len / 2个结点,则链表的第len / 2个结点就是我们要求的链表的中间结点;若链表的长度len是偶数的话,则需再去遍历第2遍链表但是循环的次数是len / 2 + 1次即只能访问到单链表的第len / 2 + 1个结点,则链表的第len / 2 + 1个结点就是我们要求的链表的第二个中间结点。(注意:先求链表的长度再求访问链表的中间结点这种方法要遍历两遍单链表;思路1的时间复杂度是O(N))

(2)思路2

先遍历1遍整个链表并在这遍历链表的过程中把链表的每个结点都存放到数组中这样就可以知道数组的长度,然后通过下标就可以直接访问到链表的空间结点。(注意:这种方法是以空间换取时间的方式即额外开辟一块空间存放链表中的每个结点;思路2的空间复杂度是O(N))

(3)思路3:快慢指针法

利用快慢指针遍历链表的相对速度来求出链表的中间结点(注意:这种方法是不额为开辟一块空间而且只遍历1遍链表的;思路3的时间复杂度是O(N))

思路3的详细描述

(1)定义1个指针slow表示慢指针和定义1个指针fast表示快指针,而这两个指针都要去遍历链表;

(2) 在遍历1遍链表的过程中,由于慢指针slow每次只走1步,而快指针fast每次只走2步导致快指针fast在链表走的速度是慢指针slow在链表走的速度的2倍,进而使得快指针fast遍历1遍整个链表的速度是慢指针slow遍历1遍整个链表速度的2倍,而且快慢指针在开始遍历1遍链表时都是从链表的同位置出发即快慢指针都是从链表头结点位置出发,所以当快指针fast指向链表的尾结点的时候即此时快指针fast刚好遍历完1遍整个链表时,此时慢指针slow才遍历了1半的链表而且此时慢指针slow恰好指向链表的中间结点的。

(注意:若链表的总结点数是奇数的话,当快指针走到链表尾结点的时候则此时慢指针slow恰好指向链表的中间位置的结点;若链表的总结点数是偶数的话,当快指针走到链表尾结点后面的空指针NUL的话则此时慢指针slow恰好指向链表的中间位置的第2个结点)

2.1.思路3的代码实现—链表是偶数个结点则下面代码可以找出链表第二个中间结点

(注意:由于思路3只用遍历一遍链表而且时间复杂度又是O(N),所以我们这里着重实现思路3;若链表是偶数个结点则下面代码指针找出链表第二个中间结点)

//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};
//代码1:快慢指针->如果有两个中间结点,则返回第2个中间结点。
//注意:一定要考虑链表结点的个数是奇数还是偶数
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* fast, * slow;
    fast = slow = head;
    //注意:当链表的结点数是偶数个的时候,题目要求的是返回第二个中间结点。
    while (fast && fast->next)//while循环的判断表达式中fast针对的是偶数个结点、fast->next针对的时奇数个结点。
    {
        slow = slow->next;
        fast = fast->next->next;//把fast->next->next赋值给fast就可以使得快指针fast走两步
    }

    return slow;//当while循环结束时,若链表有奇数个结点则慢指针指针slow恰好指向链表的中间结点;若链表有偶数个结点则慢指针指针slow恰好指向链表的第二个中间结点。
}

代码分析

1、对while(fast && fast->next)的循环条件fast && fast->next进行解析:

注意:

①由于不知道链表的总结点数是奇数个还是偶数个,所以while循环条件必须包括判断指针fast是否为空指针NULL和判断指针fast->next是否为空指针NULL;

②若链表的总结点数为偶数的话,则要判断指针fast是否为空指针NULL);

③若链表的总结点数为偶数的话,则要判断指针fast->next是否为空指针NULL;

(1)对循环条件fast && fast->next中的指针fast->next进行解析:

图形解析:

若链表的总结点数为奇数的话,当快指针fast恰好指向链表的尾结点且此时fast->next = NULL的话慢指针slow才能恰好指向链表的中间结点。总的来说,当链表的结点总数是奇数个的时候,while循环必须要判断指针fast->next的值是否为空指针NULL以此来判断慢指针slow是否找到链表的中间结点。

(2) 对循环条件fast && fast->next中的指针fas进行解析:

图形解析:

若链表的总结点数为偶数的话,当快指针fast恰好指向链表尾结点后面的空指针NULL的话慢指针slow才能恰好指向链表的中间结点。总的来说,当链表的结点总数是偶数个的时候,while循环必须要判断指针fast的值是否为空指针NULL以此来判断慢指针slow是否找到链表的第二个中间结点。

2.对slow = slow->next代码进行解析:

把原链表下一个结点的地址slow->next赋值给慢指针slow的目的是让慢指针slow在原链表上走1步即让慢指针slow指向原链表的下一个结点。

3.对fast = fast->next->next代码进行解析:

把原链表下下个结点的地址fast->next->next赋值给快指针fast目的是让快指针fast在原链表上走2步即让快指针fast指向原链表的下下个结点。

4.对return slow代码进行解析:

//注意:此时慢指针slow指向原链表的中间位置即此时指针slow是链表中间结点的地址

利用return slow代码返回链表中间结点的地址slow。

注意事项

(1)若链表有奇数个结点时,当快指针fast恰好指向链表的尾结点且此时fast->next = NULL的话慢指针slow才能恰好指向链表的中间结点;若链表有偶数个结点时,当快指针fast恰好指向链表尾结点后面的空指针NULL的话慢指针slow才能恰好指向链表的中间结点。

(2)该思路利用了快慢指针的相对速度来求出链表的中间结点的。因为快指针fast遍历1遍整个链表的速度是慢指针slow遍历1遍整个链表速度的2倍,所以当快指针fast刚好遍历完1遍整个链表时,此时慢指针slow才遍历完1半的链表进而导致慢指针slow可以找到链表的中间结点。

2.2.拓展代码—若链表是偶数个结点则下面代码可以找出链表第一个中间结点

(注意:下面代码也可以找奇数个结点链表的中间结点)

//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};

//代码2:快慢指针->如果有两个中间结点,则返回第1个中间结点。
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* fast, * slow, * prev;
    fast = slow = head;
    prev = NULL;//指针prev永远指向链表slow位置的前一个结点
    
    while (fast && fast->next)//while循环的判断表达式中fast针对的是偶数个结点、fast->next针对的时奇数个结点。
    {
        prev = slow;
        slow = slow->next;
        fast = fast->next->next;
    }

    if(fast == NULL)//这个if语句处理的是偶数个结点的情况。
        return prev;//当while循环结束时,慢指针指针slow恰好指向链表的第二个中间结点,指针prev指向链表的第一个中间结点。

    if (fast->next == NULL)//这个if语句处理的是奇数个结点的情况。
        return slow; //当while循环结束时,慢指针指针slow恰好指向链表的中间结点。
}

2.3.测试代码

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

//题目:给你单链表的头结点 head ,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。(注意:偶数个结点的链表有两个中间结点)
//注意事项:链表的结点数范围是 [1, 100]。1 <= Node.val <= 100。

//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};

//创建结点
struct ListNode* BuySLTNode(Listdata x)
{
    struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
    if (node == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }

    //对新创建结点的成员进行初始化
    node->val = x;
    node->next = NULL;

    return node;
}

//创建n个结点的链表
struct ListNode* CreateSList(Listdata* arr, int n)
{
    struct ListNode* phead, * tail;//phead指向新创建链表的头结点、tail指向新创建链表的尾结点
    phead = tail = NULL;
    int i = 0;
    for (i = 0; i < n; i++)
    {
        //创建新结点
        struct ListNode* newnode = BuySLTNode(arr[i]);
        //判断phead是否是空链表
        if (phead == NULL)
        {
            phead = tail = newnode;
        }
        else//尾插
        {
            //把尾结点与新创建的结点链接起来
            tail->next = newnode;
            //换尾
            tail = tail->next;//让tail移动到新的尾结点,即此时新创建的结点就是新的尾结点。
        }
    }
    return phead;
}

//链表打印函数
void ListPrint(struct ListNode* phead)
{
    assert(phead);
    struct ListNode* cur = phead;
    while (cur)
    {
        printf("%d->", cur->val);
        cur = cur->next;
    }
    printf("NULL\n");
}

//代码1:快慢指针->如果有两个中间结点,则返回第2个中间结点。
//注意:一定要考虑链表结点的个数是奇数还是偶数
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* fast, * slow;
    fast = slow = head;
    //注意:当链表的结点数是偶数个的时候,题目要求的是返回第二个中间结点。
    while (fast && fast->next)//while循环的判断表达式中fast针对的是偶数个结点、fast->next针对的时奇数个结点。
    {
        slow = slow->next;
        fast = fast->next->next;
    }

    return slow;//当while循环结束时,若链表有奇数个结点则慢指针指针slow恰好指向链表的中间结点;若链表有偶数个结点则慢指针指针slow恰好指向链表的第二个中间结点。
}

//代码2:快慢指针->如果有两个中间结点,则返回第1个中间结点。
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* fast, * slow, * prev;
    fast = slow = head;
    prev = NULL;//指针prev永远指向链表slow位置的前一个结点
    //注意:当链表的结点数是偶数个的时候,题目要求的是返回第二个中间结点。
    while (fast && fast->next)//while循环的判断表达式中fast针对的是偶数个结点、fast->next针对的时奇数个结点。
    {
        prev = slow;
        slow = slow->next;
        fast = fast->next->next;
    }

    if(fast == NULL)//这个if语句处理的是偶数个结点的情况。
        return prev;//当while循环结束时,慢指针指针slow恰好指向链表的第二个中间结点,指针prev指向链表的第一个中间结点。

    if (fast->next == NULL)//这个if语句处理的是奇数个结点的情况。
        return slow; //当while循环结束时,慢指针指针slow恰好指向链表的中间结点。
}

void test()
{
    //测试用例:
    Listdata arr[] = { 1,2,3,4,5 };
    //Listdata arr[] = { 1,2,3,4,1 };
    //Listdata arr[] = { 1,2 };

    //创建n个结点的链表
    struct ListNode* plist = CreateSList(arr, sizeof(arr) / sizeof(Listdata));

    //创建空链表
    //struct ListNode* plist = NULL;//这个也是测试用例

    //找链表的中间结点
    plist = middleNode(plist);

    //打印链表
    ListPrint(plist);
}

int main()
{
    test();
    return 0;
}

二、找链表倒数第K个结点

(注意:下面思路1与思路2都是使用快慢指针法,只是写法不同罢了)

1.思路1

1.1.思路步骤

(1)先让快指针fast往后走k步。
(2)然后再让慢指针slow从链表头结点出发,此后快指针fast和慢指针slow一起往后遍历链表。
(3)直到快指针fast走到链表尾结点后面的空指针NULL时慢指针slow才恰好指向链表倒数第k个结点。

其原理就是,保证快指针fast和慢指针slow在出发前就保持k个结点的距离,此后快指针fast和慢指针slow以相同的速度前进,那么当快指针fast遇到空指针NULL停止时,慢指针slow所在位置就是倒数第k个结点。

1.2.代码

图形解析

代码
//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};

//找倒数第k个结点->注意:这道题不用考虑链表的结点个数是奇数还是偶数个。
//方法1:快慢指针->快指针先走k步,然后快慢指针再以同样的速度走。
struct ListNode* FindKthToTail1(struct ListNode* phead, int k)
{
    //判断指针phead指向的链表是否为空链表
    assert(phead);

    struct ListNode* fast, * slow;
    fast = slow = phead;

    //快指针fast先走k步->目的是:保证快指针fast和慢指针slow在出发前就保持k个结点的距离。
    while (k--)
    {
        //由于k可能比链表的长度len大,从而导致快指针fast在走k步的过程中会变成空指针,为了防止对空指针进行解引用则我们必须判断fast是否为空指针。
        if (fast == NULL)
            return NULL;
        fast = fast->next;//快指针fast往后走一步
    }

    //然后快慢指针再以同样的速度走
    while (fast)
    {
        slow = slow->next;//慢指针slow往后走一步
        fast = fast->next;//快指针fast往后走一步
    }
    return slow;//当while (fast)结束时,此时指针slow恰好指向链表倒数第K个结点。
}

2.思路2

2.1.思路描述

(1)先让快指针fast往后走k-1步。
(2)然后再让慢指针slow从链表头结点出发,此后快指针fast和慢指针slow一起往后遍历链表。
(3)直到快指针fast走到链表尾结点且fast->next = NULL时慢指针slow才恰好指向链表倒数第k个结点。

2.2.代码

图形解析

代码
//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};

//方法2:快慢指针->快指针先走k-1步,然后快慢指针再以同样的速度走。
struct ListNode* FindKthToTail2(struct ListNode* phead, int k)
{
    //判断指针phead指向的链表是否为空链表
    assert(phead);

    struct ListNode* fast, * slow;
    fast = slow = phead;

    //快指针fast先走k-1步
    while (--k)
    {
        //由于k可能比链表的长度len大,从而导致快指针fast在走k步的过程中会变成空指针,为了防止对空指针进行解引用则我们必须判断fast是否为空指针。
        if (fast == NULL)
            return NULL;
        fast = fast->next;//快指针fast往后走一步
    }

    //然后快慢指针再以同样的速度走
    while (fast->next)
    {
        slow = slow->next;//慢指针slow往后走一步
        fast = fast->next;//快指针fast往后走一步
    }
    return slow;//当while(fast->next)循环结束时此时指针slow恰好指向链表倒数第K个结点。
}

2.3代码分析

3.测试代码

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};

//创建结点
struct ListNode* BuySLTNode(Listdata x)
{
    struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
    if (node == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }

    //对新创建结点的成员进行初始化
    node->val = x;
    node->next = NULL;

    return node;
}

//创建n个结点的链表
struct ListNode* CreateSList(Listdata* arr, int n)
{
    struct ListNode* phead, * tail;//phead指向新创建链表的头结点、tail指向新创建链表的尾结点
    phead = tail = NULL;
    int i = 0;
    for (i = 0; i < n; i++)
    {
        //创建新结点
        struct ListNode* newnode = BuySLTNode(arr[i]);
        //判断phead是否是空链表
        if (phead == NULL)
        {
            phead = tail = newnode;
        }
        else//尾插
        {
            //把尾结点与新创建的结点链接起来
            tail->next = newnode;
            //换尾
            tail = tail->next;//让tail移动到新的尾结点,即此时新创建的结点就是新的尾结点。
        }
    }
    return phead;
}

//链表打印函数
void ListPrint(struct ListNode* phead)
{
    assert(phead);
    struct ListNode* cur = phead;
    while (cur)
    {
        printf("%d->", cur->val);
        cur = cur->next;
    }
    printf("NULL\n");
}


//找倒数第k个结点->注意:这道题不用考虑链表的结点个数是奇数还是偶数个。
//方法1:快慢指针->快指针先走k步,然后快慢指针再以同样的速度走。
struct ListNode* FindKthToTail1(struct ListNode* phead, int k)
{
    //判断指针phead指向的链表是否为空链表
    assert(phead);

    struct ListNode* fast, * slow;
    fast = slow = phead;

    //快指针fast先走k步
    while (k--)
    {
        //由于k可能比链表的长度len大,从而导致快指针fast在走k步的过程中会变成空指针,为了防止对空指针进行解引用则我们必须判断fast是否为空指针。
        if (fast == NULL)
        {
            printf("输入的k > 链表的长度len!\n");//注意:这句话可以加可不加。因为fast是走k步所以只有输入的k > len才会打印这句话。
            return NULL;
        }
        fast = fast->next;
    }

    //然后快慢指针再以同样的速度走
    while (fast)
    {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

//方法2:快慢指针->快指针先走k-1步,然后快慢指针再以同样的速度走。
struct ListNode* FindKthToTail2(struct ListNode* phead, int k)
{
    //判断指针phead指向的链表是否为空链表
    assert(phead);

    struct ListNode* fast, * slow;
    fast = slow = phead;

    //快指针fast先走k-1步
    while (--k)
    {
        //由于k可能比链表的长度len大,从而导致快指针fast在走k步的过程中会变成空指针,为了防止对空指针进行解引用则我们必须判断fast是否为空指针。
        if (fast == NULL)
        {
            printf("输入的k > 链表的长度len + 1!\n");//注意:这句话可以加可不加。因为fast是走k-1步所以只有输入的k > len + 1才会打印这句话。
            return NULL;
        }
        fast = fast->next;
    }

    //然后快慢指针再以同样的速度走
    while (fast->next)
    {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

void test()
{
    //测试用例:
    Listdata arr[] = { 1,2,3,4,5,6,7 };

    //创建n个结点的链表
    struct ListNode* plist = CreateSList(arr, sizeof(arr) / sizeof(Listdata));

    //创建空链表
    //struct ListNode* plist = NULL;//这个也是测试用例

    //输入k
    int k = 0;
    while (printf("请输入你要查找链表倒数第几个结点:>"), scanf("%d", &k) != EOF)//只要按多几次Ctrl + Z就可以结束scanf的多组输入。
    {
        //找链表的倒数第k个结点
        struct ListNode* node = FindKthToTail2(plist, k);
        if(node != NULL)
        printf("单链表倒数第k个结点是:%d \n", node->val);
    }
}


int main()
{
    test();
    return 0;
}

4.总结

4.1.对思路1进行小结

(1)该思路利用了快慢指针的相对距离来求出链表的中间结点的。因为快指针fast遍历1遍整个链表的速度和慢指针slow遍历1遍整个链表的速度是一样的,但是快慢指针一开始是从链表的不同位置出发的,而慢指针slow一开始是从链表的头结点位置出发,而快指针fast一开始是从链表的第k个结点位置出发,所以当快指针fast刚好遍历完1遍整个链表时,此时慢指针slow才遍历到链表的倒数第k个位置。

(2)一般遍历数组才用for循环,而遍历链表一般用的是while循环。

(3)while(k--)循环的次数是k次,而)while(--k)循环的次数是k-1 次。

(4)一定要考虑k有可能比链表的长度要长的情况。

4.2.求链表中间结点和求链表倒数第k个结点进行对比

(1).求链表的中间结点利用快慢指针的思路:(快慢指针同起点,不同速度)

该题思路利用了快慢指针的相对速度来求出链表的中间结点的。这题在设计快慢指针时,设计快指针fast在链表走的速度是慢指针slow在链表走的速度的两倍而且快慢指针一开始都是从链表的头结点开始遍历链表的。(总的来说,快指针fast走的速度是慢指针slow走的速度的两倍,快慢指针在链表头结点的位置同地出发)

(2)求链表倒数第k个结点利用的快慢指针的思路:(快慢指针不同起点,同速度)

该题思路利用了快慢指针的相对距离来求出链表倒数第k个结点的。这题在设计快慢指针时,设计快指针fast在链表走的速度和慢指针slow在链表走的速度是一样的,而且慢指针slow一开始是从链表的头结点开始遍历链表,而快指针fast一开始是从链表的第k个结点开始遍历链表或者快指针fast一开始是从链表的第k-1个结点开始遍历链表。(总的来说,快指针fast走的速度和慢指针slow的走的速度是一样的,快慢指针在链表的不同位置出发,慢指针slow从链表的头结点位置出发,快指针fast从链表的第k个结点位置出发或者快指针fast从链表第k-1个结点位置出发)

三、判断是否是带环链表

. - 力扣(LeetCode)

1.解题思路

思路描述:快慢指针即慢指针一次走一步,快指针一次走两步,两个指针从链表头结点位置同时开始遍历链表,如果链表带环则快慢指针一定会在环中相遇;若快指针率先走到链表的末尾则此时说明这个链表不是带环链表。

(注意:假设链表不是带环链表,若是偶数个结点的链表时只有快指针fast指向链表尾结点后面的空指针NULL才能证明链表不是带环链表;若是奇数个结点的链表时只有快指针fast指向链表尾结点且fast->next = NULL才能证明链表不是带环链表)


2.扩展问题

(1)问题1:为什么快指针每次走1步,慢指针走2步,他们一定会在环中相遇呢?

图形解析


假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。最好的情况是当慢指针刚进环时可能就和快指针相遇了;最坏情况下是慢指针刚进环时快慢指针之间的距离N恰好是环的周长C(注意:最坏的情况下实际N != C而是N = C - 1,若是N = C快慢指针不就相遇了吗)。即使在最坏的情况下,由于快指针每次走2步而慢指针每次走1步使得两个指针在环中每移动一次,它们之间的距离N就减少1,直到N减少到0为止则此时快慢指针在环中相遇,因此在慢指针走上一圈之前,快指针肯定是可以追上慢指针的。


(2)问题2:慢指针一次走1步,而快指针一次走3步,走4步,...n步行会使得快慢指针在环中相遇吗?

假设慢指针进环的时候,快指针和慢指针之间的距离为N,在他们追击的过程中,快指针每次走3步而慢指针每次走1步则此时快慢指针的相对速度是2,所以快慢指针每移动一次则他们之间的距离N就会减少2。

分两种情况:
1、若N为偶数,则他们之间的距离最终会减为0,即相遇。

2、若N为奇数,则他们之间的距离会减到1,然后减到-1,减到-1也就意味着他们之间的距离N又变为了C-1(即最坏情况下快慢指针的距离为N = C - 1 ),则此时又分以下两种情况:


①若C为奇数,则N = C-1为偶数,由于快慢指针每移动一次他们之间的距离N就会减少2,当N = 0时快慢指针就会相遇。
②若C为偶数,则N = C-1还是为奇数,也就是说快慢指针之间的距离还是为奇数,他们永远不会相遇。

总结:当慢指针走1步,快指针走3步时。若慢指针进环时与快指针之间的距离N为奇数,并且环的周长C恰好为偶数,那么他们会一直在环里面打转转,永远不会相遇。(当慢指针走1步,快指针走4步或是走n步时,证明过程类似)

3.代码

#include<stdio.h>
#include<stdbool.h>

//题目描述:给你一个链表的头节点 head ,判断链表中是否有环。

//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};

//方法1——快慢指针法:快慢指针即慢指针一次走1步,快指针一次走2步,两个指针从链表头结点位置同时开始遍历链表,如果链表带环则快慢指针一定会在环中相遇;若快指针率先走到链表的末尾则此时说明这个链表不是带环链表。

bool hasCycle(struct ListNode* head)
{
    struct ListNode* fast = head, * slow = head;
    //当我们用快慢指针同时遍历链表时,由于fast比slow遍历链表的速度快,所以我们只需判断fast与fast->next其中一个是否遇到NULL来以此判断链表是否是环形链表,若fast或者fast->next其中一个遇到NULL则说明链表不是环形链表,所以我们可以利用while(fast && fast->next)判断链表不是环形链表这一情况。
    //注意:若链表不是环形链表的话,则当链表是偶数个结点时只有fast会遇到NULL;当链表是奇数个结点时只有fast->next会遇到NULL。总的来说,while循环判断表达式中的fast针对的是判断偶数个结点的链表是否是环形链表,而fast->next针对的是判断奇数个结点的链表是否是环形链表)。
    while (fast && fast->next)//当fast或者fast->next其中一个遇到空指针NULL就意味着链表不是环形链表并且while循环就结束,所以我们要用逻辑与&&操作符来表达fast或者fast->next其中一个遇到空指针NULL就使得while循环结束。
    {
        fast = fast->next->next;//快指针fast走2步
        slow = slow->next;//慢指针slow走1步
        //由于快指针fast遍历链表的速度是慢指针slow的2倍,若快指针fast在链表中遇到慢指着slow则说明链表是带环链表。
        if (fast == slow)
            return true;
    }

    //若while循环结束了,则说明链表不是带环链表。
    return false;
}

四、求带环链表的第一个入环结点

. - 力扣(LeetCode)

1.思路1

1.1.思路1步骤

(1)步骤1:先利用快慢指针法判断链表是否是带环链表。若是带环链表才进行步骤2。

(2)步骤2:若链表是带环链表则让一个指针head从链表起始位置(即头结点位置)开始遍历链表,同时让另一个指针meet从判环时快慢指针的第一个相遇点的位置开始绕环遍历链表,指针head和指针meet都是每次均走一步,最终指针head和指针meet肯定会在环入口点的位置相遇,返回head和meet的相遇点就是我们要找的环入口点。

1.2.思路1证明 (对下面结论进行证明)

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

 1.3.思路1代码

#include <stdio.h>

//题目描述:给定一个链表的头结点 head,返回链表开始入环的第一个结点。如果链表无环,则返回 null。

//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};

//找环形链表的开始入环的第一个结点函数
//代码1:快慢指针法。
struct ListNode* detectCycle(struct ListNode* head)
{
    struct ListNode* fast = head, * slow = head;
    //1.先利用快慢指针法来判断链表是否是环形链表,若确定是环形链表后再找链表开始入环的第一个结点。
    while (fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        //1.1.判断链表是否是环形链表
        if (fast == slow)//若fast = slow,说明链表是环形链表,则此时需要找出开始入环的第一个结点。
        {
            //1.2.找链表开始入环的第一个结点
            struct ListNode* meet = slow;//让指针meet指向快慢指针在环的相遇点。
            while (head != meet)
            {
                meet = meet->next;//让指针meet从相遇点开始遍历链表
                head = head->next;//让指针head从头结点开始遍历链表
            }
            return meet;//若head = meet则说明此时meet就是开始入环的第一个结点
        }
    }

    return NULL;//当fast = NULL 或者fast->next = NULL则说明链表不是环形链表则此时要返回NULL表示链表无环。
}

2.思路2

2.1.思路描述

图形解析

思路2:先利用快满指针fast和slow找到原链表的相遇点,然后利用指针meet指向原链表的相遇点,同时把原链表相遇点位置的下一个结点作为另外一个新链表的头结点并且用指针otherHead指向这个新链表的头结点(即指针otherHead指向的原链表相遇点位置的下一个结点作为新链表的头结点),然后把指针meet->next设置为空指针NULL的目的是把指针meet指向的原链表相遇点与指针otherHead指向的新链表链表头结点断开链接,断开链接后,原链表变成了一个相交链表(注意:这个相交链表是由指针head指向的链表和指针otherHead指向的新链表等这两个链表组成),则此时相交链表的第一个相交结点就是带环链表的第一个入环结点,所以只要利用getIntersectionNode函数找到相交链表的第一个相交结点就是这个带环链表的第一个入环结点。

注意:思路2的本质是:把找出带环链表的第一个入环结点的问题转换成找出相交链表的第一个相交结点的问题。

2.2.代码

#include <stdio.h>

//题目描述:给定一个链表的头结点 head,返回链表开始入环的第一个结点。如果链表无环,则返回 null。

//链表存放的数据类型
typedef int Listdata;

//链表结点的结构体类型
struct ListNode
{
    Listdata val;
    struct ListNode* next;
};

//代码2:快慢指针 + 找相交链表的第一个相交结点
//找相交链表的第一个交点的函数
struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB)
{
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;

    int lenA = 0;
    int lenB = 0;

    //计算两条链表的长度
    while (curA->next)
    {
        lenA++;
        curA = curA->next;
    }

    while (curB->next)
    {
        lenB++;
        curB = curB->next;
    }
    //若两条链表的尾结点的地址都不相等则说明这两条不是相交链表
    if (curA != curB)
        return NULL;

    int gap = abs(lenA - lenB);
    struct ListNode* shortList = headA;
    struct ListNode* longList = headB;
    if (lenA > lenB)
    {
        shortList = headB;
        longList = headA;
    }

    //长链表先走gap步
    while (gap--)
    {
        longList = longList->next;
    }

    //同时遍历两条链表,找出第一个相同的结点就是第一个相交结点。
    //找第一个相交结点写法1:
   /* while (1)
    {
        if (longList == shortList)
            return longList;
        else
        {
            longList = longList->next;
            shortList = shortList->next;
        }
    }*/

    //找第一个相交结点写法2
    while (longList != shortList)
    {
        longList = longList->next;
        shortList = shortList->next;
    }
    return longList;

}

//找环形链表的开始入环的第一个结点的函数
struct ListNode* detectCycle(struct ListNode* head)
{
    struct ListNode* fast = head, * slow = head;
    //先利用快慢指针法来判断链表是否是环形链表,若确定是环形链表后再找链表开始入环的第一个结点。
    while (fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;

        if (fast == slow)//判断链表是否是环形链表。若fast = slow,说明链表是环形链表。
        {
            struct ListNode* meet = slow;//让指针meet指向快慢指针在环的相遇点

            //找快慢指针的相遇点的下一个结点作为新链表头结点
            struct ListNode* otherHead = meet->next;

            //把相遇点与下一个结点断开链接,使得环形链表变成一个相交链表则此时我们只要找相交链表的第一个相交结点就是我们要找的链表开始入环的第一个节点。
            meet->next = NULL;
            
            return getIntersectionNode(head, otherHead);//利用getIntersectionNode找相交链表的第一个交点
        }
    }

    return NULL;//若链表不是环形链表则要返回NULL表示没有找到链表开始入环的第一个节点。
}

2.3.对思路2进行总结

(1)解析只有把相遇点断开才能把环形链表变成相交链表的原因:

若链表是环形链表。虽然只需要把环中的任意一个结点中的成员变量指针next设置为空指针NULL后就可以把原链表变成一个相交链表,但是我们无法确定此时我们是否在环中,所以我们必须通过快慢指针法在原链表中找到快慢指针在环的相遇点才能确定我们此时正在环中,由于相遇点是环中的结点所以在找到相遇点之后一定把相遇点的成员变量meet->next设置为NULL就可以把环形链表的环断开进而使得环形链表变成一个相交链表。

(注意:必须是把处于环中的任意一个结点的成员变量指针nest设置为NULL后得到的相交链表中的第一个相交结点才是带环链表的第一个入环结点)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值