链表算法题(下)

链表算法题(上)长中我们已经学习了一系列的链表算法题,那么在本篇中我们将继续来学习链表的算法题,接下来就继续来破解链表的算法题吧!


1.相交链表

160. 相交链表 - 力扣(LeetCode)

通过以上的题目的描述该算法题要我们实现的代码功能是判断两条链表是否相交,如果相较的话就返回相较节点的指针,不相较就返回NULL

那么在实现该算法题的代码之前先要来分析如何实现判断是否是相交链表

首先来看相交链表有什么特征呢?来看以下的示例

从以上的示例就可以看出当两个链表是相交的,那么在这两条链表内一定会有两个节点的下一个节点是相同的。而如果链表不是相交的,那么就不会有两个节点的下一个节点是相同的

当两个链表是相较的时候我们需要返回的就是以上这个节点,那么该如何来找出这个相交的节点呢?

在此我们的解决方法是先定义两个指针变量一开始分别指向两个链表的第一个节点,之后通过各遍历一次两条链表,之后就得到这两条链表各自的节点个数,之后将得到的两个节点数大的减小的得到两链表长度的差值,将节点个数多的链表的定义的节点指针往后走之前得到的差值,最后将两个链表的指针同时往后遍历,当两个指针相同时就说明这两个链表为相交链表,否则就不相交

例如以下示例:

首先A链表个数为5,B节点个数为6,两个链表节点差值就为1

在定义两个指向两个;链表的第一个节点的指针pcur1和pcur2,由于B链表为节点个数多的链表因此将B链表的指针pcur2先向后走1步

之后让pcur1和pcur2同时向后遍历,之后pcur1和pcur2同时指向c1节点,这就可以说明A和B这两个链表是相交链表

接下来我们就来实现该算法题的代码
在以下得到两个链表的节点数差值使用的是库函数abs,该函数能计算出差值的绝对值

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    ListNode* pcur1=headA;
    ListNode *pcur2=headB;
    
    int count1=0;
    int count2=0;
    while(pcur1)//得到第一个链表的结点数
    {
        count1++;
        pcur1=pcur1->next;
    }
    while(pcur2)//得到第二个链表的节点数
    {
        count2++;
        pcur2=pcur2->next;
    }
    int tmp=abs(count1-count2);//得到两个链表节点数的差值
    ListNode* longlist=headA;
    ListNode* shortlist=headB;
    if(count1<count2)//确定长短链表
    {
        longlist=headB;
        shortlist=headA;
    }
    while(tmp--)//先将长链表走tmp步
    {
        longlist=longlist->next;
    }
    while(longlist && shortlist)//同时遍历两个链表
    {
        if(longlist==shortlist)
        {
            return longlist;
        }
        longlist=longlist->next;
        shortlist=shortlist->next;
    }
    return NULL;
}

2.环形链表I

141. 环形链表 - 力扣(LeetCode)

根据以上的题目描述就可以看出该算法题要我们实现的是判断链表是否为循环链表,在此循环链表是指链表的尾节点不在是连接NULL,而是连接链表中的其中一个节点

那么使用什么方法能判断链表是否为循环链表呢?

在此我们使用的是之前学习过的前后指针法,首先定义有两个指针变量fast和slow,之后fast指针每次走两步,slow指针每次走一步,到最后如果fast指针和slow相遇就说明该链表为循环链表

例如以下示例:

要判断以上链表是否为循环链表我们来看看使用前后指针法的过程是什么样的,首先定义快指针fast和满指针slow,之后让fast和slow遍历链表

正如上图所示slow指针和fast指针在节点-4相遇,这就说明该链表为循环链表为循环链表

接下来我们就来实现该算法题的代码

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

在解决了以上的算法题后我们就要思考为什么快指针每次走两步,慢指针走一步可以相遇,有没有可能遇不上呢?

slow一次走⼀步,fast一次走2步,fast先进环,假设slow也走完入环前的距离,准备进环,此时fast和slow之间的距离为N,接下来的追逐过程中,每追击⼀次,他们之间的距离缩小1步

这时在追击过程中fast和slow之间的距离变化:

因此,在带环链表中慢指针走一步,快指针走两步最终一定会相遇。

那么在解决以上问题后思考快指针一次走3步,走4步,...n步行吗?

step1:
按照上面的分析,慢指针每次走一步,快指针每次走三步,此时快慢指针的最大距离为N,接下来的追逐过程中,每追击一次,他们之间的距离缩小2步
追击过程中fast和slow之间的距离变化:

分析:
1、如果N是偶数,第一轮就追上了
2、如果N是奇数,第一轮追不上,快追上,错过了,距离变成-1,即C-1,进入新的一轮追击

