jjzjj

数据结构:带环单链表基础OJ练习笔记(leetcode142. 环形链表 II)(leetcode三题大串烧)

摆烂小青菜 2023-05-26 原文

目录

一.前言 

二.leetcode160. 相交链表 

1.问题描述

2.问题分析与求解

三.leetcode141. 环形链表

1.问题描述

2.代码思路 

3.证明分析 

下一题会用到的重要小结论:

四.leetcode142. 环形链表 II

1.问题描述

2.问题分析与求解

Judgecycle接口:

方法一:

方法二: 


一.前言 

单链表和带环单链表OJ题是笔试面试常考的题目,本期是关于带环单链表基础题的刷题小笔记(前两个题的求解过程可以用于求解第三个题哦!)

二.leetcode160. 相交链表 

leetcode链接:160. 相交链表 - 力扣(Leetcode)

1.问题描述

给你两个单链表的头节点的地址 headA 和 headB ,请你找出并返回两个单链表相交部分起始节点。如果两个链表不存在相交节点,返回 null 。

比如图示两个链表:

已知a1和b1的地址,编写程序返回c1的地址。

  • 测试用例中的链表不存在环
  • 函数返回结果后,两个链表必须保持其原始结构 

题解接口:

class Solution 
{
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) 
    {
        
    }
};

2.问题分析与求解

方法一:

  • 先各自求出两个链表的长度,并求出它们长度的差值:

  • 然后再用两个指针来分别遍历两个链表,其中遍历较长链表的指针要先向前走N步(N表示两个链表长度的差值),然后两个指针再一起向前遍历两个链表,若链表存在交点,则两个指针必定会在交点相遇:

题解代码:

class Solution 
{
public:
    int countNode(ListNode * head)   //封装一个求节点个数的函数
    {
        int count = 0;
        while(head)
        {
            count++;
            head=head->next;
        }
        return count;
    }
    ListNode * foundNode(ListNode* longlist,ListNode* shortlist,int diff)
    //封装一个求第一个相交节点的函数
    {
        while(diff)                   //遍历长链表的指针先向前走diff步
        {
            longlist = longlist->next;
            --diff;
        }
        while(longlist && shortlist)  //两指针一起向前走直到相遇或指向空指针
        {
            if(longlist == shortlist)
            {
                return longlist;
            }
            longlist=longlist->next;
            shortlist=shortlist->next;
        }
        return nullptr;               //最终指向空指针则说明两表不相交
    }
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) 
    {
        int countA = countNode(headA);
        int countB = countNode(headB);
        if(countA>=countB)
        {
            return foundNode(headA,headB,countA-countB);
        }
        else
        {
            return foundNode(headB,headA,countB-countA);
        }
    }
};

这个方法思路比较简单但是不够简洁优雅,还有一个更简洁优雅的解法。

方法二:

  • 我们先考虑遍历表A的指针:当遍历表A的指针走到尾节点后,我们令其返回指向表B的头节点,此后如果该指针继续向前走countB步,则指针会来到两个链表的第一个相交节点,此时遍历表A的指针总共向前走了(countA + public + countB)次,如图:
  • 类似地,遍历B表的指针走到表尾后,我们令其返回指向表A的头节点,此后如果该指针再向前走countA步则同样会来到两表的第一个相交节点.此时遍历B表的指针同样总共向前走了(countA+countB+public)次.
  • 因此如果我们让遍历表A和表B的指针同时向前遍历链表,当他们走到表尾后,则令他们返回指向另外一个链表的头节点,两指针最终必定会在两链表第一个相交节点相遇(此时两个指针同时向前走了(countA + countB + public)次)。如图:

题解代码:

class Solution 
{
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) 
    {
        ListNode * ptrA = headA;
        ListNode * ptrB = headB;
        while(ptrA!=ptrB)
        {
            ptrA = (ptrA==nullptr)? headB : ptrA->next; //(ptrA==nullptr)代表指针指向A表尾
            ptrB = (ptrB==nullptr)? headA : ptrB->next; //(ptrB==nullptr)代表指针指向B表尾
        }
        return ptrA;
    }
};
  •  若两个链表不相交,最终两个指针会同时变为空指针,函数会返回空指针

三.leetcode141. 环形链表

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

1.问题描述

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

