最新出炉的面试题:判断链表是否有环的解法
题目背景
在数据结构与算法面试中,"判断链表是否有环"堪称经典问题。这道题不仅出现在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;
}
关键点解析
- 初始条件处理:空链表直接返回false
- 指针初始化:快指针先行一步,避免初始位置重合
- 循环终止条件:双重判断
fast && fast.next确保访问安全 - 相遇判定:通过引用地址比较判断是否相遇
复杂度分析
- 时间复杂度: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:计算环的长度
- 先用快慢指针找到相遇点
- 从相遇点出发,计算回到原点的步数
变体问题3:判断两个链表是否相交
可转化为环检测问题,将其中一个链表首尾相连,判断另一个链表是否存在环
应用场景
- 内存泄漏检测:防止对象引用形成循环导致内存无法释放
- 死循环检测:编译器优化时检测程序控制流中的循环
- 区块链验证:检测交易链中的异常环状结构
- 社交网络分析:发现用户关系中的循环依赖
常见误区
- 指针初始化错误:需确保初始位置不同
- 循环条件遗漏:容易忘记判断
fast.next是否存在 - 边界条件处理:空链表、单节点链表等特殊情况
- 误判相遇条件:应比较指针引用地址而非节点值
总结
这道题的价值不仅在于本身,更在于其延伸出的算法思想:
- 双指针技巧:快慢指针、对撞指针等变体应用广泛
- 数学建模能力:将抽象问题转化为数学公式
- 空间优化意识:在O(n)与O(1)解法间的权衡选择
建议候选人不仅要掌握代码实现,更要理解背后的数学原理。在面试中,可以进一步探讨:
- 如何证明该算法的正确性?
- 该算法能否检测双向链表的环?
- 在并发环境下如何处理?
掌握这类经典问题,不仅能应对面试,更能培养解决实际工程问题的能力。记住:算法的本质是解决问题的思维模式,而非代码的堆砌。