a:C-1如果是偶数,C-1如果是偶数,那么下一轮就追上了
b:C-1如果是奇数,那么就永远都追不上
总结一下追不上的前提条件:N是奇数,C是偶数

step2:

在以上的step1中我们得出当fast追不上的前提条件:N是奇数,C是偶数,那么接下来我们就要继续分析这种情况是否会出现

假设:
环的周长为C,头结点到slow结点的长度为L,slow走一步,fast走三步,当slow指针入环后,slow和fast指针在环中开始进行追逐,假设此时fast指针已经绕环x周。
在追逐过程中,快慢指针相遇时所走的路径长度:


由于慢指针走一步,快指针要走三步,因此得出:3 * 慢指针路程 = 快指针路程 ,即:


对上述公式继续分析:由于偶数乘以任何数都为偶数,因此 2L 一定为偶数,则可推导出可能得情
况:

• 情况1:偶数 = 偶数 - 偶数
• 情况2:偶数 = 奇数 - 奇数

由step1中(1)得出的结论,如果N是偶数 ,则第一圈快慢指针就相遇了。
由step1中(2)得出的结论,如果N是奇数 ,则fast指针和slow指针在第一轮的时候套圈了,开始进行下一轮的追逐;当N是奇数 ,要满足以上的公式,则 (x+1)C 必须也要为奇数 ,即C为奇数,满足(2)a中的结论,则快慢指针会相遇

因此, step1 中的 N是奇数,C是偶数不成立 ,既然不存在该情况,则快指针一次走3步最终一定也可以相遇。

因此快指针一次走4、5.....步最终也会相遇,其证明方式同上

注:虽然已经证明了快指针不论走多少步都可以满足在带环链表中相遇,但是在编写代码的时候会有额外的步骤引入,涉及到快慢指针的算法题中通常习惯使用慢指针走⼀步快指针走两步的方式。

3.环形链表II

142. 环形链表 II - 力扣(LeetCode)

在以上环形链表I中我们已经实现了如何判断一个链表是否为环形链表,接下来我们在环形链表算法题中我们将跟进一步当链表是环形链表时我们还要返回链表进入环的第一个节点

那么要如何才能找到环形链表进入环的第一个节点呢?接下来我们要来了解一个性质

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

例如以下示例:

根据使用快慢指针法我们就可以的到以上链表两指针的相遇节点为-4

在此之后我们再定义一个指针变量pcur指向链表的第一个节点,之后让pcur指针和fast指针同时遍历,当这两个指针指向同一个节点时,指针指向的节点就是入环前的节点

接下来我们就来实现该算法题的代码

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode *detectCycle(struct ListNode *head)
{
    ListNode* fast,*slow;
    fast=slow=head;
    while(fast&& fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
        if(slow==fast)
        {
            ListNode* pcur=head;
            while(pcur!=fast)
            {
                pcur=pcur->next;
                fast=fast->next;
            }
            return fast;
        }
    }
    return NULL;
}

在解决了以上的算法题后我们就要证明为什么在环形链表中 以上的这种性质

在以上图示当中H为链表的起始点,E为环入口点,M与判环时候相遇点

在此我们设环的长度为R,H到E的距离为L,E到M的距离为 X ,则:M到E的距离为 R-X

由于快指针和满指针走过的路径分别为

又因为快指针走过的路径长度为满指针走过的路径长度的两倍,这时就能得到以下的公式

根据以上公式就可以推断出以下公式:

在此可以继续得到

当慢指针进入环时,快指针可能已经在环中绕了n圈了,n⾄少为1因为:快指针先进环走到M的位置,,最后又在M的位置与慢指针相遇

**所以以上公式n可能为1,2,3,4......,n的大小取决于环的大小,环越小n越大,在此极端情况下,假设n=1,此时:**L=R-X

根据L=R-X即可说明**:****⼀个指针从链表起始位置运行,⼀个指针从相遇点位置绕环,每次都走⼀步,两个指针最终会在入口点的位置相遇**

4.随机链表的复制

138. 随机链表的复制 - 力扣(LeetCode)

根据以上的题目描述可以看出该算法题和我们之前解决的题不同,在该算法题中的链表节点中的有三个成员变量,相比正常的链表节点多了一个随机的指针random。在此题目要我们实现的是链表的拷贝,注意在此不是将原链表内的指针复制一份,这种只是浅拷贝在此算法题中我们要实现的是深拷贝,也就是要额外开一份和原链表一样的内存空间,在将原链表内的数据拷贝给新的链表

接下来我们就来分析如何来实现链表的拷贝

例如以下示例:

在以上链表中若要实现链表的拷贝,那么该如何实现呢?

