【数据结构与算法】链表2W字终极无敌总结(二)

简介: 【数据结构与算法】链表2W字终极无敌总结(二)

4. 链表成环问题


4.1 给定一个链表,判断链表中是否有环


由于有扩展问题,我们先解决题目:


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


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

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


示例 1:

微信图片_20230224165115.png


输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

微信图片_20230224165122.png


输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

微信图片_20230224165126.png


输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos-1 或者链表中的一个 有效索引


进阶: 你能用 O(1)(即,常量)内存解决此问题吗?


对于成环问题,如果是环,那么链表迭代的过程中不会截止,但是我们不能根据是否截止进行判断是不是环,这样只会运行超时,因此,需要采用一定的特殊技巧:


利用快慢指针,即快指针一次走两步,慢指针一次走一步,当慢指针进入环后,转化思想为快指针追赶慢指针:根据相对运动,每次移动快指针都会离慢指针更进一步,这就使得二者一定会在圈中相遇。即为真。

如果不是环,快指针一定先走到末端。


bool hasCycle(struct ListNode *head) {
    if(head == NULL)
        return false;
    struct ListNode* slow = head,*fast = head;
    while(fast && fast->next)//一次走两步,防止出现野指针需要判断两个条件
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            return true;
        }
    }
    return false;
}


【扩展问题】


为什么快指针每次走两步,慢指针走一步可以?

假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。


微信图片_20230224165520.png

快指针一次走3步行吗?


不一定。慢指针在进圈后,快指针以相对慢指针两个单位两个单位的追赶,如果N为奇数(N代表两个指针之间的距离),距离就会变成:N-2,N-4,……3,1,-1。当变成-1时,又是一个新的开始,此时二者相距C-1个长度,C为环的周长,如果c-1是奇数,那么就永远不会相遇,因此不一定。

快指针一次走M步行吗?


对此,可与一次走三步的类似,需要看N与C的关系。

慢指针一次走N步,快指针一次走M步行吗?(M>N)


只要是在环中,并且M比N大1个单位,那么就可以认为快指针相对慢指针靠近一步,这样相当于遍历所有可能性,一定会相遇。


总结:只要fast比slow快一步,无论他们一次走几个单位,都一定可以相遇。


4.2返回入环的第一个结点


对于这种类型的,先证明一下无疑是最好的学习方式:


微信图片_20230221175402.png


假设进环前的长度是L,假设环的长度是C,假设入口点到相遇点距离为X


1.公式推导:

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


slow走的距离:L+X;


fast走的距离:L+N*C+X;(fast转了N圈,N>=1)


注: 为什么slow走的不是L+n*C+X呢? 即为什么slow在圈里一定走了不到一圈就相遇了呢?我们知道当slow刚刚进圈时slow与fast之间的距离一定小于C-1,fast一次走两步,slow一次走一步,距离逐渐减小,即一定走了小于C-1次就会相遇,因此推出此时slow走了不到一圈。


即:根据二倍关系:2(L+X) = L+X+N*C,即L = N * C - X;进一步得出:

L = ( N − 1 ) ∗ C + C − X L = (N-1)*C+C-X

L=(N−1)∗C+C−X


结论:一个指针A从头开始走,一个指针B从相遇点开始走,他们会在入口点相遇。


2.转化成相交问题

当我们通过快慢指针找到相遇点记录下来以后,可以想象把此相遇节点与下一节点断开,记录下一个节点为链表B的头,并记录起始位置为链表A的头,这样通过相交链表的方法,就能求得入环的第一个节点,也就是链表的第一个交点


微信图片_20230221175519.png


那么我们可以尝试解决这道题目:


142. 环形链表 II


给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。


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


不允许修改 链表。

示例 1:

微信图片_20230224165650.png


输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。


示例2:


微信图片_20230224165654.png


输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。


示例 3:


微信图片_20230224165657.png


输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。


提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

进阶: 你是否可以使用 O(1) 空间解决此题?


  • 公式推导法:


struct ListNode* hasCycle(struct ListNode *head) {
    if(head == NULL)
        return NULL;
    struct ListNode* slow = head,*fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            return slow;
        }
    }
    return NULL;
}
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* meet = hasCycle(head);
    if(meet == NULL)
        return NULL;
    struct ListNode* begin = head;
    while(1)
    {
        if(begin == meet)
        {
            return begin;
        }
        begin = begin->next;
        meet = meet->next;
    }
    return NULL;
}

微信图片_20230224165911.png