(如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环.)

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

比如:

题解接口:

class Solution 
{
public:
    bool hasCycle(ListNode *head) 
    {
        
    }
};

2.代码思路 

本题的代码思路很简单,利用的是快慢指针法,两个指针同时遍历链表,快指针一次走两步,慢指针一次走一步。

  • 如果链表中不存在环,则快指针会率先达到表尾。
  • 如果链表中存在环,则快慢指针最终会在环中相遇。

题解代码:

class Solution 
{
public:
    bool hasCycle(ListNode *head) 
    {
        if(nullptr == head || nullptr == head->next)//单节点和无节点链表做额外判断
        {
            return false;
        }
        ListNode* fast = head->next->next; //让快指针先走两步,慢指针走一步让它们指向不同节点
        ListNode* slow = head->next;
        while((fast && fast->next && fast!=slow))
        {
            fast=fast->next->next;         //快指针一次走两步
            slow=slow->next;               //慢指针一次走一步
        }
        return (fast==slow)? true : false; //判断两指针是否相遇并确定返回值(若无环fast一定不等 
                                           //slow)
    }
};

然而本题的关键并不在于代码如何写,而是在于如何去证明上述求解思路的合理性

接下来我们尝试对快慢指针法在本题中的合理性做一个比较严格的证明。

3.证明分析 

下文的所谓的距离指的是两个链表节点位置之间指针链的数目。

  • 我们先将带环链表用一个概念图表示一下: 
  •  我们令快慢指针同时从链表头节点出发:(fast=fast->next->next表示快指针一次走两步)(slow=slow->next表示慢指针一次走一步)
  • 如果链表中不存在环,易知快指针fast必然率先结束遍历链表的过程(fast或fast->next指向空),此时返回false。
  • 如果链表中存在环,那么快指针会率先进环,之后慢指针入环时,快指针此时一定处于环中某个位置:
  • 此后快指针开始在环中追赶慢指针,假设慢指针入环时,快指针与慢指针的距离为N(N小于或等于环的总长度减一)(N为某一个正整数)
  • 慢指针入环时两指针的环上距离是整数N.快指针每次循环前进两步,慢指针每次循环前进一步,可知两个指针的距离每次循环后会缩小1,则快指针必定会在环上某个点与慢指针相遇(即fast==slow,此时说明链表中存在环)

下一题会用到的重要小结论:

  • 另外还有一个重要小结论快慢指针相遇时,慢指针在环上走过的距离一定小于环的长度(因为N小于或等于环的总长度减一) (该结论在下一题中会用到)

更进一步的思考:如果快指针一次走三步或者n步(n>2),慢指针仍然一次走一步,那么是否还能确保快慢指针一定会在环中相遇?

  • 答案是否定的,我们可以规定让快指针一次走三步来做一下分析,设当慢指针刚入环时,两个指针的距离为N:
  • 快指针一次走三步,那么每次循环两个指针的距离会缩小2
  • 假如N是偶数,那么快指针最终会与慢指针相遇
  • 假如N是奇数,那么快指针追上慢指针后会处于慢指针的前一个位置。(整除余1)
  • 此时快指针重新开始追赶慢指针:设环的长度为X,则此时相当于快指针与慢指针的距离为X-1
  • 若X-1为偶数,那么快指针最终可以与慢指针相遇
  • 若X-1为奇数,那么快指针追上慢指针后会又一次处于慢指针的前一个位置。紧接着就开始了无限循环追赶,两个指针永远都不会相遇
  • 同样地,若令快指针一次走3,4,5...n步,通过数学归纳思维,我们同样能分析出(在各种不同的环长度的链表中)可能会出现和上述类似的无限追赶的情况,因此可以得出结论:快指针每次必须比慢指针多走1步才能确保(在任何带环链表中)两指针最终会在环中会相遇

四.leetcode142. 环形链表 II

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

1.问题描述

该题在上一题的基础上,要求我们编写的接口能够返回链表开始入环的第一个节点的地址。如果链表无环,则返回 nullptr.

比如:

题解接口:

class Solution
{
public:
    ListNode* detectCycle(ListNode* head)
    {
        
    }
};

2.问题分析与求解

第一步:

本题的求解建立在上一个题目的基础之上.

