blog/content/post/算法题/单链表判环.md
2025-02-12 19:15:28 +08:00

229 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 单链表判环
description: 判定一个单链表是否存在环
date: 2025-02-10T00:10:00+08:00
slug: 单链表判环
# image: helena-hertz-wWZzXlDpMog-unsplash.jpg
categories:
- 算法题
tags: [
"链表",
"单链表",
"环",
"快慢指针",
"算法",
"哈希表"
]
math: true
---
> 给定一个链表,如果它是有环链表,实现一个算法返回环路的开头节点。若环不存在,请返回 `null`。<br>如果链表中有某个节点,可以通过连续跟踪 `next` 指针再次到达,则链表中存在环。
下面介绍两种解决方法快慢指针法Floyd 判圈算法)和哈希表法。链表节点定义如下:
```java
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
```
## 方法一:快慢指针法
### 思路
1. **检测环是否存在**
使用两个指针:慢指针 `slow` 每次走一步,快指针 `fast` 每次走两步。如果链表有环,则快慢指针必然在环内相遇;如果无环,快指针会遇到 `null`
2. **寻找环的入口**
当快慢指针相遇时,让其中一个指针(例如 `slow`)回到链表头,然后两个指针都每次走一步。它们下一次相遇的节点就是环的入口点。
**证明(简要说明):**
设链表头到环入口的距离为 `L`,环的长度为 `C`,相遇点距离环入口的距离为 `D`。相遇时,快指针走的距离为 `L + mC + D`,慢指针走的距离为 `L + nC + D`,并且快指针走的路程是慢指针的两倍,所以有:
$$
2(L + nC + D) = L + mC + D
$$
化简得到:
$$
L + D = (m - 2n) \times C
$$
也就是说,从头到环入口的距离 `L` 等于从相遇点到环入口沿环走的距离(补全环一圈的剩余距离),因此当两个指针分别从链表头和相遇点出发以相同速度前进时,会在环入口相遇。
### 实现代码
#### Java
```java
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
boolean hasCycle = false;
// 第一阶段:判断是否存在环
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
if (slow == fast) { // 相遇,说明有环
hasCycle = true;
break;
}
}
if (!hasCycle) {
return null; // 无环,直接返回 null
}
// 第二阶段:寻找环的入口
slow = head; // 将慢指针移回链表头
while (slow != fast) {
slow = slow.next;
fast = fast.next; // 两指针均一次走一步
}
// 当两指针相遇时,就是环的入口点
return slow;
}
}
```
#### C++
```c++
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
if (head == nullptr) {
return nullptr;
}
ListNode* slow = head;
ListNode* fast = head;
bool hasCycle = false;
// 第一阶段:判断是否存在环
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next; // 慢指针走一步
fast = fast->next->next; // 快指针走两步
if (slow == fast) { // 相遇,说明存在环
hasCycle = true;
break;
}
}
if (!hasCycle) {
return nullptr; // 无环,返回 nullptr
}
// 第二阶段:寻找环的入口
slow = head; // 慢指针回到链表头
while (slow != fast) {
slow = slow->next;
fast = fast->next; // 两个指针均一次走一步
}
// 此时 slow 和 fast 相遇,指向环的入口节点
return slow;
}
};
```
---
## 方法二:哈希表法
### 思路
利用一个哈希表(或哈希集合)来记录已经遍历过的节点。遍历链表时:
- 如果当前节点已在哈希集合中,则说明该节点是环的入口(第一次重复出现的节点)。
- 如果遍历过程中遇到 `null`,则说明链表无环。
这种方法虽然简单直观,但需要额外的空间,其空间复杂度为 O(n)。
### 实现代码
#### Java
```java
import java.util.HashSet;
import java.util.Set;
public class Solution {
public ListNode detectCycleWithHashTable(ListNode head) {
Set<ListNode> visited = new HashSet<>();
ListNode current = head;
while (current != null) {
// 如果当前节点已经存在于集合中,则找到环的入口
if (visited.contains(current)) {
return current;
}
visited.add(current);
current = current.next;
}
// 如果遍历结束都没有重复节点,则链表无环
return null;
}
}
```
#### C++
```c++
#include <unordered_set>
class Solution {
public:
ListNode* detectCycleWithHashTable(ListNode* head) {
std::unordered_set<ListNode*> visited;
ListNode* current = head;
while (current != nullptr) {
// 如果当前节点已经访问过,则找到了环的入口
if (visited.find(current) != visited.end()) {
return current;
}
visited.insert(current);
current = current->next;
}
// 遍历结束仍未发现重复节点,说明链表无环
return nullptr;
}
};
```
---
## 总结
- **快慢指针法**
- 时间复杂度O(n)
- 空间复杂度O(1)
- 优点:不需要额外空间,效率高
- 缺点:理解其证明和算法思路需要一定的数学推导
- **哈希表法**
- 时间复杂度O(n)
- 空间复杂度O(n)
- 优点:实现简单,直观易懂
- 缺点:需要额外的空间