【数据结构】环形链表问题(链表带环)

本文详细介绍了如何使用快慢指针判断链表是否为环形链表,以及如何找到环形链表中的入环结点。通过实例分析不同步长情况下相遇条件,并提供了两种OJ解题思路:一是利用环形链表的特性重新构造相交链表,二是直接在原链表上操作,避免修改链表结构。

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

目录


前言

单链表带环是链表中比较经典的问题,重要是推导和证明


一、什么是环形链表?

链表中最后节点的next不是链接到NULL,而是链接到该链表中任一节点包括自己
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、如何判断链表是否是环形链表呢

我们这里运用到了快慢指针

思路:
slow走一步,fast走两步,如果没有环,fast在会NULL或者是fast->next为NULL就结束(取决于链表的长度),有环fast和slow就会相遇
三种情况:

  1. fast进环(在环入口),slow走了一半
  2. slow进环(在环入口),fast已经在环内走了一段路(这里取决于环的长度,可能走了几圈了,可能一圈都没有走完)
  3. fast开始追击slow,直到最后相遇
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

1. 假设slow刚进环时,fast与slow之间的距离为N,环的长度为C

  1. slow每次走1步,fast每次走2步,一定可以相遇吗?
  2. slow每次走1步,fast每次走3步,一定可以相遇吗?
  3. slow每次走1步,fast每次走4步,一定可以相遇吗?

2.slow每次走1步,fast每次走2步,一定可以相遇吗?
每次追击,fast和slow的距离就在减1,N,N-1,N-2,…,0,最后一定能相遇
所以这种情况一定能相遇
在这里插入图片描述
在这里插入图片描述
3.slow每次走1步,fast每次走3步,一定可以相遇吗?
每次追击,fast和slow距离就减2,就需要对N分情况
(1) N是偶数,N-2,N-4,N-6,…,0,那就可以相遇
(2) N是奇数,N-2,N-4,N-6,…,1,-1。-1的意思是fast刚好超过了slow1步,现在fast就需要追C-1的长度,相当于C-1 = N,现在能否相遇就取决于C的长度

  • 如果C-1是偶数,就可以相遇
  • 如果C-1是奇数,就永远不会相遇,因为C-1是奇数,fast追击还是会超1步,陷入死循环了,永远都超slow1步

在这里插入图片描述

4.slow每次走1步,fast每次走4步,一定可以相遇吗?
每次追击,fast和slow距离就减3,就需要对N分情况
(1)N是3的倍数,N-3,N-6,N-9,…,0,那就可以相遇
(2)N不是3的倍数,需要对fast和slow相差的步数分情况

  • 最后fast超slow1步,C-1 = N,就是C-1必须要是3的倍数,才能相遇,否则永远不相遇
  • 最后fast超slow2步,C-2 = N,就是C-2必须要是3的倍数,才能相遇,否则永远不相遇

总结:只有slow走1步,fast走2步,每步差距为1的情况下,一定能相遇,其他情况每步差距 > 1,取决于N(fast和slow相差的距离)和C(环的长度)。

四、OJ:环形链表 I

OJ链接
思路:
通过快慢指针,slow走1步,fast走2步,是环形链表两个指针一定能相遇,fast=NULL或fast->next=NULL,就不是环形链表

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 
bool hasCycle(struct ListNode *head) {
    struct ListNode* slow, *fast;
    slow = fast = head;
    
    // 快慢指针
    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;

        // 相遇的话就是环形链表
        if (fast == slow)
            return true;
    }
    // 循环走完了,就说明不是环形链表
    return false;
}

五、环形链表入环结点

带环问题还是用slow走1步,fast走2步,他们两个肯定会相遇,所以有一个相遇点meet,而fast的步数是slow的步数的2倍,得:

fast走的距离 = 2 * slow走的距离

为什么x是slow在环内走的这段距离,而不是一圈?

因为slow不可能在环内走超过一圈,就是在一圈内fast绝对能遇到meet,slow走了1圈,fast都走了2圈,因为fast每次走是slow的2倍,(两个指针的差距)N绝对比C小,追的过程差距是逐步-1,不会错过。

在这里插入图片描述
还需要说明下slow进环的时候,fast在环内走了多久呢,这是有两种情况

  • 当L很长C比较小,fast在环内假设走了很多圈(假设N圈)。
    在这里插入图片描述

  • 当L很短C比较大,fast在环内一圈都没有走。
    在这里插入图片描述

