Java单链表循环检测:快慢指针高效判断(含代码)
2025-03-24 06:08:07
Java 单链表循环检测:揪出隐藏的“环”
单链表,大家肯定不陌生。要是链表里出现了“环”,也就是循环,该怎么办?怎么快速判断链表有没有环?这篇文章咱们就来好好聊聊这个问题,顺便看看一些常见的解决办法。
啥是单链表循环?
简单来说,正常单链表里,每个节点的 next
指针都指向下一个节点,最后一个节点的 next
指针指向 null
。但如果某个节点的 next
指针指向了链表中已经出现过的节点,就形成了一个“环”,这就是单链表循环。 如下图, 就存在循环.
1 -> 2 -> 3 -> 4
^ |
| |
----------
为啥会产生循环?
产生循环的原因五花八门,主要有下面几种:
- 代码错误: 写代码的时候手一抖,把指针指错了。
- 数据损坏: 某些情况下,内存数据可能出错,导致指针异常。
- 特殊场景需求: 极少数情况下, 程序员自己故意写了环状数据.
咋检测循环?
这里列出几种常见的检测方法。
1. 哈希表法
最容易想到的方法,就是用个哈希表(HashSet)记录下访问过的节点。
-
原理:
遍历链表,每访问一个节点,就把它丢进哈希表。如果发现某个节点已经存在于哈希表里了,那肯定就有环。如果遍历到链表结尾(null
),就说明没环。 -
代码示例:
import java.util.HashSet; import java.util.Set; class ListNode { int val; ListNode next; ListNode(int val) { this.val = val; this.next = null; } } public class LinkedListCycleDetection { public static boolean hasCycle(ListNode head) { Set<ListNode> nodesSeen = new HashSet<>(); while (head != null) { if (nodesSeen.contains(head)) { return true; } nodesSeen.add(head); head = head.next; } return false; } public static void main(String[] args) { ListNode head = new ListNode(1); head.next = new ListNode(2); head.next.next = new ListNode(3); head.next.next.next = head.next; // 创建一个环 System.out.println(hasCycle(head)); // 输出 true } }
-
解释: 代码很直观,就是按上面说的原理实现的。
-
进阶: 如果要返回环的入口节点,稍作修改即可,当
nodesSeen.contains(head)
返回True
时,head
就是环入口.
2. 快慢指针法(Floyd 算法)
这个方法比较巧妙,也更省空间。
-
原理:
想象一下,两个人赛跑,一个快一个慢。如果跑道有环,快的那个最终肯定会追上慢的那个。具体到链表,用两个指针,一个
slow
一次走一步,一个fast
一次走两步。如果链表有环,fast
和slow
一定会相遇。 -
代码示例:(原代码的修正)
class ListNode { int val; ListNode next; ListNode(int val) { this.val = val; this.next = null; } } public class LinkedListCycleDetection { public static boolean hasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; // 修正这行: while 条件, 用 && 替换 || while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { return true; } } return false; } public static void main(String[] args) { ListNode head = new ListNode(1); head.next = new ListNode(2); head.next.next = new ListNode(3); head.next.next.next = head.next; System.out.println(hasCycle(head)); } }
```
-
代码解释 & 原代码问题分析
原代码的主要问题出在
while
循环的条件上。 应该是fast != null && fast.next != null
,原代码是fast != null || fast.next != null
。为什么要用
&&
? 假设现在fast 为 null, 那么 fast.next 就是空指针异常. 所以要保证fast 和 fast.next 都有意义(不是null), 才继续走.
如果有一个是null,就说明没有环,直接退出循环。 -
进阶:寻找环的入口
快慢指针相遇后,将slow
指针重置到链表头部,然后slow
和fast
每次都走一步,再次相遇的地点就是环的入口。public static ListNode detectCycleStart(ListNode head) { if (head == null || head.next == null) { return null; // 没有环 } ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { // 找到相遇点 slow = head; // slow 回到起点 while (slow != fast) { slow = slow.next; fast = fast.next; } return slow; // 再次相遇点即为环入口 } } return null; // 没有环 }
- 代码解析
上面代码首先使用快慢指针方法找到快慢指针的相遇点, 然后将slow
指针指回头节点.slow
和fast
指针继续同时往前走(每次都只走一步), 当slow
和fast
再次相等的时候, 所指节点, 就是环的入口点.
- 代码解析
3. 标记法(破坏性方法)
这个方法会修改链表结构。如果题目允许修改链表,可以考虑这种简单粗暴的方法。
-
原理:
遍历链表,每访问一个节点,就给它打个标记。下次再遇到带标记的节点,就知道有环了。
具体实现,可以把节点的next
指针指向一个特殊节点(比如head
自己),或者把节点里的某个值改成特殊值(比如负数,如果值都是正数的话)。 -
代码示例(以修改
next
指针为例):public class LinkedListCycleDetection { public static boolean hasCycle(ListNode head) { if (head == null) { return false; } ListNode current = head; while (current != null) { if (current.next == head) { return true; // 发现环 } ListNode temp = current.next; current.next = head; // 将 next 指向 head,做标记 current = temp; } return false; } public static void main(String[] args) { ListNode head = new ListNode(1); head.next = new ListNode(2); head.next.next = new ListNode(3); head.next.next.next = head.next; System.out.println(hasCycle(head)); } }
-
解释:
代码的核心是current.next = head;
这句。 每次访问一个节点, 把这个节点的next指回头节点. 那么当再次碰到头节点的时候, 说明有循环了. -
注意:
这种方法破坏了链表结构, 使用完之后无法再复原。除非再遍历一次恢复, 所以不推荐经常使用.
总结
单链表循环检测是个挺常见的问题,解决方法也有很多。面试的时候遇到,可以跟面试官沟通下,看看有没有空间限制、能不能修改链表之类的要求,再选择合适的方法。 熟悉每种方法的使用场景和局限性,能帮你做出最佳选择。