最新出炉的面试题:判断链表是否有环的解法

155 阅读4分钟

最新出炉的面试题:判断链表是否有环的解法

题目背景

在数据结构与算法面试中,"判断链表是否有环"堪称经典问题。这道题不仅出现在LeetCode、剑指Offer等主流题库中,更是各大互联网公司校招/社招的高频考点。其核心在于考察候选人对双指针技巧的掌握程度,以及对循环结构本质的理解。

问题描述

给定一个单链表,请判断该链表是否包含环形结构。若存在环,返回true;否则返回false。

解法演进

基础解法:哈希表法(O(n)空间)

最直观的思路是用哈希表记录访问过的节点:

var hasCycle = function(head) {
    const set = new Set();
    while (head) {
        if (set.has(head)) return true;
        set.add(head);
        head = head.next;
    }
    return false;
}

局限性:虽然时间复杂度O(n),但空间复杂度同样为O(n),对于内存敏感的场景不够友好。


优化解法:快慢指针法(O(1)空间)

算法原理

采用Floyd判圈算法(又称龟兔赛跑算法):

  • 快指针(fast)每次移动两步
  • 慢指针(slow)每次移动一步
  • 若存在环,快慢指针终将相遇

数学证明: 假设环入口距离起点为a,环长为b:

  • 当慢指针进入环后移动距离x时,快指针已移动a+x+nb(n为整数)
  • 由相对速度公式:(x + nb) - x = a → 快指针相对于慢指针每步追1单位
  • 最终两者将在环中某点相遇
代码实现
var hasCycle = function(head) {
    // 处理空链表边界情况
    if (!head) return false;
    
    // 初始化快慢指针
    let slow = head;
    let fast = head.next;
    
    // 循环终止条件:防止空指针异常
    while (fast && fast.next) {
        // 指针移动
        if (slow === fast) return true;
        slow = slow.next;
        fast = fast.next.next;
    }
    
    return false;
}
关键点解析
  1. 初始条件处理:空链表直接返回false
  2. 指针初始化:快指针先行一步,避免初始位置重合
  3. 循环终止条件:双重判断fast && fast.next确保访问安全
  4. 相遇判定:通过引用地址比较判断是否相遇

复杂度分析

  • 时间复杂度:O(n)
    • 无环时:快指针遍历n次
    • 有环时:快慢指针在环内最多追上n次
  • 空间复杂度:O(1)
    • 仅使用两个指针的存储空间

高阶拓展

变体问题1:寻找环的入口节点

当确认链表有环后,如何找到入环的第一个节点?

var detectCycle = function(head) {
    let slow = head, fast = head;
    
    // 检测是否存在环
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow === fast) break;
    }
    
    // 无环情况
    if (!fast || !fast.next) return null;
    
    // 找入环点
    slow = head;
    while (slow !== fast) {
        slow = slow.next;
        fast = fast.next;
    }
    
    return slow;
}

变体问题2:计算环的长度

  1. 先用快慢指针找到相遇点
  2. 从相遇点出发,计算回到原点的步数

变体问题3:判断两个链表是否相交

可转化为环检测问题,将其中一个链表首尾相连,判断另一个链表是否存在环


应用场景

  1. 内存泄漏检测:防止对象引用形成循环导致内存无法释放
  2. 死循环检测:编译器优化时检测程序控制流中的循环
  3. 区块链验证:检测交易链中的异常环状结构
  4. 社交网络分析:发现用户关系中的循环依赖

常见误区

  1. 指针初始化错误:需确保初始位置不同
  2. 循环条件遗漏:容易忘记判断fast.next是否存在
  3. 边界条件处理:空链表、单节点链表等特殊情况
  4. 误判相遇条件:应比较指针引用地址而非节点值

总结

这道题的价值不仅在于本身,更在于其延伸出的算法思想:

  1. 双指针技巧:快慢指针、对撞指针等变体应用广泛
  2. 数学建模能力:将抽象问题转化为数学公式
  3. 空间优化意识:在O(n)与O(1)解法间的权衡选择

建议候选人不仅要掌握代码实现,更要理解背后的数学原理。在面试中,可以进一步探讨:

  • 如何证明该算法的正确性?
  • 该算法能否检测双向链表的环?
  • 在并发环境下如何处理?

掌握这类经典问题,不仅能应对面试,更能培养解决实际工程问题的能力。记住:算法的本质是解决问题的思维模式,而非代码的堆砌。