【经典题目】链表OJ(相交链表、环形链表、环形链表II、随机链表的复制)

小编主页详情<-请点击
小编gitee代码仓库<-请点击


本文主要介绍了有关链表的各种经典面试题目(相交链表、环形链表、环形链表II、随机链表的复制),内容全由作者原创(无AI),同时深度解析了题目的经典解决方法,并带有配图帮助博友们更好的理解,点个关注不迷路,下面进入正文~~


目录

1.相交链表

2.环形链表

3.环形链表II

4.随机链表的复制

结语:


1.相交链表

题目链接:相交链表

**方法1:**遍历链表A的所有节点,并且将A链表的所有节点都与B链表的所有节点进行比较,如果有节点相同,说明他们是相交的;如果没有节点相同,说明这两个链表不相交。

要求:上面的方法的时间复杂度是O(N^2),有没有更快的方法呢?

方法2:(比较推荐)
1.判断是否相交。如果这两个链表相交了,说明他们最后一个节点一定是相同的。我们可以分别遍历两个链表,找出这两个链表的最后一个节点并进行比较。如果不相同,直接返回NULL;如果2相同再找出他们相交的节点。
2.若相交,找出第一个节点。先分别算出这两个链表的长度,再求出这两个链表长度的差值gap。然后找出他们两其中较长的链表,并重新遍历这两个链表。长链表先走gap步,然后两个链表同时开始遍历。当找到两个相同的节点时,返回该节点的地址

我们更推荐使用方法2 ,因为他的时间复杂度是O(N),要优于方法1

下面是用方法2解决这道题的完整代码

cs 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;

    int countA = 1;
    int countB = 1;

    while(curA->next)
    {
        curA = curA->next;
        countA++;
    }

     while(curB->next)
    {
        curB = curB->next;
        countB++;
    }

    if(curA!=curB)
    {
        return NULL;
    }

    struct ListNode* fast = headA;
    struct ListNode* slow = headB;
    int gap = abs(countA - countB);
    if(countA < countB)
    {
        fast = headB;
        slow = headA;
    }

    while(gap--)
    {
        fast = fast->next;
    }

    while(fast!=slow)
    {    
        fast = fast->next;
        slow = slow->next;
    }
    return fast;
}

2.环形链表

题目链接:环形链表

解题思路:快慢指针

慢指针一次走一步,快指针一次走两步。当slow进环的时候,fast开始追击slow。fast追上slow就有环,如果fast走到NULL就说明没有环。

为什么一定会相遇?有没有可能永远错过?请证明

假设slow进环时,fast和slow的距离是N。每当fast走两步slow走一步时,fast和slow的距离都会缩小1,直到fast和slow的距离为零。因此fast和slow一定会相遇

下面是这道题目的完整代码:

cs 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode *head) 
{
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    
    while(fast&&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(slow==fast)
        {
            return true;
        }
    }
    return false;
}

拓展:当slow走1步,fast走3步、4步、5步、n步时,fast和slow还一定会相遇吗?

我们先探究当fast走三步时的情况

同样的,我们假设slow进环时,fast和slow的距离是N。每当fast走三步slow走一步时,fast和slow的距离都会缩小2。这里就要分两种情况:

当N为偶数时,每走一次fast和slow的距离都会缩小2,直到N为0。因此,当N为偶数时,fast和slow一定会相遇。

当N为奇数时,每走一次fast和slow的距离都会缩小2,直到N为-1。N为-1的意思也就是fast超前了slow一步,fast和slow并没有相遇。

我们现在假设圆环的长度是C,那么现在fast到slow的距离就是C-1。

当C-1为偶数时,每走一次fast和slow的距离都会缩小2,直到N为0。因此,当C-1为偶数时,fast和slow一定会相遇。

当C-1奇数时,每走一次fast和slow的距离都会缩小2,直到N为-1。此时就陷入了死循环了,fast永远也不会和slow相遇。

总结一下:

1.N是偶数,第一轮就追上了

2.N是奇数,第一轮追击会错过,距离变成C-1

a.如果C-1为偶数,那么下一轮就追上了

b.如果C-1为奇数,那么就永远追不上

此时,我们发现fast追不上slow只有一种情况,就是N为偶数且C-1为奇数
可是,这种情况存在吗?

同样的,我们假设slow进环时,fast和slow的距离是N,并且我们设slow刚进环时fast已经在环里面转了x圈。此时slow走的距离是L,fast走的距离是L+x*C+C-N。又因为fast走的距离是slow的三倍,那么这时我们就可以列一个等式

3L=L+x*C+C-N

2L=x*C+C-N

现在N为奇数,C为偶数

偶数=(x+1)*偶数 - 奇数

很明显,我们看到这个等式是矛盾的。说明N为偶数且C-1为奇数的情况不存在!

结论:一定能追上

N等于偶数时,第一轮就能追上

N等于奇数时,C-1一定为偶数,第二轮就能追上

那么当fast走4步、5步、n步呢?
这里的分析方法都是一模一样的,欢迎博友们在评论区展开讨论,这里就不详细探究了~

3.环形链表II

题目链接:环形链表II

方法1: 与上一题的思路一样,可以先用快慢指针先找到在环里相遇的一个节点,然后将这个节点与后一个节点断开,这样问题就转化成了相交链表 的问题

完整代码如下:

