该部分主要针对链表中环的问题进行总结,提出自己的理解和白银挑战笔记联动,进一步深化对双指针的理解,强化知识点,同时巩固链表寻找倒数第K节点模板,完成对应的变式训练
链表中有关环的问题分成两类,第一类是判断链表中是否存在环结构,第二类是找到链表环结构的入口。两类问题相辅相成,第一类问题是第二类问题的基础,第二类问题反过来加强巩固第一类问题。
1.判断链表中是否存在环结构
这种题目有两种方法解决,分别是双指针(空间复杂度O(1))和集合(空间复杂度O(n)),前者带有一定的思考性存在,但是了解之后记住就行!后者emmm太无脑了,不推荐!(工作中力推!)
集合
这种方法的思想很简单:遍历链表、存储节点、查询节点!遍历过程中,如果节点存在于集合,那么存在环,如果不存在于集合,则存入集合。遍历到表尾还没判断有环,大胆点,没环!直接上代码!
public static boolean hasCycleByMap(ListNode head) {
HashSet<ListNode> set = new HashSet<>();
ListNode pNode = head;
while(pNode != null){
if(set.contains(pNode)){
return true;
}else{
set.add(pNode);
}
pNode = pNode.next;
}
return false;
}
双指针
这种方法的思想,我称其为“相对运动”,试想一下操场上有兔子和乌龟比赛跑步,兔子贪睡,乌龟勤快,若干时间后,乌龟绕操场爬完一圈后总能遇到还在起点贪睡的兔子。
在这里,我们设置快慢指针fast和slow,fast步幅每次比slow多走一步(为什么我不说fast走2步,slow走1步呢?事实上,fast走3步,slow走2步未尝不可!)那么“相对来看”,slow似乎是静止的,fast始终移动,此时fast相当于乌龟,slow相当于兔子,那么在环结构里,fast肯定能跑完一圈再次遇到slow。(这也就是笨鸟先飞吧!)
我是笨鸟,我先上代码!双指针判断链表是否存在环结构是个模板!
我的理解:注意哈,fast走两步的代码逻辑是fast = fast.next.next,因此:fast存在,才能访问fast.next;fast.next存在,才能访问fast.next.next,于是乎while判断条件顺利得出(特别提醒:先有爸妈,才有你,先有fast才有fast.next)
public static boolean hasCycleByTwoPoint(ListNode head) {
if(head == null){
return false;
}
ListNode fast = head, slow = head;
while(fast != null && fast.next != null ){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return true;
}
}
return false;
}
2.寻找链表环结构的入口
这种题目有三种方法解决,分别是两组双指针(空间复杂度O(1))、三组双指针(空间复杂度O(1))和集合(空间复杂度O(n))。
第一种方法思考性强,但是了解之后代码精简;第二种方法,属于是双指针变式应用和寻找链表第K节点模板的变式应用,十分推荐练习巩固思路!最后一种方法嘛,我的评价:******(工作中力推!******:我是谁?)
集合
万事开头难嘛,我们先从简单的来,还是那句话:韩信带净化!特殊情况特殊记!遍历链表、存储节点、查询节点!方法思想同上,唯一一点区别:如果节点存在于集合,那么该节点就是环结构的入口!直接上代码!
public static ListNode detectCycleByMap(ListNode head) {
HashSet<ListNode> set = new HashSet<>();
ListNode pNode = head;
while(pNode != null){
if(set.contains(pNode)){
return pNode;
}else{
set.add(pNode);
}
pNode = pNode.next;
}
return null;
}
两组双指针
这种思想如下:第一组双指针判断环存在!第二组双指针找到环结构入口!当第一组双指针判断环存在的时候,fast==slow,我们记当前节点为Z,紧接着第二组双指针,分别从表头和Z节点开始遍历,首次相等的时候就是环结构的入口!
第二组双指针为什么行?答案如下:
简单情形:fast在第二圈和slow相遇
fast = a+b+c+b
slow = a+b
fast = 2slow
联立方程,求解得:a = c
复杂情形:fast在第n圈和slow相遇
fast = a+n(b+c)+b
slow = a+b
fast = 2slow
联立方程,求解得:a = (n-1)(b+c) + c
为什么slow还是a+b,不可能多跑几圈嘛?答:不可能!且仅能是a+b(自己分析一下,以b = c 即slow跑了半圈为例)
如何理解a = (n-1)(b+c) + c,理解就是第二组双指针,slow在环结构中打转(n-1)圈,然后ptr和slow再运动c长度相遇在环结构入口!
了解清楚为什么之后,直接上代码!(是不是十分清晰,paper tiger!)
public static ListNode detectCycleByTwoPoint(ListNode head) {
//第一次双指针判断是否存在环
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
//存在环
if(slow == fast){
//第二次双指针找到环入口
ListNode ptr = head;
while(ptr != slow){
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
写到这里我不得不补充一下!链表判断环结构是否存在是个模板啊!
三组双指针
这种思想如下:第一组双指针判断环存在!第二组双指针求环结构的长度K!第三组双指针寻找链表倒数第K节点!
这么一看,这道题确实很简单,两个模板,一个简单遍历。真的很简单嘛?答:未必!
在《链表白银挑战笔记中》我们给出了寻找链表倒数第K节点模板,还记得while循环的判断条件嘛?不记得了?那我给你看一下模板代码!(稍等我找找....2000 Years Later)
public static ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow = head, fast = head;
while(fast != null && k > 0){
fast = fast.next;
k--;
}
while(fast != null){
fast = fast.next;
slow = slow.next;
}
return slow;
}
while循环条件是fast != null,当然有问题!环结构中fast不可能为null,这个while岂不是陷入死循环了?难道模板失灵了吗?答:并非如此!
按照模板fast会先走K步,然后slow和fast同步移动,当走到蓝色位置(倒数K-1)时,再走一步就会出现slow和fast指向同一个位置的情况,这个节点就是倒数第K个节点,模板中while循环条件也应该修改为while(fast != slow),纸上谈兵?NO!直接上代码!
public static ListNode detectCycleByThreePoint(ListNode head){
//第一组双指针判断环
ListNode slow = head, fast= head;
boolean existCircle = false;
int len = 1;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
//存在环
if(slow == fast){
existCircle = true;
break;
}
}
if(existCircle){
//第二组双指针求环长
ListNode pNode = slow;
while(pNode.next != slow){
len++;
pNode = pNode.next;
}
//第三组双指针求链表倒数k节点
slow = head;
fast = head;
while(fast != null && len > 0){
fast = fast.next;
len--;
}
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return slow;
}else{
return null;
}
}
纸上得来终觉浅,绝知此事要躬行!Ok,《算法通关村第一关——链表黄金挑战笔记》结束,喜欢的朋友三联加关注!关注鱼市带给你不一样的算法小感悟!(幻听)
再次,感谢鱼骨头教官的学习路线!鱼皮的宣传!小y的陪伴(终于想好了!)!ok,拜拜,第二关第一幕见!