你有没有遇到过这种奇葩情况:写了个遍历链表的代码,结果程序卡到没反应,控制台疯狂转圈,最后报个“栈溢出”?大概率是链表偷偷形成了“环”——就像迷宫里的死循环,指针进去就绕不出来了!今天用“操场跑步抓小偷”的段子给你讲透怎么揪出环,两个指针搞定,看完笑到会写代码,再也不怕程序被“环”卡死~
先懂“链表环”:像小朋友手拉手围成圈,进去就出不来
正常的链表是“一条道走到黑”,最后一个节点的`next`指向`null`,就像小朋友排队一直往前走,最后一个人后面没人。但如果某个节点的`next`指向了前面的某个节点,就形成了“环”——相当于排尾的小朋友突然抓住了排头的手,围成一个圈,你从任何一个人开始数,永远数不完,指针遍历到这也会卡死!
举个栗子:链表`1→2→3→4→2`,4的`next`不是`null`而是2,就形成了`2→3→4→2`的环。指针走到2,就会在2→3→4→2之间无限循环,程序要么卡死,要么抛出异常,新手遇到这种情况,往往会懵圈:“我代码没写错啊,怎么跑不完?”
抓环神器:快慢指针像“警察抓小偷”,同圈必相遇
判断链表有没有环,最牛的方法是“快慢指针”,原理超简单:让两个指针在链表上跑,一个慢(一次走1步),一个快(一次走2步)。如果链表有环,快指针迟早会追上慢指针(就像跑步时快的总能套慢的一圈);如果没环,快指针会先跑到终点(`null`)。
这就像操场抓小偷:
- 你(快指针)一步跨2阶,小偷(慢指针)一步跨1阶,同时从起点出发;
- 如果操场是环形(有环),你速度比小偷快,迟早会追上他(相遇);
- 如果操场是直线(没环),你会先跑到终点,小偷还在后面磨蹭。
Java实现3步走:操场跑步 analogy 全还原
用“快慢指针”判断环,代码简单到离谱,三步就能搞定,对应操场抓小偷的场景:
1. 初始化“快慢选手”:找两个指针当“跑步者”
先定义两个指针,都从链表头节点出发:
- `slow`(慢指针):一次走1步,像“小乌龟”;
- `fast`(快指针):一次走2步,像“小兔子”。
Java代码:
// 定义链表节点
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
public class HasCycle {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false; // 空链表或只有一个节点,肯定没环
}
ListNode slow = head; // 慢指针(小乌龟)
ListNode fast = head.next; // 快指针(小兔子),先多走一步,避免初始就相遇
// 后面步骤写这里...
}
}2. 让“选手”开始跑步:循环移动指针
让两个指针同时在链表上跑,`slow`每次走1步(`slow = slow.next`),`fast`每次走2步(`fast = fast.next.next`)。就像小乌龟和小兔子同时出发,一个慢一个快。
循环条件:只要`fast`和`slow`没相遇,就一直跑。
代码:
while (slow != fast) { // 没相遇就继续跑
// 快指针跑到头了(没环),直接返回false
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next; // 小乌龟走1步
fast = fast.next.next; // 小兔子走2步
}3. 判断是否相遇:相遇就是有环,跑到头就是没环
如果`fast`和`slow`相遇了(`slow == fast`),说明链表有环(就像小兔子追上了小乌龟,证明操场是环形);如果`fast`跑到`null`了(`fast == null`或`fast.next == null`),说明没环(直线跑道,小兔子先到终点)。
最后返回`true`(相遇=有环):
return true; // 跳出循环说明slow == fast,有环!完整代码加起来不到20行,却能高效判断环,这就是“算法的魔力”!
避坑提醒:这2个坑能让“抓环”失败,新手必踩!
1. **初始指针位置错:两个指针都从head出发**
如果你让`slow`和`fast`一开始都指向`head`(`fast = head`),而链表刚好头节点自己形成环(`head.next = head`),第一次循环就会判定`slow == fast`,正确返回有环。但如果是正常环,比如`1→2→3→1`,这种初始位置也能判断。不过更规范的是让`fast`先多走一步(`fast = head.next`),避免一些边界判断失误,就像跑步让快的人稍微领先一点,规则更清晰。
2. **没判断fast.next是否为null:快指针“踩空”**
`fast`每次走2步,必须确保`fast.next`不为`null`,不然`fast.next.next`会报空指针异常(`NullPointerException`)。就像小兔子跨2步时,得确保脚下有台阶,不然会摔沟里。所以循环里一定要先判断`fast == null || fast.next == null`,再移动指针。
为啥快慢指针这么牛?效率吊打“哈希表法”
判断环还有种方法是“哈希表记录走过的节点”:每遍历一个节点就存进HashSet,遇到重复节点就是有环。但这种方法要额外占O(n)的空间(存所有节点),就像跑步时带个笔记本记每一步,累得慌。
而快慢指针法只需要2个指针,空间复杂度O(1),时间复杂度O(n)(最多跑两圈),就像光凭腿跑步,不用带任何东西,轻量又高效——这就是算法的“四两拨千斤”!
互动时间:来测测你的“抓环实战力”!
1. 链表`1→2→3→4→2`(4指向2),用快慢指针判断,会相遇吗?相遇在哪个节点?(提示:画图模拟一下,慢指针走1步,快指针走2步)
2. 如果链表是`1→2→3→null`(无环),快指针会先跑到哪个位置?
3. 你写代码时遇到过“链表环导致卡死”的情况吗?当时排查了多久才发现是环的问题?评论区说说你的“抓环血泪史”!
评论区交出你的答案,前3名答对的送“Java链表调试手册”(含环检测、环入口查找的通俗讲解+代码模板)!关注我,下期揭秘“如何找到链表环的入口位置”——比判断环多一步,却能帮你彻底根治链表死循环,实用到爆~