在此你可能会想到可以通过遍历原链表,在每遍历到一个节点时就创建一个新的节点,再将原链表节点内的数据拷贝给新节点,之后再将新节点尾插新链表

但是在以上的示例按照这种方法来实现就会发现问题了,在以上示例的链表按照以上的方法来拷贝在第一个节点时没问题,当到了第二个节点就出现一个问题****就是原链表第二个节点中的random指针指向的第一个节点,但在使用以上方法时第二个节点中的random无法找到第一个节点,这时因为单向链表中无法向前遍历链表

因此要解决链表的拷贝以上的方法行不通,在此我们需要想其他的方法
接下来就来讲解一种很妙的方法

首先是在原链表基础上复制链表

在此我们先定义两个指针变量pcur和Next分别指向原链表第一个节点和第二个节点

之后创建一个新的节点并且将第一个节点内的值val拷贝到新节点中,再将新节点插入到pcur和Next节点中间,完成以上操作后让pcur指向Next指向的节点,Next指向其next指针指向的节点

之后在遍历原链表每个节点时重复以上的操作,直到pcur指向NULL停止操作,这时原链表就变为以下形式

在以上我们创建的新节点只进行了val的拷贝,那么接下来就来对random来进行拷贝,要进行random的拷贝接下来要再创建一个指针变量copy让其一开始指向创建的第一个新节点,并且之后还要让pcur和Next指针重新回到初始位置

之后继续置random指针

在此pcur指向节点的random为NULL,就让这时copy指向的节点内的random也置为NULL,之后让pcur指向Next指向的节点,Next指向其next指针的next指向的节点,copy指向改变后的pcur的next指针指向的节点

之后再遍历原链表,当pcur指向节点内的random不为NULL时,让copy内的random指针指向pcur内random指向的节点next指针指向的节点,也就是copy->random=pcur->random->next,最后当pcur为NULL停止,这时原链表就变为以下形式

完成以上操作后最后就要将原链表和赋值链表断开

在此让pcur的next指针指向copy的next指针指向的节点,copy的next指针指向pcur的next指针指向的节点next指针指向的节点,之后让copy和pcur都往后走一步

之后再遍历原链表,重复以上的操作,这时原链表就变为以下形式

通过以上示例的讲解就可以发现链表的复制分为以下3步:

接下来就来实现该算法题的代码

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

typedef struct Node Node;
 Node* NewNode(int x)
 {
    Node* newnode=(Node*)malloc(sizeof(Node));
    newnode->val=x;
    newnode->next=newnode->random=NULL;
    return newnode;
 }

struct Node* copyRandomList(struct Node* head)
{
    if(head==NULL)
    {
        return head;
    }
    Node* pcur=head;
//在原链表基础上复制链表
    while(pcur)
    {
        Node* Next=pcur->next;
        Node*newnode=NewNode(pcur->val);
        newnode->next=Next;
        pcur->next=newnode;

        pcur=pcur->next->next;
    }
//置random指针
    pcur=head;
    while(pcur!=NULL)
    {
        Node* newpcur=pcur->next;
        if(pcur->random!=NULL)
        {
         newpcur->random=pcur->random->next;
        }
        pcur=newpcur->next;
    }
//将原链表和赋值链表断开
    pcur=head;
    Node* newhead,*newptail;
    newhead=newptail=pcur->next;
    while(pcur->next->next)
    {
       pcur=pcur->next->next;
       newptail->next=pcur->next;
       newptail=newptail->next;
    }
	return newhead;
}

以上就是链表算法题的全部内容了,希望能得到你的点赞、收藏

相关推荐
Reese_Cool8 分钟前
【数据结构与算法】排序
java·c语言·开发语言·数据结构·c++·算法·排序算法
加密新世界13 分钟前
优化 Solana 程序
人工智能·算法·计算机视觉
djk888840 分钟前
.net将List<实体1>的数据转到List<实体2>
数据结构·list·.net
搬砖的小码农_Sky1 小时前
C语言:结构体
c语言·数据结构
飞升不如收破烂~1 小时前
redis的map底层数据结构 分别什么时候使用哈希表(Hash Table)和压缩列表(ZipList)
算法·哈希算法
九圣残炎1 小时前
【从零开始的LeetCode-算法】3354. 使数组元素等于零
java·算法·leetcode
程序猿小柒2 小时前
leetcode hot100【LeetCode 4.寻找两个正序数组的中位数】java实现
java·算法·leetcode
雨中rain3 小时前
贪心算法(1)
算法·贪心算法
不爱学习的YY酱3 小时前
【操作系统不挂科】<CPU调度(13)>选择题(带答案与解析)
java·linux·前端·算法·操作系统
平头哥在等你3 小时前
求一个3*3矩阵对角线元素之和
c语言·算法·矩阵