我们先编写一个额外的Judgecycle接口,用于判断链表是否带环,并且返回快慢指针在环中相遇位置节点的地址(链表不带环则返回空指针)。

Judgecycle接口:

    ListNode* Judgecycle(ListNode* head)
    {
        if(nullptr==head || nullptr == head->next)
        {
            return nullptr;
        }
        ListNode *fast =head->next->next;
        ListNode *slow = head->next;
        while(fast && fast->next && fast!=slow)
        {
            fast=fast->next->next;
            slow = slow ->next;
        }
        return (fast&&fast->next)? fast : nullptr;//(fast或fast->next为空则表示链表无环)
    }                                             //(为空说明fast走到表尾)  
  • 若链表带环,返回的fast指针就是快慢指针在环中相遇的位置的节点的地址
  • 该接口的原理参见上一题的分析
  • 题解接口中我们用一个temmet指针来接收Judgecycle接口的返回值
     
    
    class Solution
    {
    public:
        ListNode* Judgecycle(ListNode* head)
        {
            if(nullptr==head || nullptr == head->next)
            {
                return nullptr;
            }
            ListNode *fast =head->next->next;
            ListNode *slow = head->next;
            while(fast && fast->next && fast!=slow)
            {
                fast=fast->next->next;
                slow = slow ->next;
            }
            return (fast&&fast->next)? fast : nullptr;//(fast或fast->next为空则表示链表无环)
        }                                             //(为空说明fast走到表尾)
    
    
                                                 
        ListNode* detectCycle(ListNode* head)         //题解接口
        {
           ListNode * temmet = Judgecycle(head);
           ListNode* temhead = head;
           //其他操作步骤
        }
    };                                             

基于上面的Judgecycle接口,接下来我们有两种方法可以求解本题

方法一:

  • 假设环中temmet指针(指向Judgecycle接口中快慢指针在环中相遇位置节点)与环入口节点的距离为N
  • 假设链表头节点与环入口节点的距离为M
  • 假设环的总长度(距离)为C(不包括M)

接着我们来分析N,M,C之间存在着什么样的数学关系.

利用前一个题的一个重要结论(见目录)Judgecycle接口中快慢指针相遇时,慢指针在环上走过的距离一定小于环的长度

  • 于是:在Judgecycle接口中,快慢指针相遇时慢指针在链表中走过的总距离为(M+C-N)
  • 进一步可以得出,快慢指针相遇时快指针在链表中走过的总距离为2*(M+C-N)
  • 假设快慢指针相遇时,快指针已经在环中走了n圈,那么我们便可以用另外一种方式表示出快慢指针相遇时快指针在链表中走过的总距离:M+n*C+(C-N)
  • 于是得到方程:2*(M+C-N)=M+n*C+(C-N)
  • 化简可得:M+C-N = n*C 即:M=(n-1)*C + N  (M,C,N,n都为整数)
  • 令一个指针temhead初始位置指向链表头节点,另外一个指针temmet初始位置指向环中快慢指针相遇的位置(由Judgecycle接口返回)
  • 两个指针同时开始遍历链表,根据关系式M=(n-1)*C + N (M,C,N,n都为整数)可知两个指针必然在链表的入环节点相遇。返回指针的值即可得到答案。
        ListNode* detectCycle(ListNode* head)
        {
            ListNode* temmet = Judgecycle(head);      //快慢指针相遇位置节点的地址
            ListNode* temhead = head;
            if (temmet) //判断temmet是否为空,为空说明链表不带环
            {
                while (temhead != temmet)             //两个指针同时向前遍历链表直到相遇
                {
                    temmet = temmet->next;
                    temhead = temhead->next;
                }
                return temmet;                        //返回相遇位置节点地址
            }
            return nullptr;      //代表链表无环
        }

题解代码:

class Solution
{
public:
    ListNode* Judgecycle(ListNode* head)
    {
        if(nullptr==head || nullptr == head->next)
        {
            return nullptr;
        }
        ListNode *fast =head->next->next;
        ListNode *slow = head->next;
        while(fast && fast->next && fast!=slow)
        {
            fast=fast->next->next;
            slow = slow ->next;
        }
        return (fast&&fast->next)? fast : nullptr;//(fast或fast->next为空则表示链表无环)
    }                                             //(为空说明fast走到表尾)   

