Leetcode Notes_#141,#142_环形链表(剑指Offer#23)
Leetcode Notes_#141,#142_环形链表(剑指Offer#23)
Contents
- 剑指Offer#23,Leetcode上面没有,是在牛客上做的,链接:链表中环的入口结点。
- leetcode#141,#142题和此题类似,所以连同这两题一起做。
Leetcode #141 环形链表
题目
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
思路分析
双指针法
设置一快一慢两个指针,慢指针每次走1步,快指针每次走2步。两个指针同时开始遍历链表。
指针进入环形部分后,两个指针的运动过程类似于我们学过的追及问题。两个指针都在绕圈,一个快,一个慢,那么快指针必然会追上慢指针。
也就是,当快指针和慢指针相遇时,说明相遇的位置就是环里的一个节点,也就证明链表有环。
否则,快指针必定先遇到null,这时直接证明链表没有环(有环就不可能遇到null,而是可以一直绕着环移动)。
特殊输入
- 输入空链表,直接返回false。
- 只有一个节点(有环或无环)的情况,可以代入代码验证。
问题:
快指针每次走2步,慢指针每次走1步,那么会不会出现刚好快指针超过慢指针1步,二者刚好错过,不能相遇情况呢?
解答:
不会出现这种情况,因为如果快指针刚好超过慢指针1步,那说明上一轮迭代时是相遇的。其他情况也类似:
- 如果快指针落后慢指针1步,那么下一轮迭代就会相遇。
- 如果快指针落后慢指针2步,那么下两轮迭代就会相遇。
...
总结规律就是,快指针和慢指针的运动是相对的,每次快指针相对慢指针靠近1步,所以不会出现因为快指针走2步而错过相遇点的情况。
解答
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null) return false;
ListNode fast = head.next;
ListNode slow = head;
while(fast != slow){//fast和slow指针相遇时,循环结束,证明有环
//因为fast指针更新的时候,需要访问fast.next.next,所以fast.next不可以是null
if(fast == null || fast.next == null) return false;
slow = slow.next;//slow前进1步
fast = fast.next.next;//fast前进2步
}
return true;
}
}
Leetcode #142 环形链表II
题目
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
方法1:三步走
这题与剑指Offer#24相同。
整个问题可以分解为三个子问题:
- 寻找环里的其中一个节点(即上述的Leetcode#141题),如果找不到,说明没有环,返回false。
- 快慢指针法,相遇节点记为
meetNode
- 快慢指针法,相遇节点记为
- 计算环的周长
- 快慢指针在
meetNode
处相遇,这时慢指针不动,快指针向后遍历,直到遇到慢指针,这个过程快指针走过的步数就是环的周长,记作n
。
- 快慢指针在
- 寻找环的入口节点
- 重置快慢指针,都指向链表头节点
- 快指针先走
n
步,然后快慢指针一起走,每次走一步 - 由于快指针多走了
n
,也就是比慢指针多走了环的一圈,所以当慢指针到达环入口节点时,快指针也刚好走完一圈,回到入口节点。快慢指针相遇处就是环入口节点。
解答
public class Solution {
//两个指针是类内公用的
ListNode fast;
ListNode slow;
public ListNode detectCycle(ListNode head) {
ListNode meetNode = findMeetNode(head);
if(meetNode == null) return null;//无环
int cycleNum = countCycleNum(meetNode);
fast = head;
slow = head;
for(int i = 1; i <= cycleNum;i++){//fast先走环周长那么多步
fast = fast.next;
}
while(fast != slow){//同时移动,直到相遇
fast = fast.next;
slow = slow.next;
}
return fast;
}
//寻找相遇节点,同141题
public ListNode findMeetNode(ListNode head){
if(head == null) return null;//没有环,返回null
slow = head;
fast = head.next;
while(fast != slow){
if(fast == null || fast.next == null) return null;//没有环,返回null
slow = slow.next;
fast = fast.next.next;
}
return fast;
}
public int countCycleNum(ListNode meetNode){
fast = fast.next;//fast先走一步,否则无法进入循环
int cycleNum = 1;//由于已经走了一步,所以直接初始化为1
while(fast != slow){
fast = fast.next;
cycleNum++;
}
return cycleNum;
}
}
方法2:更简洁的解法,两步走
通过分析数学规律,我们有一种更加简洁的解法,只需要两步。
符号:
- f:
fast
指针走过的路程 - s:
slow
指针走过的路程 - b:环形的周长
- a:入口之前的链表长度
流程:
fast
和slow
从head
出发,slow
一次走一步,fast
一次走两步。如果有环,必定在环中某个位置相遇。
- 到这里为止,
fast
比slow
多走的路程一定是周长的倍数,即:
f = s + nb
- 显然,
fast
指针走的路程是slow
的两倍,即:
f = 2s
- 根据上述两式,可以得出,
slow
所走路程是周长的倍数
s = nb
- 入口之前的链表长度是a,那么从
head
开始走a + nb
步,一定刚好到环形入口节点。也就是说,让现在的slow
再走a步即可。
fast
指针重新回到head
,slow
指针不动。然后让两个指针同时移动,每次移动一步,那么相遇位置刚好是环形入口的节点。
总结来说,这种方法避免了第一种方法里“计算环的周长”这一步,所以代码要简洁不少,但是思路没那么直观,得在纸上简单推导下。
解答
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null) return null;
//这里不可以按照#141的写法,让fast先走一步,那样就不符合fast路程是slow两倍的关系了
ListNode fast = head;
ListNode slow = head;
//因为没有把两个指针错开,所以循环条件也不能直接写成fast != slow,否则循环不会开始
//必须要等两个指针移动之后才可以比较,所以只能这么写
while(true){
if(fast == null || fast.next == null) return null;
slow = slow.next;
fast = fast.next.next;
if(fast == slow) break;
}
fast = head;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
剑指Offer#23 链表中环的入口节点
链接:链表中环的入口结点
本题和leetcode 142一样,按照142题写法,修改下函数输入参数名,即可通过。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步