一句话总结:
判断链表是否成环就像追人游戏——要么用“记脚印法”(哈希表),要么用“龟兔赛跑法”(快慢指针)!
一、哈希表法(记脚印法)
原理: 每走一步就在本子上记下位置,如果发现重复点就是有环!
data class ListNode(val value: Int, var next: ListNode? = null)
// 哈希表法检测环
fun hasCycleHash(head: ListNode?): Boolean {
val visited = mutableSetOf<ListNode>() // 小本本记下所有走过的节点
var current = head
while (current != null) {
if (current in visited) { // 发现重复脚印!
return true
}
visited.add(current) // 记录新位置
current = current.next // 继续前进
}
return false // 走到头没发现环
}
举个栗子 🌰:
链表:1 → 2 → 3 → 2(指向之前的2,形成环)
遍历到第二个2时发现已存在 → 检测到环
特点:
- 时间复杂度:O(n)(每个节点遍历一次)
- 空间复杂度:O(n)(需要存所有节点)
二、快慢指针法(龟兔赛跑法)
原理: 让快指针(兔子)每次跑两步,慢指针(乌龟)跑一步,如果存在环,兔子最终会追上乌龟!
Kotlin 代码:
fun hasCycleFloyd(head: ListNode?): Boolean {
var slow = head // 乌龟
var fast = head // 兔子
while (fast?.next != null) { // 兔子还能跳两步
slow = slow?.next // 乌龟走一步
fast = fast.next?.next // 兔子走两步
if (slow == fast) { // 龟兔相遇!
return true
}
}
return false // 兔子跑到了终点,没环
}
举个栗子 🌰:
环形链表:1 → 2 → 3 → 1
龟兔同时从1出发:
- 第1轮:龟到2,兔到3
- 第2轮:龟到3,兔到2(绕环一圈)
- 第3轮:龟到1,兔到1 → 相遇!
三、两种方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 哈希表法 | O(n) | O(n) | 快速实现,小规模数据 |
| 快慢指针法 | O(n) | O(1) | 内存敏感,大规模数据 |
四、常见问题解答
Q1:为什么快指针走两步,不能走三步吗?
A:走两步保证一定能相遇(数学可证明)。走三步可能错过,导致死循环。
Q2:如果链表很长,快指针会不会永远追不上慢指针?
A:不会!假设环长L,快慢指针进入环后,快指针每次追近1步(2-1=1),最多追L次必相遇。
Q3:如何处理空链表或单节点链表?
A:代码中的空检查已经覆盖:
- 空链表 → 直接返回false
- 单节点自环 →
fast?.next不为null,进入循环后检测到相遇
五、测试用例
fun main() {
// 测试1:无环链表 1 → 2 → 3
val node3 = ListNode(3)
val node2 = ListNode(2, node3)
val node1 = ListNode(1, node2)
println(hasCycleFloyd(node1)) // false
// 测试2:有环链表 1 → 2 → 3 → 2
node3.next = node2 // 形成环
println(hasCycleFloyd(node1)) // true
// 测试3:单节点自环
val soloNode = ListNode(1)
soloNode.next = soloNode
println(hasCycleFloyd(soloNode)) // true
}
六、终极口诀
链表成环不用愁,
龟兔赛跑解你忧。
快慢指针效率高,
哈希表法也可靠!
环若存在必相遇,
无环终究走到头!