fast走的距离:

  • slow指针走的两倍
  • L+N*C+X (N >= 1)
    通过这些结论得出关系距离等式:
2(L+X) = L + N*C + X
L = N*C - X
分解:L = (N-1)C + C - X

把N*C 分解成 (N-1)C + C,其中(N-1)C 就是meet又回到meet,绕了几圈又回到了原来的位置,其中C比较大,N = 1,就等于没有走。N > 1的话就等于走了但是最终还是回到原点,所以N >= 1的情况是一样的

最后的结论是L = C-X,C-X是meet到环入口的距离,这段距离和L到环入口的距离是一样的。

meet——入环口的距离 = L——入环口的距离
所以从这两个点匀速运行,最后相等的地方就是入环口(这里相当于链表的相交)

主要的原因就是把X的距离给去掉了,x是slow在环入口到环内走了一段的距离,X的起点是入环口,这是最关键的,起定位的作用,不管X有多长,用C-X后得出一段距离的终点是入环点,所以最后会走到环入口的时候相遇

六、OJ:环形链表 II

OJ链接
方法1思路:按照我们刚才的证明和分析,两个结点一个从头开始走,另一个从相遇点meet开始走,最终就可以找到入环口

在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* slow, *fast;
    slow = fast = head;
    
    // 快慢指针
    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        
        // 找到相遇点meet
        if (slow == fast)
        {
            struct ListNode* meetNode = slow;
            struct ListNode* cur  = head;
           
            // 一个从头(head)开始走,另一个从相遇点(meetNode)开始
            while (meetNode != cur)
            {
                cur = cur->next;
                meetNode = meetNode->next;
            }
            
            // 相等后,就说明是找到了入环口,随便返回其一
            return meetNode;
        }
    }
    // 链表走完了,就说明不是环形链表
    return NULL;
}

方法2思路:
链表相交不需要证明和推导,直接求解,此方法可以分成两种解法

  • 第一种:找到相遇点后,把相遇点断开,把meet的next设为新链表的头,meet的next设为NULL,直接断开环。然后就成了链表相交问题了,但是这修改环形链表的结构,OJ上面说明不能修改。

