题意描述:
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
示例:
- 示例 1
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
- 示例 2
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
- 示例 3
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
解题思路:
Alice:我有点不明白 ?为什么输入有一个 pos
但是函数里面只有一个 head
参数?
Bob:额,是啊,这个 pos
应该是给 leetcode
后台判题的程序用的吧,标准答案怎么可能直接放在输入里面呢、
Alice: 那他题目里面完全没有必要说明这一点啊,程序直接返回了 true
或者 false
, 判题程序的输入暴露给我们干什么,真是的。。。
Bob: 对啊,不过这道题到底要怎么做呢 ?
Alice: 还能怎么做,打标记呗,假如你在森林里面迷了路,为了防止你在兜圈,就在你经过的地方打个标记,绑个红绳呗。
Bob: 哇,对啊,只要见到红绳,就说明在兜圈,就是有环。
Alice: 具体怎么打标记呢 ?用 set
吧,set
里面的元素是哈希存储的,查找起来会很快。
Bob: 当然可以啦,不过用 set
的话就不是 O(1)
的内存使用了 ?
Alice:: 那你有O(1)
内存的方法 ?
Bob: 有啊,链表里面的值都是数字,我们给访问过的节点的值改成字符串,这样只要检测一下要访问的节点的值的类型就知道是不是来过这个节点,就知道是不是有环了。
Alice: hhh, 你这是 street smart 好不好,直接修改链表也亏你想的出来,旁门左道!!
Bob: 直接修改链表是不好,不过我们得到是否有环的答案,还可以再改回去呀,这样遍历两遍链表,O(1)的内存也没有用,就是强制类型转换可能要多耗点时。瞒天过海,判题程序又不知道。
Alice: 肯定还有更好的解法,修改链表,再改回去,一点也不优雅。
Bob: ╭(╯^╰)╮那你来一个优雅的呗
Alice: 你在操场上跑过步吗 ?假设有两个人在环形跑道上不同位置以不同的速度同时起跑,这两个人是不是一定会有相遇的那一刻 ?
Bob: 不同位置,不同速度 ?额鹅鹅鹅,只要是不同速度,无论初始位置是不是相同 都会相遇的, 而且还可以多次相遇,如果两个人的速度都是恒定呢,那么他们还会周期性相遇。
Alice: 对啊,我们可以搞快慢两个指针放到环形区域,快指针一次跑两步,慢指针一次跑一步,如果没有环,两个指针相继到达末尾节点,并不会相遇。相反,如果有环,快慢两个指针就一定会相遇。
Bob: Σ(っ°Д°;)っ你好聪明啊 !
Alice: 哪里哪里,我不是一向如此嘛😉
2023-7-29 更新
Alice: 这题正经的解法叫做快慢双指针,你跑过步吗 ?你在学校的橡胶跑道上面跑过步吗 ?
Bob: 跑过啊
Alice: 那这种解法你应该能很快领悟,它的本质就是在一个环形跑道上,速度快的人一定会多次超过速度慢的人,如果他们一直跑的话。你甚至不用关心他们最初的起始位置是什么样的,他们只需要在跑道上就可以了,速度不同,一定会再次相遇。
Bob: 那快慢两个指针要多快多慢呢 ?
Alice: 这个其实也无所谓,应为链表这个跑道是离散的,快慢两个指针总是跑在节点上。
Bob: 那你如何判断他们相遇呢 ?某个时刻的位置相同 ?任意的速度差都会相遇吗 ?
Alice: 假如能够相遇,他们的速度差是 x, 最后相遇的时刻一定是快指针比慢指针多跑了 k 圈,也就是说环的长度 length 的若干倍能够整除 x 就行。
Bob: length 的长度是不定的,x 还是要挑一下的吧,x == 1
肯定行,x == 2
也行的,多跑两圈呗。
Alice: 这算是证明了任意速度差都行 ?不过最快的还是 x == 1
Bob: 不是的,我们刚才推导的只是必要条件,不是充分条件,只是能相遇的情况下一定满足的。我们换个思路好了,假设最后能相遇,最终差了 k 圈,速度差是 x,最后 k 圈的运动等同于,慢指针的速度是 0,快指针的速度是 x, x 速度跑 k 圈,中途是否会经过慢指针的位置。
Alice: 这样看来速度差还是取 1 最靠谱,其他都有可能跳过的。
Bob: 应该是这样。
代码:
Python 方法一: 修改链表中存储的值,以此标记是否已经访问过这个节点。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
node = head
while node != None:
if type(node.val) == str: # 如果在某个节点中遇到了字符串,说明再次访问了这个节点,即链表有环。
return True
node.val = str(node.val) # 将节点中的数字改为字符串。
node = node.next
return False
Python 方法二: 使用 set 存储链表节点的地址,以此检测是否重复访问节点。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
nodeAddress = set()
while head != None:
if head in nodeAddress: # 若在集合中已经有了该地址,说明已经访问过该节点,即链表有环。
return True
nodeAddress.add(head) # 将节点地址添加到集合中
head = head.next
return False # 如果正常结束了while循环,说明链表无环,返回假。
Java 方法 二:使用 HashSet()
记录已经访问过的节点地址。
/**
* 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) {
Set nodeAddress = new HashSet();
while(head != null){
if(nodeAddress.contains(head)){
return true;
}
nodeAddress.add(head);
head = head.next;
}
return false;
}
}
Python 方法 三:使用快慢两个指针在环形区域访问, 由于是环形区域且快慢两个指针,则两个指针必定会相遇。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
slow = head
if head:
fast = head.next
else:
return False
while slow != None and slow.next != None and fast != None and fast.next != None:
if slow == fast:
return True
slow = slow.next
fast = fast.next.next
return False
Java 方法三:
/**
* 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) {
ListNode slow = head;
ListNode fast = new ListNode(0);
if(head == null){
return false;
}else{
fast = head.next;
}
while(slow != null && slow.next != null && fast != null && fast.next != null){
if(slow == fast){
return true;
}
slow = slow.next;
fast = fast.next.next;
}
return false;
}
}
ts 快慢双指针
function hasCycle(head: ListNode | null): boolean {
if(!head) {
return false;
}
let fast = head?.next;
let slow = head;
// 两个指针都值
while(fast && slow) {
if(fast === slow) {
return true;
}
slow = slow.next;
fast = fast.next?.next;
}
// 链表无环
return false;
};
易错点:
- 使用双指针法的时候需要注意: 在
fast.next
和fast.next.next
之前需要先保证fast != null and fast.next != null
一些测试样例:
[3,2,0,-4]
1
[1, 2]
0
[1]
-1
[1, 2]
-1
[]
-1
答案
true
true
false
false
false
总结: