巧用快慢指针找出链表中的环结构

很多同学可能都还没用过快慢指针,这次就带你们见识一下。这篇文章是由leetcode第141题和第142题引出的。先来容易的,判断链表中是否存在环结构:

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

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
示例 1:
在这里插入图片描述

输入:head = [3,2,0,-4], pos = 1
输出:true

解释:链表中有一个环,其尾部连接到第二个节点。

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

解法一:使用List

解题思路
定义一个List,然后while遍历链表中的节点,对于每次遍历,若List中不存在该节点,则执行add操作,若List中已存在,则返回true,若遍历到null节点则返回false。
代码

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    List<ListNode>nodes=new ArrayList<>();
    public boolean hasCycle(ListNode head) {
        while(head!=null){
            if(nodes.contains(head)){
                return true;
            }else{
                nodes.add(head);
                head=head.next;
            } 
        }
        return false;        
    }
}

这种解法当然可以实现判断是否有环的目的,但是执行时间为三百多毫秒,再看看题目的进阶要求——使用O(1)的空间,这种解法就有心无力了,因此我们提出第二种解法,也就是今天的主角,快慢指针。

解法二:快慢指针

解题思路
定义快指针fast和慢指针slow,fast每次走两步,slow每次走一步,若链表中存在环结构,则fast与slow一定会在环中相遇,只不过是时间问题而已,这里又涉及到了为什么设置fast每次走两步,走三、四、五步行不行?当然也行,如果面试的时候考官问到了,你可以说根据大量研究实验表明,使用两步所用的相遇时间最短。
代码

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head==null||head.next==null)return false;
        ListNode fast=head.next;
        ListNode slow=head;
        while(fast!=slow){
            if(fast.next==null||fast.next.next==null)return false;
            slow=slow.next;
            fast=fast.next.next;
        }
        return true;
    }
}

在这里插入图片描述
找出环结构后,就上点有难度的,如何找出入环的第一个节点,用额外的list同样可以完成解答,但无法满足题目要求,因此我们讲讲如何用快慢指针来实现。

142. 环形链表 II

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。
进阶
你是否可以使用 O(1) 空间解决此题?
示例 1
在这里插入图片描述

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点

解释:链表中有一个环,其尾部连接到第二个节点。

解题思路
这道题是在141基础上的进阶,同样的,我们使用快慢指针解法,但最难的就是找出进入循环的节点了。借助官方题解的一个图:
在这里插入图片描述

图中,头节点到进入循环的节点长度为a,进入循环的节点到相遇节点的长度为b,从相遇节点再到进入循环的节点长度为c,假设相遇时,快指针在环中走了n圈,慢指针在环中走了m圈,则由图可知,快指针走过的路程为:

S1=a+n(b+c);

慢指针走过的路程为:

S2=a+m(b+c);

由于他们到相遇点所用的时间相同且速度已知,则可以推出:

S1/2=S2/1;
即
a+n(b+c)=2(a+m(b+c));

合并同类项得:

a=c+(n-2m-1)(b+c);

也就是:

头节点到进入循环的节点长度=相遇节点到进入循环的节点长度+(n-2m-1)*环的长度;

然后我们转换一下思路,这里是最难理解的,就是将距离与路程的概念区分开来,假想一下,你在一个环中跑步,跑完一圈后与开始的位置相比,你走的路程是一圈,但是你移动的距离其实是0。那么上述公式我们就可以这样说:
头节点到进入循环的节点的距离=相遇节点到进入循环的节点的距离,也就是a=c。后面不管n和m取值为多少,距离都是0。
那么,当快慢指针相遇后,我们只需将快指针重置为头节点的位置,然后让两者每次移动一步,那么它们第二次相遇时即为进入循环的节点所在。
说起来有点绕,但代码很简单:

代码

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head==null||head.next==null)return null;
        ListNode fast=head.next; //快指针
        ListNode slow=head;//慢指针
        //快慢指针异步前进,直到第一次相遇
        while(fast!=slow){
            if(fast.next==null||fast.next.next==null)return null;
            slow=slow.next; //slow走一步
            fast=fast.next.next;//fast走两步
        }
        //将fast置于头指针位置
        fast=head;
        slow=slow.next;
        //fast与slow同步前进,直到再次相遇
        while(fast!=slow){
            fast=fast.next;
            slow=slow.next;
        }
        return fast;
    }
}

在这里插入图片描述

学到的话,不妨点赞加关注吧


版权声明:本文为a321123b原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。