    ListNode* detectCycle(ListNode* head)
    {
        ListNode* temmet = Judgecycle(head);      //快慢指针相遇位置节点的地址
        ListNode* temhead = head;
        if (temmet) //判断temmet是否为空,为空说明链表不带环
        {
            while (temhead != temmet)             //两个指针同时向前遍历链表直到相遇
            {
                temmet = temmet->next;
                temhead = temhead->next;
            }
            return temmet;                        //返回相遇位置节点地址
        }
        return nullptr;      //代表链表无环
    }
};

方法二: 

  • 确定了Judgecycle接口中快慢指针在环中相遇的位置后,我们在两指针相遇的节点处将环断开
  • 于是问题就转换成了求两个相交链表第一个相交节点地址的问题(问题求解参见本期第一个OJ题) ,其中快慢指针在环中相遇位置的节点作为断环后新链表的头节点

因此我们可以调用前两个题的接口来求解本题:

class Solution
{
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)                        
    //求相交链表第一个交点的接口
    {
        ListNode * ptrA = headA;
        ListNode * ptrB = headB;
        while(ptrA!=ptrB)
        {
            ptrA = (ptrA==nullptr)? headB : ptrA->next; //(ptrA==nullptr)代表指针指向A表尾
            ptrB = (ptrB==nullptr)? headA : ptrB->next; //(ptrB==nullptr)代表指针指向B表尾
        }
        return ptrA;
    }
public:
    ListNode* Judgecycle(ListNode* head)          //求快慢指针在环中相遇位置的接口
    {
        if(nullptr==head || nullptr == head->next)
        {
            return nullptr;
        }
        ListNode *fast =head->next->next;
        ListNode *slow = head->next;
        while(fast && fast->next && fast!=slow)
        {
            fast=fast->next->next;
            slow = slow ->next;
        }
        return (fast&&fast->next)? fast : nullptr;//(fast或fast->next为空则表示链表无环)
    }                                             //(为空说明fast走到表尾)   

    ListNode* detectCycle(ListNode* head)
    {
        ListNode* temmet = Judgecycle(head);
        if (temmet)                                 //判断temmet是否为空,为空说明链表不带环
        {
            ListNode* breakpoint = temmet;
            while(breakpoint->next != temmet)       //找到环中的断开点
            {
                breakpoint = breakpoint->next; 
            }
            breakpoint->next = nullptr;             //将环断开
            return getIntersectionNode(temmet,head);//转化为求两链表第一个交点的问题
        }
        return nullptr;                             //代表链表无环
    }
};

  • 根据我们上面各步骤的分析不难得出两种求解方法的时间复杂度都是O(N),但是方法一会比方法二略高效一些。

