前言
今天这道题目的第一种解法很奇葩,用计时器竟然可以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中的循环条件也要跟着改,略微麻烦点。
总结
很多人做完算法题之后,下次再拿出来就不会做了,我也遇到这个问题,后来我发现如果一道算法题你掌握了多种解法,并且偶尔回过头来复习,就不会那么容易忘了,这也是我为什么写此类博客的原因:在巩固自己记忆的同时, 又能帮助其他人,何乐而不为呢?