相交法:


struct ListNode* hasCycle(struct ListNode *head) {
    if(head == NULL)
        return NULL;
    struct ListNode* slow = head,*fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            return slow;
        }
    }
    return NULL;
}
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    if(headA == NULL ||headB == NULL)
        return NULL;
    struct ListNode* curA = headA, *curB = headB;
    int lenA = 1;
    //找尾节点
    while(curA)
    {
        curA = curA->next;
        ++lenA;
    }
    int lenB = 1;
    while(curB)
    {
        curB = curB->next;
        ++lenB;
    }
    struct ListNode* longList = headA, *shortList = headB;
    if(lenA<lenB)
    {
        longList = headB;
        shortList = headA;
    }
    //长的链表先走差距步
    int gap = abs(lenA-lenB);
    while(gap--)
    {
        longList = longList->next;
    }
    while(longList!=shortList)
    {
        longList = longList->next;
        shortList  = shortList->next;
    }
    return longList;
}
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* meet = hasCycle(head);
    if(meet == NULL)
        return NULL;
    struct ListNode* newheadB = meet->next;
    meet->next = NULL;//必须断开,否则在求相交链表在求长度时会死循环。
    struct ListNode* newheadA = head;
    struct ListNode* newmeet = getIntersectionNode(newheadA,newheadB);
    meet->next = newheadB;//恢复环
    return newmeet;
}

微信图片_20230224170008.png

相信这个代码大家已经看出来了,复用的两个函数不正是判断是否有环的函数和相交链表的函数吗?只不过判断是否有环的函数的返回值稍微改了一下,因此,只要掌握思路,写出的代码一定是有联系的。

5. 复制带随机指针的链表


138. 复制带随机指针的链表

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。


构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。


例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。


返回复制链表的头节点。


用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:


val:一个表示 Node.val 的整数。

random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。

你的代码 只 接受原链表的头节点 head 作为传入参数。


示例 1:


微信图片_20230224170056.png


输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]


示例 2:


微信图片_20230224170100.png


输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

微信图片_20230224170104.png


输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]


提示:


0 <= n <= 1000

-104 <= Node.val <= 104

Node.random 为 null 或指向链表中的节点。

这是一道有挑战性的题目。因此,我把他单拿出来放在这里。这道题本身的难度在于random指针,通过常规暴力的方法,我们需要的是将每一个random指针的位置给记录下来,从而当处理拷贝链表的过程中,再利用双层循环将每个特定的位置的random指向这个拷贝链表相对应的位置,但是为什么不能根据val值从而链接呢?因为val的值本身是允许重复出现的,只有通过具体位置才能锁定,因此需要创建数组来记录位置(思路清晰,实现起来繁琐,自己也是想了好久),下面的代码实现就是这样的思路:


1.暴力解决


/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
struct Node* BuySLTNode(struct Node* node)
{
    struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
    newnode->next = NULL;
    newnode->random = NULL;
    newnode->val = node->val;
    return newnode;
}
struct Node* copyRandomList(struct Node* head) {
    //创建复制的链表
    struct Node* newhead = (struct Node*)malloc(sizeof(struct Node));
    newhead->next = NULL;
    struct Node* ntail = newhead;
  struct Node* cur = head;
    while(cur)
    {
        ntail->next = BuySLTNode(cur);
        ntail = ntail->next;
        cur = cur->next;
    }
   // 记录节点个数,
    cur = head;
    int n = 1;
    while(cur)
    {
        n++;
        cur = cur->next;
    }
    cur = head;
    //将位置记录到count数组里,count数组的每一个元素记录该节点random指针指向的位置
    struct Node* repcur = cur;
    int* count = (int*)calloc(n,sizeof(int));//开辟数组记录每一个random指向的数据的位置
    int i = 0;
    while(cur)
    {
        int order = 0;
        while(repcur)
        {
            if(cur->random == NULL)
            {
                count[i++] = n-1;
                break;
            }
            if(cur->random == repcur)
            {
                count[i++] = order;
                break;
            }
            order++;
            repcur = repcur->next;
        }
        repcur = head;
        cur = cur->next;
    }
    i = 0;
    //通过之前的数组找到复制之后的位置
    struct Node* newcur = newhead->next;
    struct Node* newcur1 = newhead->next;
    while(newcur)
    {
        int j=0;
        while(newcur1)
        {
           if(j == count[i])
           {
               newcur->random = newcur1;
               break;
           }
           j++;
           newcur1 = newcur1->next;
        }
        newcur1 = newhead->next;
        newcur = newcur->next;
        i++;
    }
    free(count);
    return newhead->next;
}