cs 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;

    int countA = 1;
    int countB = 1;

    while(curA->next)
    {
        curA = curA->next;
        countA++;
    }

     while(curB->next)
    {
        curB = curB->next;
        countB++;
    }

    if(curA!=curB)
    {
        return NULL;
    }

    struct ListNode* fast = headA;
    struct ListNode* slow = headB;
    int gap = abs(countA - countB);
    if(countA < countB)
    {
        fast = headB;
        slow = headA;
    }

    while(gap--)
    {
        fast = fast->next;
    }

    while(fast!=slow)
    {    
        fast = fast->next;
        slow = slow->next;
    }
    return fast;
}
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(slow==fast)
        {
            struct ListNode* newnode = slow->next;
            slow->next=NULL;
            return getIntersectionNode(newnode,head);
        }
    }
    return NULL;
}

方法二: 这个方法比较巧妙,实现起来比方法一更快,也是更推荐 使用的方法。

我们可以设进环的位置到相遇节点的距离为N。那么slow走的路程就是L+N,而fast走的路程就是L+x*C+N,根据fast走的距离时slow的两倍,我们可以得出一个等式

2(L+N)= L+x*C+N

L = x*C-N

L = (x-1)*C+C-N

根据这个等式我们可以得出,起点到进入环的第一个节点的距离等于(x-1)个圆环长度加上meet到起点到进入环的第一个节点的距离。此时我们让两个指针从head和meet开始遍历链表,最后相遇的节点肯定是进入环的第一个节点

题目完整代码如下:

cs 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

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(slow==fast)
        {
            struct ListNode* meet = slow;
            while(meet!=head)
            {
                meet=meet->next;
                head=head->next;
            }
            return meet;
        }
    }
    return NULL;
}

4.随机链表的复制

题目链接:随机链表的复制

这题也是这四道题目难度最大 的一题,也是链表掌握程度的一块试金石。

简单来说,深拷贝 就是拷贝一个值和指针指向跟当前链表一模一样的链表。

下面一起跟着我的思路看看这道题目。

这道题目的难点关键在于怎么找到random指向的节点。因为我们要创建一个全新的链表,又不能使用原来链表的节点,所以我们不能直接拷贝random的值。那我们如果在每个random指向的节点后面创建一个与random指向节点一样的节点,是不是就可以通过random->next->next来找到我们新链表random指向的节点。为了方便,我们干脆在每个节点后都创建一个一模一样的节点,如下图所示

这样做的好处也是让原链表和新链表建立起了一种关联关系。

如果节点的random指向为空,那我们就让新节点的random也指向为NULL,接着让指针指向下一个新创建的节点。

如果节点的random指向不为空,那我们就让新节点的random指向原节点random的next指针,也就是对应random所指向的新创建节点,接着让指针指向下一个新创建的节点。

当我们把所有新节点的random都设置好后,就可以把新链表串起来,并把原链表恢复原样。

具体的实现思路:

1.拷入节点插入在原节点后面

2.用两个指针分别遍历新链表和原链表,给新节点的random赋值

3.把拷贝节点取下来尾插成为新链表,然后恢复原链表(不恢复也可以)

下面是这道题目的完整代码:

cs 复制代码
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */

struct Node* copyRandomList(struct Node* head) 
{
	struct Node* cur = head;
    while(cur)
    {
        struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
        copy->val=cur->val;
        copy->next = cur->next;
        cur->next=copy;
        cur=copy->next;
    }

    cur = head;
    while(cur)
    {
        struct Node* copy = cur->next;
        if(cur->random==NULL)
        {
            copy->random=NULL;
        }
        else
        {
           copy->random=cur->random->next;
        }
        cur=copy->next;
    }
    struct Node* copyhead = NULL;
    struct Node* copytail = NULL;
    cur=head;
    while(cur)
    {
        struct Node* copy = cur->next;
        struct Node* next = copy->next;
        if(copyhead==NULL)
        {
            copyhead=copytail=copy;
        }
        else
        {
            copytail->next=copy;
            copytail=copy;
        }
        cur->next=next;
        cur=next;
    }
    return copyhead;
}

这道题目综合链表的多种方法,逻辑也比较复杂,也有不少易错点,需要勤加练习。

结语:

这篇文章全文由作者手写,图片由画图软件所制,无AI制作,希望各位博友能有所收获

欢迎各位博友的讨论,觉得不错的小伙伴,别忘了点赞关注哦~

相关推荐
张人玉2 小时前
SMT 贴片机上位机项目
开发语言·c#
ん贤2 小时前
口述Map
开发语言·面试·golang
YuanDaima20482 小时前
Python 数据结构与语法速查笔记
开发语言·数据结构·人工智能·python·算法
asdzx672 小时前
C#:从 URL 下载 PDF 文档到本地
开发语言·pdf·c#
阿凤212 小时前
uniapp如何修改下载文件位置
开发语言·前端·javascript
m0_716765232 小时前
数据结构--循环链表、双向链表的插入、删除、查找详解
开发语言·数据结构·c++·学习·链表·青少年编程·visual studio
聆风吟º2 小时前
【C标准库】深入理解C语言strstr函数:子字符串查找的实用指南
c语言·开发语言·库函数·strstr
XY_墨莲伊2 小时前
【编译原理】实验一:基于正则文法的词法分析器设计与实现
开发语言·数据结构·算法
Tirzano2 小时前
springsession全能序列化方案
java·开发语言