【LeetCode】详解环形链表141. Linked List Cycle Given a linked list, determine if it has a cycle in it. To


前言

今天这道题目的第一种解法很奇葩,用计时器竟然可以AC,并且可以自己调整时间多少,跟我一起来看看吧。


正文

原题:

链接:环形链表

Given a linked list, determine if it has a cycle in it.
To represent a cycle in the given linked list, we use an integer pos which represents the position (0-indexed) in the linked list where tail connects to. If pos is -1, then there is no cycle in the linked list.
Example 1:
Input: head = [3,2,0,-4], pos = 1
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the second node.
在这里插入图片描述
Example 2:
Input: head = [1,2], pos = 0
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the first node.
在这里插入图片描述
Example 3:
Input: head = [1], pos = -1
Output: false
Explanation: There is no cycle in the linked list.
在这里插入图片描述
Follow up:
Can you solve it using O(1) (i.e. constant) memory?

题目大意
给定一个链表,判断是不是环形链表,没错就是这么简单。

思路1:

一般我们遍历链表(非环形链表)的时候,直接上来一个while (head != null),然后循环体里面head = head.next,类似下面的语句

while (head != null) {
	head = head.next;
}

但是环形链表就不行了,可能会遇到无限循环导致超时的问题,这时就产生了第一种思路。
既然环形链表会超时,那我们把时间限制在某个范围之间,比如0.1s后还在运行的话,那可能就是存在环形链表了;可以使用Java自带的System.currentTimeMillis()方法获取当前时间,当然别的语言也同样提供了类似的方法,通过以下代码就可以计算出代码运行时间:

// 过去的时间
long oldTime = System.currentTimeMillis();
while (true) {
	// 这里是循环体代码
}
// 当前时间
long curTime = System.currentTimeMillis();
// 代码运行时间等于curTime - oldTime

curTime - oldTime就是循环体运行的时间了,我们来看一下思路1的完整代码:

代码

public class Solution {
    public boolean hasCycle(ListNode head) {
        // 如果超过某个时间,比如0.1s,则说明有环
        long oldTime = System.currentTimeMillis();
        while (head != null) {
            head = head.next;
            long curTime = System.currentTimeMillis();
            if (curTime - oldTime > 100) {
                return true;
            }
        }
        return false;
    }
}

提交代码,成功了,但是时间太慢了,我们试着把时间差100减少一半
在这里插入图片描述
当把时间差改成50时,发现也是可以AC的
在这里插入图片描述
继续改成25,仍然可以AC
在这里插入图片描述
继续缩小一半,12
在这里插入图片描述
二分缩小,试一下6
在这里插入图片描述
算了,直接改成1吧(curTime - oldTime > 1),还是可以AC,但是这个时间确实慢了点
代码的时间差是大于1,也就是1ms,之所以提交会变成17ms是因为提供了17个测试用例,每个用例耗时1ms
在这里插入图片描述
后面就没有必要继续了,我们换一种思路吧。

思路2

由于环形链表中每个节点都有属于自己的地址,当我们遍历链表时,若出现了重复地址的情况,那么可以说明存在环形链表。
检测是否重复的方法也很简单,直接使用Set(Set一种无顺序、不可重复的数据结构),每次遍历一个节点,判断其地址是否在set中存在,若存在则说明是环形链表,否则将其存储到set中。

代码

public class Solution {
    public boolean hasCycle(ListNode head) {
        // 使用set存储hashcode
        Set<Integer> set = new HashSet<>();
        while (head != null) {
            if (set.contains(head.hashCode())) 
                return true;
            else 
                set.add(head.hashCode());
            head = head.next;
        }
        return false;
    }
}

代码讲解
在存储地址之前需要检查是否存在于set,只有不存在时才可以添加到set中;
整个循环遍历之后,直接返回false,这是因为若存在环形链表的话,肯定会满足if条件,直接就返回true,反过来也就是不存在环形链表,循环结束,返回false。
提交代码,时间稍稍好一些,但是空间就不行了,由于借助了额外的空间,复杂度为O(N),接下来我们尽可能将其空间减小为O(1)。
在这里插入图片描述

思路3

这个方法我没有想到,我也是通过资料查阅得来的,思路3有点类似龟兔赛跑,假设兔子的速度为2,乌龟的速度为1,当然兔子没有睡觉,如下图所示:
在这里插入图片描述
(图案不是重点)
他们两个的跑步轨迹是这样的,兔子跑到2的时候,乌龟爬到1;兔子跑到4的时候,乌龟爬到2…兔子跑到10的时候,乌龟爬到5,如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
但如果赛道换了,变成下面这个样子:
在这里插入图片描述
这时,只要兔子和乌龟仍在跑,就一定会相遇!
在这里插入图片描述
如上图,兔子可能在跑了n圈小环的时候,乌龟刚好也进入了小环,这时候他们两个相遇了。
我们把一开始的线段赛道看成普通的链表,把带有小环的赛道看成环形链表,判断环形链表是否存在的条件就变成了判断乌龟和兔子是否相遇,而乌龟我们可以用一个慢指针表示、兔子用一个快指针表示,看代码吧:

代码

public class Solution {
    public boolean hasCycle(ListNode head) {
        // 快慢指针
        ListNode rabbit = head;
        ListNode tortoise = head;
        while (tortoise != null && rabbit != null && rabbit.next != null) {
            rabbit = rabbit.next.next;
            tortoise = tortoise.next;
            if (rabbit == tortoise) 
                return true;
        }
        return false;
    }
}

代码解释
一开始的兔子变量跟乌龟变量都指向head,即都在起点开始跑

ListNode rabbit = head;
ListNode tortoise = head;

由于兔子的速度是2,即每次循环时,兔子 = 兔子的下一个的下一个;乌龟的速度为1,即每次循环时,乌龟 = 乌龟的下一个

rabbit = rabbit.next.next;
tortoise = tortoise.next;

由于每次兔子每次要走2步,因此while中除了要让当前兔子的节点不为空,还要让当前兔子的下一个不为空,这是为了避免空指针的发生。

while (tortoise != null && rabbit != null && rabbit.next != null) 

当两者相遇时,即rabbit == tortoise,则返回true表示存在环形链表,提交代码,完美!
在这里插入图片描述
或许有人有疑问,兔子的速度一定要为2吗?其实不一定的,兔子为2乌龟为1只是为了更快的相遇,当然你也可以改成3、4、5,只不过while中的循环条件也要跟着改,略微麻烦点。


总结

很多人做完算法题之后,下次再拿出来就不会做了,我也遇到这个问题,后来我发现如果一道算法题你掌握了多种解法,并且偶尔回过头来复习,就不会那么容易忘了,这也是我为什么写此类博客的原因:在巩固自己记忆的同时, 又能帮助其他人,何乐而不为呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值