返回

Java单链表循环检测:快慢指针高效判断(含代码)

java

Java 单链表循环检测:揪出隐藏的“环”

单链表,大家肯定不陌生。要是链表里出现了“环”,也就是循环,该怎么办?怎么快速判断链表有没有环?这篇文章咱们就来好好聊聊这个问题,顺便看看一些常见的解决办法。

啥是单链表循环?

简单来说,正常单链表里,每个节点的 next 指针都指向下一个节点,最后一个节点的 next 指针指向 null。但如果某个节点的 next 指针指向了链表中已经出现过的节点,就形成了一个“环”,这就是单链表循环。 如下图, 就存在循环.

1 -> 2 -> 3 -> 4
     ^         |
     |         |
     ----------

为啥会产生循环?

产生循环的原因五花八门,主要有下面几种:

  1. 代码错误: 写代码的时候手一抖,把指针指错了。
  2. 数据损坏: 某些情况下,内存数据可能出错,导致指针异常。
  3. 特殊场景需求: 极少数情况下, 程序员自己故意写了环状数据.

咋检测循环?

这里列出几种常见的检测方法。

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 一次走两步。如果链表有环,fastslow 一定会相遇。

  • 代码示例:(原代码的修正)

      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 指针重置到链表头部,然后 slowfast 每次都走一步,再次相遇的地点就是环的入口。

      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指针指回头节点. slowfast指针继续同时往前走(每次都只走一步), 当slowfast再次相等的时候, 所指节点, 就是环的入口点.

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指回头节点. 那么当再次碰到头节点的时候, 说明有循环了.

  • 注意:
    这种方法破坏了链表结构, 使用完之后无法再复原。除非再遍历一次恢复, 所以不推荐经常使用.

总结

单链表循环检测是个挺常见的问题,解决方法也有很多。面试的时候遇到,可以跟面试官沟通下,看看有没有空间限制、能不能修改链表之类的要求,再选择合适的方法。 熟悉每种方法的使用场景和局限性,能帮你做出最佳选择。