微信图片_20230224170349.png


2.在链表本身进行拷贝


再次本着无优解博客不写的原则,既然暴力的解决了,那为什么还要有优解呢? emm……这是一个很痴呆的问题,有好的方法,谁会不愿意用呢?那下面,我们就来说说这个美妙的方法:


上文提到了,难点在于random指针的拷贝,但我们又根据原链表很清晰的知道random指向的位置,因此,我们就要靠着原链表,在原链表的基础上进行改造:


/

微信图片_20230224170417.png

在此基础上进行改造:

微信图片_20230224170420.png


在每一个节点的后方,拷贝一个与该节点一模一样的节点(当然地址肯定不一样喽)即图中的copy节点插入到原链表,这样就可以对random指针进行下面操作:


copy->random = cur->random->next;//后者代表着仍然是copy的节点,因为有指向next


这样就可以完美的解决random指针的问题啦。

因此应该进行以下步骤:

  • 1.复制节点插入原链表中,并对copy的random进行赋值(关键操作)。
  • 2.将copy的节点拿出来尾插编程新的链表。
  • 3.在第二步的同时将原链表恢复原状
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
struct Node* copyRandomList(struct Node* head) {
    //1.插入copy节点
    struct Node* cur = head;
    struct Node* copy = NULL;
    struct Node* next = NULL;
    while(cur)
    {
        next = cur->next;
        copy = (struct Node*)malloc(sizeof(struct Node));
        copy->val = cur->val;
        //连接到原链表
        cur->next = copy;
        copy->next = next;
        //迭代往后走
        cur = next;
    }
    //2.更新copy->random
    cur = head;
    while(cur)
    {
        copy = cur->next;
        if(cur->random == NULL)
            copy->random = NULL;
        else
            copy->random = cur->random->next;
        //迭代往后走
        cur = cur->next->next;
    }
    //将其copy的节点尾插,并还原原链表
    cur = head;
    struct Node* copyhead = NULL;
    struct Node* copytail = copyhead;
    while(cur)
    {
        copy = cur->next;
        next = copy->next;
        if(copyhead == NULL)
        {
            copyhead = copytail = copy;
        }
        else
        {
            copytail->next = copy;
            copytail = copytail->next;
        }
        //重新还原原链表
        cur->next = next;
        //迭代
        cur = next;
    }
    return copyhead;
}

微信图片_20230224170557.png

6. 双向带头循环链表


6.1函数实现


// 2、带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
    LTDataType _data;
    struct ListNode* next;
    struct ListNode* prev;
}ListNode;
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的结点
void ListErase(ListNode* pos);

与单链表不同的是,这里无论是什么函数传的都是一级指针,原因是此结构是带头的,即哨兵位,哨兵位的数据域不存储,并且传进去的哨兵位为真正哨兵位的形参,但是其next和prev分别记录了实际节点的地址,因此,这里都用一级指针完全可以解决。


顺序表与链表的优异


微信图片_20230221175934.png


7. 总结


这篇文章呕心沥血,是我目前写的时间最长的文章,不过到这里也就收尾了,本篇文章侧重的是实现链表所需要注重的细节及经过链表oj强训所带来的一系列的新的逻辑与方法,相信读到这里的你对于链表的掌握会上升一个非常大的台阶,即便这样,仍要多加训练,因此,将我的链表oj仓库放在这里以便大家食用(以后刷的链表也会在这里),一起加油哇!


相关文章
|
23天前
|
算法 索引
❤️算法笔记❤️-(每日一刷-141、环形链表)
❤️算法笔记❤️-(每日一刷-141、环形链表)
37 0
|
23天前
|
算法
【❤️算法笔记❤️】-(每日一刷-876、单链表的中点)
【❤️算法笔记❤️】-(每日一刷-876、单链表的中点)
40 0
|
3天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
25 4
|
4天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
4天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
22天前
|
存储 缓存 算法
经典算法之链表篇(三)
经典算法之链表篇(三)
|
22天前
|
算法
经典算法之链表篇(二)
经典算法之链表篇(二)
|
22天前
|
算法 索引
经典算法之链表篇
经典算法之链表篇
|
23天前
|
算法
❤️算法笔记❤️-(每日一刷-160、相交链表)
❤️算法笔记❤️-(每日一刷-160、相交链表)
16 1
|
3天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
13 0