一、题目
网址:https://leetcode-cn.com/problems/linked-list-cycle-ii/
题目:142. 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
二、解题
1.分析题目:
目的:判断链表是否是环,如果是环则返回环的第一个节点
输入:输入头节点(例子中pos只是为了告知你该链表是否有环,不为输入)
输出:输出头节点位置或者null
官方测试用例:
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。
提示内容:引入一个pos表示有效索引
注意:无
2.思考+思路
此题是141环形链表的变种,141里面只需要判断是否有环,而该题在判断是否有环之后,若有环还需输出环的第一个节点。而此题的解题方法和141类似,暴力、哈希表、双链表。
2.1自己思考1~~~暴力破解
暴力破解感觉是这三个方法里面最不好弄的一个,在找初始节点时需要两个for循环叠加,一次次比对。
思路1~~~从第一个点开始遍历,让其与较第二或第三,第10010比较,如果有相等的则直接输出该点,如果没有相等则开始第二个点,以此类推。
2.2自己思考2~~~哈希表
哈希表就比较简单了,定义一个哈希表,将所有Node都存入哈希表内,当哈希表重复时则表明出现了重复的点,则直接输出Node即可,如果到最后都没有点,则输出null即可。
2.3自己思考3~~~双链表
首先通过双链表,一个快一个慢,慢指针每次移动一步,而快指针每次移动两步。然后当快的追上慢快的追上慢的则表明是一个环,当快的为null则表明不是环。
当为环的时候,从第一个点开始遍历,当fast走一圈的时候还没点等于第一个点,则对下一个点进行遍历。
2.4题解思路1~~~双链表
参考链接:
强烈建议去看这个题解,太优秀了,太优秀了。
总结起来如下:
设快的节点走了f步,慢的走了s步,一个环有b个节点。
快的走的节点个数是慢的2倍即: f=2s
快的和慢的重合时多走了n圈即: f=s+nb
两式相减得s=nb
此时精彩的来了,已知慢的走了s步即nb步。若此时有一个点从起点出发,当他走到环起始点的时候这个慢的点走到哪里?
当一个新的点从出发点走到初始点的时候走了a步,此时在a这个位置
而慢的已经走了s步即nb步,当他也走了s+a即nb+a,此时也在a这个位置
即我们定义一个点从初始点出发,该点和slow一起走,当相等时就是初始点。
注:看完解析之后发现这才是正解,自己的思路还是差好多,自己让每一个点都进行了比较,实际上没有必要,大佬通过数学运算简化了步骤与思考。
3.实现
3.1暴力破解
/**
* 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 ans = head;
//节点最多1000次,定义一个1010的循环,如果1010次循环还没返回false则一定为环
for(int i=0;i<10010;i++)
{
//如果next为空则表示无下个节点了,表示不为环返回false
if(ans.next == null)
return null;
//获取下一个节点
ans = ans.next;
}
//此时确定有环,两个for循环来寻找初节点
//定义初节点
ans = head;
ListNode ans2 = head.next;
if (ans.next == ans) {
return ans;
}
while(true)
{
for(int i=0;i<10010;i++)
{
//如果匹配的上,则表示是循环开始的第一个节点
if (ans == ans2) {
return ans;
}
//匹配不上ans2则进入下一个节点
ans2 = ans2.next;
}
// 该节点不是环的第一个节点,开始循环下一个节点
ans = ans.next;
ans2 = ans.next;
}
}
}
执行结果:通过
执行用时:222 ms,在所有Java提交中击败了6.94%的用户
内存消耗:39 MB,在所有Java提交中击败了50.60%的用户
分析:这个时间消耗没谁了,主要还是这个10010次的循环太耗时了。
3.2哈希表
/**
* 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) {
// 定义一个哈希表
Set<ListNode> ans = new HashSet<>();
// 定义一个Node存储值
ListNode ans_Node = head;
while (ans_Node != null) {
// contains()方法用于检查是否有任何键映射到给定值元素(val_ele)中。
if (ans.contains(ans_Node)) {
return ans_Node;
} else {
// 将ans_Node添加到ans中
ans.add(ans_Node);
}
ans_Node = ans_Node.next;
}
return null;
}
}
执行结果:通过
执行用时:5 ms,在所有Java提交中击败了20.40%的用户
内存消耗:39.8 MB,在所有Java提交中击败了15.95%的用户
分析:该方法和141完全一样,一个哈希表解决了所有问题
3.3双指针
/**
* 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 slow = head;
ListNode fast = head.next;
//定义一个ans节点
ListNode ans = head;
//当快的和慢的相等则表示追上了,则肯定有环
while (slow != fast) {
//对不是环的情况进行判断
if (fast == null || fast.next == null) {
return null;
}
//设置快慢两个速度
slow = slow.next;
fast = fast.next.next;
}
//此时已经得知为环,但是不知道第一个节点是哪个
while(true)
{
//如果ans == fast则表明该点为第一个节点
if(ans == fast){
return ans;
}
fast = fast.next;
//当fast==slow表示走了一轮,则ans进入下一个进行判断
if(fast == slow){
ans = ans.next;
fast = fast.next;
}
}
}
}
执行结果:通过
执行用时:23 ms,在所有Java提交中击败了6.94%的用户
内存消耗:39 MB,在所有Java提交中击败了48.11%的用户
分析:在使用快慢双指针
三、其他答案
1.双指针
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
while (true) {
//快慢两个节点,让其每次移动步数相差1
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
//当fast=slow表示是环跳出循环
if (fast == slow) break;
}
//后期fast没用了,直接让其为那个从初始点出发的点
fast = head;
//这一步看解析,思考量很大
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
执行结果:通过
执行用时:0 ms,在所有Java提交中击败了100.00%的用户
内存消耗:38.9 MB,在所有Java提交中击败了63.86%的用户
分析:这个结果好优啊,通过减少环的比较时间大大优化了速率
四、总结
本题整体难度较低,通过Java方法解题一般内存都在39M上下浮动,但速度方面相差甚多。
必看学习连接如下:
环形链表 II:https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode/
环形链表 II(双指针法,清晰图解):https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/linked-list-cycle-ii-kuai-man-zhi-zhen-shuang-zhi-/
练习本题需要掌握两个知识点——双指针和哈希表
其中第二次碰撞也是属于点睛之笔,很秀。
双指针主要是定义两个指针来进行一些距离限定或者两节点相等的操作
双指主要针用于如下场景:获取倒数第k个元素,获取中间位置的元素,判断链表是否存在环,判断环的长度等和长度与位置有关的问题。
哈希表主要是需要掌握可以使用哈希表来存储指针的内容。
五、Python
个人在学习Python,所以用Python实现了下上述方法。
1.暴力破解
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
if not head or not head.next: #节点不存在或下一个节点不存在
return
node_1 = head
node_2 = head
for i in range(0,10010):
if not node_1.next:
return
node_1 = node_1.next
node_1 = head
node_2 = head.next
while 1:
for i in range(0,10010):
if node_1 == node_2:
return node_1
node_2 = node_2.next
node_1 = node_1.next
node_2 = node_1.next
# Python中没有return null直接return 空即可
# while true 也没有,有while 1
执行结果:通过
执行用时:9340 ms,在所有Python3提交中击败了5.03%的用户
内存消耗:16.4 MB,在所有Python3提交中击败了34.83%的用户
注:这个运行时间没谁了,不过发现整体Python比Java耗时多,度娘也没特别好的解释,就是说启动一些加载项或者隐形代码的问题,但没有特别信服的结果。
2.表
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
a = set() # 定义集合
while head:
# 如果head已存在于a
if head in a:
return head
# 当head不在a里面,则添加head
a.add(head)
head = head.next
return
执行结果:通过
执行用时:80 ms,在所有Python3提交中击败了14.74%的用户
内存消耗:16.9 MB,在所有Python3提交中击败了5.16%的用户
注:这个的耗时就比较好点了,不过感觉还是没有特别优,还是存储耗费时间啊。
3.双指针
class Solution(object):
def detectCycle(self, head):
# 定义快慢两个节点
fast = head
slow = head
while True:
if not (fast and fast.next):
return
fast = fast.next.next
slow = slow.next
if fast == slow:
break
fast = head
while fast != slow:
fast, slow = fast.next, slow.next
return fast
执行结果:通过
执行用时:56 ms,在所有Python3提交中击败了94.60%的用户
内存消耗:16.2 MB,在所有Python3提交中击败了86.87%的用户
注:这个内存变化就很恐怖,提高了0.7超过了80%多的用户。然后耗时也明显提高了,双指针+大佬的思路果然是好的。