229 lines
6.1 KiB
Markdown
229 lines
6.1 KiB
Markdown
---
|
||
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)
|
||
- 优点:实现简单,直观易懂
|
||
- 缺点:需要额外的空间
|
||
|
||
|
||
|
||
|
||
|