有关数据结构:带环单链表基础OJ练习笔记(leetcode142. 环形链表 II)(leetcode三题大串烧)的更多相关文章

  1. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  2. 牛客网专项练习30天Pytnon篇第02天 - 2

    1.在Python3中,下列关于数学运算结果正确的是:(B)a=10b=3print(a//b)print(a%b)print(a/b)A.3,3,3.3333...B.3,1,3.3333...C.3.3333...,3.3333...,3D.3.3333...,1,3.3333...解析:    在Python中,//表示地板除(向下取整),%表示取余,/表示除(Python2向下取整返回3)2.如下程序Python2会打印多少个数:(D)k=1000whilek>1:    print(k)k=k/2A.1000 B.10C.11D.9解析:    按照题意每次循环K/2,直到K值小于等

  3. Python 刷Leetcode题库,顺带学英语单词(31) - 2

    ValidPalindromeGivenastring,determineifitisapalindrome,consideringonlyalphanumericcharactersandignoringcases. [#125]Example:"Aman,aplan,acanal:Panama"isapalindrome."raceacar"isnotapalindrome.Haveyouconsiderthatthestringmightbeempty?Thisisagoodquestiontoaskduringaninterview.Forthepurposeofthisproblem

  4. Unity Shader 学习笔记(5)Shader变体、Shader属性定义技巧、自定义材质面板 - 2

    写在之前Shader变体、Shader属性定义技巧、自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用,方便后续回顾查看,如有以偏概全、不祥不尽之处,还望海涵。1、Shader变体先看一段代码......Properties{ [KeywordEnum(on,off)]USL_USE_COL("IsUseColorMixTex?",int)=0 [Toggle(IS_RED_ON)]_IsRed("IsRed?",int)=0}......//中间省略,后续会有完整代码 #pragmamulti_c

  5. IDEA使用LeetCode插件 - 2

    前言我们习惯用idea编写、调试代码,在LeetCode上刷题时,如果能够在IDEA编写代码,并且做好代码管理,是一件事半功倍的事情。对于后续复习题目,做笔记也会非常便利。本文目的在于介绍LeetCodeEditor的使用,以及配置工具类,最终目录结构如下:note:放置笔记src:放置代码leetcode.editor.cn:插件LeetCodeEditor自动生成utils:自定义的工具包,可用于自动化输入测试用例,定义题目需要的类(结构体)out:运行测试时自动生成LeetCodeEditorGitHub:https://github.com/shuzijun/leetcode-edit

  6. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  7. 计算机网络笔记:TCP三次握手和四次挥手过程 - 2

    TCP是面向连接的协议,连接的建立和释放是每一次面向连接的通信中必不可少的过程。TCP连接的管理就是使连接的建立和释放都能正常地进行。三次握手TCP连接的建立—三次握手建立TCP连接①若主机A中运行了一个客户进程,当它需要主机B的服务时,就发起TCP连接请求,并在所发送的分段中用SYN=1表示连接请求,并产生一个随机发送序号x,如果连接成功,A将以x作为其发送序号的初始值:seq=x。主机B收到A的连接请求报文,就完成了第一次握手。客户端发送SYN=1表示连接请求客户端发送一个随机发送序号x,如果连接成功,A将以x作为其发送序号的初始值:seq=x②主机B如果同意建立连接,则向主机A发送确认报

  8. 华为数通笔记VXLAN&BGP EVPN - 2

    VXLAN简介定义RFC定义了VLAN扩展方案VXLAN(VirtualeXtensibleLocalAreaNetwork,虚拟扩展局域网)。VXLAN采用MACinUDP(UserDatagramProtocol)封装方式,是NVO3(NetworkVirtualizationoverLayer3)中的一种网络虚拟化技术。目的随着网络技术的发展,云计算凭借其在系统利用率高、人力/管理成本低、灵活性/可扩展性强等方面表现出的优势,已经成为目前企业IT建设的新趋势。而服务器虚拟化作为云计算的核心技术之一,得到了越来越多的应用。服务器虚拟化技术的广泛部署,极大地增加了数据中心的计算密度;同时,为

  9. [蓝桥杯单片机]学习笔记——串口通信的基本原理与应用 - 2

    目录一、原理部分1、什么是串行通信(1)并行通信与串行通信(2)串行通信的制式(3)串行通信的主要方式  2、配置串口(1)SCON和PCON:串行口1的控制寄存器(2)SBUF:串行口数据缓冲寄存器 (3)AUXR:辅助寄存器​编辑(4)ES、PS:与串行口1中断相关的寄存器(5)波特率设置  3、串口框架编写二、程序案例一、原理部分1、什么是串行通信(1)并行通信与串行通信微控制器与外部设备的数据通信,根据连线结构和传送方式的不同,可以分为两种:并行通信和串行通信。并行通信:数据的各位同时发送与接收,每个数据位使用一条导线,这种方式传输快,但是需要多条导线进行信号传输。串行通信:数据一位一

  10. 【微服务笔记23】使用Spring Cloud微服务组件从0到1搭建一个微服务工程 - 2

    这篇文章,主要介绍如何使用SpringCloud微服务组件从0到1搭建一个微服务工程。目录一、从0到1搭建微服务工程1.1、基础环境说明(1)使用组件(2)微服务依赖1.2、搭建注册中心(1)引入依赖(2)配置文件(3)启动类1.3、搭建配置中心(1)引入依赖(2)配置文件(3)启动类1.4、搭建API网关(1)引入依赖(2)配置文件(3)启动类1.5、搭建服务提供者(1)引入依赖(2)配置文件(3)启动类1.6、搭建服务消费者(1)引入依赖(2)配置文件(3)启动类1.7、运行测试一、从0到1搭建微服务工程1.1、基础环境说明(1)使用组件这里主要是使用的SpringCloudNetflix

随机推荐