在这里插入图片描述

  • 第二种:找到相遇点后,把相遇点设为尾,把meet的next设为新链表的头,就成了链表相交问题,没有修改链表结构
    在这里插入图片描述
    我的解法是第二种,不修改链表的结构,两种方法大同小异,本质上都是相交链表
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* slow, *fast;
    slow = fast = head;
    
    // 快慢指针判断是否是环
    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        
        // 找到相遇点meet
        if (slow == fast)
        {
            struct ListNode* meetNode = slow;  // 假设为尾
            struct ListNode* A = head;
            struct ListNode* B = meetNode->next;    // 新链表
            struct ListNode* tailA = A, *tailB = B;
            int lenA = 0, lenB = 0;

            // 两个链表到meetNode的长度
            while (tailA != meetNode)
            {
                lenA++;
                tailA = tailA->next;
            }
            while (tailB != meetNode)
            {
                lenB++;
                tailB = tailB->next;
            }

            // 长链表先走gap步
            struct ListNode* longList = A, *shortList = B;
            int gap = abs(lenA - lenB);
            if (lenA < lenB)
            {
                longList = B;
                shortList = A;
            }
            while (gap--)
                longList = longList->next;

            // 相等就找到交点了,也就是第一个入环点
            while (longList != shortList)
            {
                shortList = shortList->next;
                longList = longList->next;
            }
            return shortList;
        }
    }
    return NULL;
}
<think>好的,我现在需要向用户介绍“数据结构逆向建立单链表带头结点”的方法。首先,我得回忆一下单链表的基本结构和常见的建立方式。通常,建立单链表有头插法和尾插法两种方法,而逆向建立应该是指头插法,因为头插法生成的链表顺序与输入顺序相反。 用户提到的是“带头结点”的情况,所以我需要先明确头结点是什么。头结点是一个不存储实际数据的节点,位于链表的头部,用于简化操作,比如统一插入和删除的处理逻辑。 接下来,我需要详细说明逆向建立的步骤。首先,创建头结点,并初始化其next指针为NULL。然后,每次新节点都插入在头结点之后,成为新的第一个有效节点。这样,最后形成的链表的顺序与输入顺序相反,比如输入1、2、3,链表顺序是3->2->1->NULL。 需要对比头插法和尾插法的区别,强调头插法在逆向建立中的效率优势,即每次插入都在头部,时间复杂度是O(1),而尾插法需要遍历到尾部,时间复杂度是O(n)。 然后,我需要给出具体的代码示例,用C语言来描述。代码部分要包括结构体定义,创建头结点,循环插入新节点的过程,并注意释放内存,避免内存泄漏。同时,要确保代码的正确性,比如检查malloc是否成功,指针操作是否正确。 可能用户还会关心应用场景,比如需要逆序处理数据时,头插法就很有用。或者在某些算法中,如链表的逆置,头插法也是一个常用的技巧。 另外,需要注意说明带头结点的好处,比如在删除第一个有效节点时不需要特殊处理,因为头结点始终存在,这样代码更简洁,减少出错可能。 最后,要提醒用户注意内存管理,尤其是在实际编程中,每次malloc之后要记得free,防止内存泄漏。同时,可以给出一个完整的示例代码,展示从输入数据到建立链表,再到遍历输出结果的整个过程,帮助用户更好地理解。 总结下来,我需要分步骤讲解逆向建立的过程,结合代码示例,说明其原理和优势,并指出应用中的注意事项。这样用户能够全面理解这一方法,并能在实际中正确应用。</think>### 数据结构逆向建立单链表(带头结点)详解 #### 基本概念 **逆向建立单链表**(头插法)指将新节点**始终插入头结点之后**,使得最终链表的顺序与输入顺序相反。带头结点的单链表通过**统一插入/删除操作**简化代码逻辑。 --- #### 实现步骤 1. **初始化头结点** ```c Node *head = (Node*)malloc(sizeof(Node)); head->next = NULL; // 头结点初始指向空 ``` 2. **循环插入新节点** ```c for (int i = 0; i < n; i++) { Node *newNode = (Node*)malloc(sizeof(Node)); newNode->data = input_data; // 输入数据 // 关键操作:新节点插入头结点之后 newNode->next = head->next; // 新节点接管原首节点 head->next = newNode; // 头结点指向新节点 } ``` --- #### 动态演示(输入顺序:1→2→3) | 步骤 | 链表状态 | 说明 | |------|-------------------|----------------------| | 初始 | `头→NULL` | 空链表 | | 插入1| `头→1→NULL` | 头结点直接指向新节点 | | 插入2| `头→2→1→NULL` | 新节点插入在头结点后 | | 插入3| `头→3→2→1→NULL` | 最终形成逆序链表 | --- #### 核心特点 1. **时间复杂度**:O(1) 每次插入(无需遍历链表尾部) 2. **空间复杂度**:O(n) 存储n个数据节点 3. **顺序特性**:`输出顺序`与`输入顺序`**相反** --- #### 完整C语言实现 ```c #include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; // 逆向建立带头结点的单链表 Node* createReverseList(int arr[], int n) { Node *head = (Node*)malloc(sizeof(Node)); head->next = NULL; for (int i = 0; i < n; i++) { Node *newNode = (Node*)malloc(sizeof(Node)); if (!newNode) exit(1); // 内存分配失败处理 newNode->data = arr[i]; newNode->next = head->next; // 新节点指向原首节点 head->next = newNode; // 头结点指向新节点 } return head; } // 打印链表 void printList(Node *head) { Node *p = head->next; while (p) { printf("%d → ", p->data); p = p->next; } printf("NULL\n"); } int main() { int arr[] = {1, 2, 3, 4, 5}; Node *list = createReverseList(arr, 5); printList(list); // 输出:5 → 4 → 3 → 2 → 1 → NULL return 0; } ``` --- #### 典型应用场景 1. **逆序数据处理**:如将输入序列倒序存储 2. **链表反转**:配合遍历可实现O(n)时间复杂度反转 3. **栈的链式实现**:符合后进先出(LIFO)特性 --- #### 注意事项 1. **内存释放**:使用后需遍历释放所有节点 2. **边界处理**:空链表时`head->next == NULL` 3. **头结点优势**:统一插入/删除操作逻辑,避免对首节点的特殊处理 通过头插法建立的逆序链表在需要快速插入和逆序访问时具有显著优势,是链式结构的基础操作之一。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值