一、前置知识
在做这道题之前,你必须先搞懂3个核心概念,不然看代码会完全懵------我会用"生活化比喻"讲透,不搞专业术语堆砌,保证完全不懂的人也能看会。
1. 什么是"单链表"?
链表是一种"数据结构",简单说就是"一串连起来的节点",类似我们生活中的"串珠子":
-
每一颗"珠子"就是一个「节点」(对应题目里的ListNode);
-
每颗珠子只能"往后串"(只能通过当前珠子找到下一颗,不能往前找),这就是"单链表"(只有一个方向);
-
正常的链表(无环),最后一颗珠子后面没有其他珠子,也就是"尾部节点"的"下一个指向"是空(Python里是None,C++里是NULL)。
举个生活化例子:排队买奶茶,每个人就是一个节点,你只能看到前面的人(下一个节点),看不到后面的人;队伍的最后一个人,前面没有人了(对应尾部节点,下一个指向空)。
2. 链表的"节点"到底是什么?(对应题目里的ListNode)
不管是Python还是C++,题目里都已经帮我们定义好了"节点"的结构,我们不用自己写,但必须懂它的含义------节点就是一个"容器",里面装了两个东西:
-
val:存储的"数值"(比如示例里的3、2、0、-4),相当于珠子上刻的数字;
-
next:"指针/引用",相当于珠子上系的一根线,指向"下一个节点";如果没有下一个节点,next就是空(None/NULL)。
补充:指针/引用是什么?不用搞复杂,就理解成"地址标签"------比如你家的地址,通过地址能找到你家;next就相当于"下一个节点的地址",通过next就能找到下一个节点。
3. 什么是"链表中的环"?(题目核心判断点)
正常的链表(无环):尾部节点的next是空,遍历的时候会"走到头"(比如排队买奶茶,最后一个人后面没人,队伍就结束了);
有环的链表:尾部节点的next没有指向空,反而指向了链表前面的某个节点,形成一个"循环圈"------就像排队时,最后一个人没有站在队尾,反而跑到队伍中间,和前面的人连在了一起,这样排队就永远排不完(遍历永远停不下来)。
题目里的"pos":是系统内部用来标记"环的连接位置"的(比如pos=1,就是尾部节点连到索引为1的节点),我们写代码的时候完全不用管pos,因为pos不会作为参数传入,我们只需要判断"有没有环",不用管环在哪里。
4. 补充:遍历链表的基本逻辑(解题基础)
遍历就是"从头走到尾(或走到循环处)",核心步骤:
-
定义一个"指针"(比如current),一开始指向链表的"头节点"(head)------相当于你站在队伍最前面,准备开始排队;
-
判断当前指针(current)是不是空:如果是空,说明走到头了(无环);如果不是空,就做相应操作(比如记录节点、移动指针);
-
移动指针:current = current.next------相当于你往前走一步,走到下一个人(下一个节点);
-
重复步骤2-3,直到满足停止条件(走到空节点,或找到环)。
二、题目重新解读
题目给你一个链表的"头节点head",让你写一个函数,判断这个链表有没有环:
-
有环:返回True(Python)/true(C++);
-
无环:返回False(Python)/false(C++);
-
约束条件:链表节点数0~104(可能是空链表,也可能有1万个节点),节点数值范围-105~105(不用管数值大小,只看节点的指向)。
进阶要求:能不能用"常量内存"(O(1)空间)解决?------后面会讲最优解法,满足这个要求。
三、解法一:哈希集合法(最直观,首选,易理解)
1. 核心思路
想象你遍历链表的时候,手里拿一个"登记本"(哈希集合),每遇到一个节点,就把这个节点"登记"在本子上:
-
如果当前节点,已经在"登记本"上了------说明你之前已经遇到过这个节点,现在又遇到了,证明链表有环(不然不会重复遇到);
-
如果当前节点,不在"登记本"上------就把它登记上去,继续往前走;
-
如果走到了空节点(current是None/NULL)------说明链表没有环,遍历结束。
2. 复杂度说明(也能懂)
-
时间复杂度:O(n)------n是链表的节点数,最多遍历所有节点1次(每个节点只登记1次,不会重复遍历);
-
空间复杂度:O(n)------需要用一个集合存储所有遍历过的节点,节点越多,集合越大,占用的内存越多(不满足进阶要求,但胜在直观)。
3. Python 完整代码(逐句注释,每句都讲透)
python
# 1. 导入必要的模块(Optional表示head可能是None,也就是空链表,不用纠结,照抄即可)
from typing import Optional
# 2. 题目固定定义的链表节点类(不用修改,重点看懂类里的两个属性)
class ListNode:
# 节点的"构造函数":创建一个节点时,需要传入节点的数值x
def __init__(self, x):
self.val = x # 节点存储的数值(比如3、2)
self.next = None # 节点的下一个指向,默认是空(None),后续会根据链表结构修改
# 3. 解题的核心类(题目要求的类名是Solution,不能改)
class Solution:
# 4. 解题的核心方法(题目要求的方法名是hasCycle,参数是head,返回值是bool类型)
# head: Optional[ListNode] → 表示head可能是ListNode类型(有头节点),也可能是None(空链表)
# return bool → 返回True(有环)或False(无环)
def hasCycle(self, head: Optional[ListNode]) -> bool:
# 5. 定义一个哈希集合(登记本),用来存储已经遍历过的节点
# 集合的特点:不能存储重复元素,判断"元素是否在集合中"的速度非常快
visited = set()
# 6. 定义当前指针current,一开始指向头节点head(相当于站在队伍最前面)
current = head
# 7. 循环遍历:只要current不是None(没有走到链表尾部),就一直循环
while current:
# 8. 判断当前节点是否已经在集合中(是否已经登记过)
if current in visited:
# 已经登记过 → 重复遇到同一个节点 → 有环,返回True
return True
# 9. 如果没登记过,就把当前节点加入集合(登记一下)
visited.add(current)
# 10. 移动指针:current指向它的下一个节点(往前走一步)
# current.next就是当前节点的"下一个节点地址",通过它就能找到下一个节点
current = current.next
# 11. 循环结束 → 说明current变成了None(走到了链表尾部) → 无环,返回False
return False
# 12. 测试主函数(自己定义,用来执行测试,可以直接运行,看输出结果)
# 作用:创建链表、调用hasCycle方法、输出测试结果,覆盖官方示例+自定义测试用例
def test_hasCycle():
# 测试用例1:官方示例1 → 输入head = [3,2,0,-4], pos = 1 → 预期输出True(有环)
# 步骤1:创建链表节点(一颗颗珠子)
node1 = ListNode(3) # 第一个节点,val=3
node2 = ListNode(2) # 第二个节点,val=2
node3 = ListNode(0) # 第三个节点,val=0
node4 = ListNode(-4) # 第四个节点,val=-4
# 步骤2:连接节点,形成链表(串珠子),并制造环(node4的next指向node2,pos=1)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node2 # 尾部节点指向第二个节点,形成环
# 步骤3:调用解题方法,获取结果
solution = Solution()
result1 = solution.hasCycle(node1)
# 步骤4:输出测试结果
print("测试用例1(官方示例1):")
print(f"输入链表:[3,2,0,-4],环位置pos=1")
print(f"预期输出:True")
print(f"实际输出:{result1}")
print(f"测试结果:{'通过' if result1 == True else '失败'}\n")
# 测试用例2:官方示例2 → 输入head = [1,2], pos = 0 → 预期输出True(有环)
node_a = ListNode(1)
node_b = ListNode(2)
node_a.next = node_b
node_b.next = node_a # 尾部节点指向第一个节点,pos=0,形成环
result2 = solution.hasCycle(node_a)
print("测试用例2(官方示例2):")
print(f"输入链表:[1,2],环位置pos=0")
print(f"预期输出:True")
print(f"实际输出:{result2}")
print(f"测试结果:{'通过' if result2 == True else '失败'}\n")
# 测试用例3:官方示例3 → 输入head = [1], pos = -1 → 预期输出False(无环)
node_c = ListNode(1)
# 不设置环(node_c.next默认是None),pos=-1表示无环
result3 = solution.hasCycle(node_c)
print("测试用例3(官方示例3):")
print(f"输入链表:[1],环位置pos=-1")
print(f"预期输出:False")
print(f"实际输出:{result3}")
print(f"测试结果:{'通过' if result3 == False else '失败'}\n")
# 自定义测试用例4:空链表 → 输入head = [] → 预期输出False(无环)
# 空链表:head是None,没有任何节点
result4 = solution.hasCycle(None)
print("自定义测试用例4(空链表):")
print(f"输入链表:[],环位置pos=-1")
print(f"预期输出:False")
print(f"实际输出:{result4}")
print(f"测试结果:{'通过' if result4 == False else '失败'}\n")
# 自定义测试用例5:3个节点,无环 → 输入head = [5,6,7] → 预期输出False
node_d = ListNode(5)
node_e = ListNode(6)
node_f = ListNode(7)
node_d.next = node_e
node_e.next = node_f
# node_f.next默认是None,无环
result5 = solution.hasCycle(node_d)
print("自定义测试用例5(3个节点无环):")
print(f"输入链表:[5,6,7],环位置pos=-1")
print(f"预期输出:False")
print(f"实际输出:{result5}")
print(f"测试结果:{'通过' if result5 == False else '失败'}\n")
# 自定义测试用例6:5个节点,环在尾部 → 输入head = [10,20,30,40,50],pos=3 → 预期输出True
node10 = ListNode(10)
node20 = ListNode(20)
node30 = ListNode(30)
node40 = ListNode(40)
node50 = ListNode(50)
node10.next = node20
node20.next = node30
node30.next = node40
node40.next = node50
node50.next = node40 # 尾部节点指向第4个节点(pos=3),形成环
result6 = solution.hasCycle(node10)
print("自定义测试用例6(5个节点有环):")
print(f"输入链表:[10,20,30,40,50],环位置pos=3")
print(f"预期输出:True")
print(f"实际输出:{result6}")
print(f"测试结果:{'通过' if result6 == True else '失败'}")
# 13. 执行测试主函数(运行这段代码,就能看到所有测试用例的结果)
if __name__ == "__main__":
test_hasCycle()
4. Python 代码运行流程详解(必看,逐步模拟)
以"测试用例1([3,2,0,-4],pos=1)"为例,逐步看代码怎么运行:
-
创建节点:node1(3)、node2(2)、node3(0)、node4(-4),并连接成"node1→node2→node3→node4→node2"(有环);
-
创建Solution对象(solution),调用solution.hasCycle(node1),传入的head是node1;
-
函数内初始化:visited = 空集合,current = node1(当前指向第一个节点3);
-
进入循环(current=node1,不是None):
-
判断node1是否在visited(空集合)→ 不在;
-
将node1加入visited(现在集合里有{node1});
-
current = current.next → current指向node2(下一个节点2);
-
-
循环继续(current=node2,不是None):
-
判断node2是否在visited({node1})→ 不在;
-
将node2加入visited(现在集合里有{node1, node2});
-
current = current.next → current指向node3(节点0);
-
-
循环继续(current=node3,不是None):
-
判断node3是否在visited({node1, node2})→ 不在;
-
将node3加入visited(现在集合里有{node1, node2, node3});
-
current = current.next → current指向node4(节点-4);
-
-
循环继续(current=node4,不是None):
-
判断node4是否在visited({node1, node2, node3})→ 不在;
-
将node4加入visited(现在集合里有{node1, node2, node3, node4});
-
current = current.next → current指向node2(因为node4.next=node2);
-
-
循环继续(current=node2,不是None):
-
判断node2是否在visited({node1, node2, node3, node4})→ 在!;
-
返回True,函数结束;
-
-
测试主函数输出"实际输出:True",和预期一致,测试通过。
5. C++ 完整代码(逐句注释,和Python对应,能看懂)
cpp
// 1. 导入必要的头文件(unordered_set是哈希集合,用来存储遍历过的节点,必须导入)
#include <iostream> // 用于输入输出(测试主函数用)
#include <unordered_set> // 用于哈希集合(核心容器)
using namespace std; // 简化代码,不用每次写std::(照抄即可)
// 2. 题目固定定义的链表节点结构体(不用修改,和Python的ListNode类对应)
struct ListNode {
int val; // 节点存储的数值
ListNode *next; // 指针,指向ListNode类型(下一个节点的地址),默认是NULL
// 结构体的构造函数:创建节点时,传入数值x,初始化val和next
ListNode(int x) : val(x), next(NULL) {}
};
// 3. 解题的核心类(和Python的Solution类对应)
class Solution {
public:
// 4. 解题的核心方法(和Python的hasCycle方法对应)
// 参数:ListNode *head → 指向头节点的指针(head可能是NULL,即空链表)
// 返回值:bool → true(有环)或false(无环)
bool hasCycle(ListNode *head) {
// 5. 定义哈希集合(登记本),存储的是"ListNode类型的指针"(节点的地址)
// 因为我们要判断"是否重复遇到同一个节点",本质是判断"节点的地址是否重复"
unordered_set<ListNode*> visited;
// 6. 定义当前指针current,一开始指向头节点head(和Python的current=head对应)
ListNode* current = head;
// 7. 循环遍历:只要current不是NULL(没有走到链表尾部),就继续循环
// 注意:C++中判断"非空"用 != NULL,和Python的"while current"逻辑一致
while (current != NULL) {
// 8. 判断当前节点(current指向的节点)是否已经在集合中
// visited.find(current) → 查找current(节点地址)在集合中是否存在
// 如果找到,返回集合中该元素的迭代器(不是end());如果没找到,返回end()
if (visited.find(current) != visited.end()) {
// 找到 → 重复遇到同一个节点 → 有环,返回true
return true;
}
// 9. 没找到,将当前节点的地址加入集合(登记一下)
visited.insert(current);
// 10. 移动指针:current指向它的下一个节点(往前走一步)
// current->next → 访问current指针指向的节点的next成员(下一个节点的地址)
current = current->next;
}
// 11. 循环结束 → current是NULL(走到尾部) → 无环,返回false
return false;
}
};
// 12. 测试主函数(自己定义,和Python的test_hasCycle函数对应,覆盖所有测试用例)
// 作用:创建链表、调用hasCycle方法、输出测试结果,可以直接运行
int main() {
// 定义Solution对象(用于调用解题方法)
Solution solution;
// 测试用例1:官方示例1 → [3,2,0,-4],pos=1 → 预期true
// 步骤1:创建节点(用new关键字,在堆上创建节点,返回节点的地址)
ListNode* node1 = new ListNode(3);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(0);
ListNode* node4 = new ListNode(-4);
// 步骤2:连接节点,制造环
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = node2; // 尾部节点指向node2,pos=1
// 步骤3:调用方法,获取结果
bool result1 = solution.hasCycle(node1);
// 步骤4:输出结果
cout << "测试用例1(官方示例1):" << endl;
cout << "输入链表:[3,2,0,-4],环位置pos=1" << endl;
cout << "预期输出:true" << endl;
cout << "实际输出:" << (result1 ? "true" : "false") << endl;
cout << "测试结果:" << (result1 == true ? "通过" : "失败") << endl << endl;
// 测试用例2:官方示例2 → [1,2],pos=0 → 预期true
ListNode* node_a = new ListNode(1);
ListNode* node_b = new ListNode(2);
node_a->next = node_b;
node_b->next = node_a; // 环在pos=0
bool result2 = solution.hasCycle(node_a);
cout << "测试用例2(官方示例2):" << endl;
cout << "输入链表:[1,2],环位置pos=0" << endl;
cout << "预期输出:true" << endl;
cout << "实际输出:" << (result2 ? "true" : "false") << endl;
cout << "测试结果:" << (result2 == true ? "通过" : "失败") << endl << endl;
// 测试用例3:官方示例3 → [1],pos=-1 → 预期false
ListNode* node_c = new ListNode(1);
// node_c->next默认是NULL,无环
bool result3 = solution.hasCycle(node_c);
cout << "测试用例3(官方示例3):" << endl;
cout << "输入链表:[1],环位置pos=-1" << endl;
cout << "预期输出:false" << endl;
cout << "实际输出:" << (result3 ? "true" : "false") << endl;
cout << "测试结果:" << (result3 == false ? "通过" : "失败") << endl << endl;
// 自定义测试用例4:空链表 → 预期false
bool result4 = solution.hasCycle(NULL); // 传入NULL,代表空链表
cout << "自定义测试用例4(空链表):" << endl;
cout << "输入链表:[],环位置pos=-1" << endl;
cout << "预期输出:false" << endl;
cout << "实际输出:" << (result4 ? "true" : "false") << endl;
cout << "测试结果:" << (result4 == false ? "通过" : "失败") << endl << endl;
// 自定义测试用例5:3个节点无环 → [5,6,7] → 预期false
ListNode* node_d = new ListNode(5);
ListNode* node_e = new ListNode(6);
ListNode* node_f = new ListNode(7);
node_d->next = node_e;
node_e->next = node_f;
// node_f->next默认是NULL,无环
bool result5 = solution.hasCycle(node_d);
cout << "自定义测试用例5(3个节点无环):" << endl;
cout << "输入链表:[5,6,7],环位置pos=-1" << endl;
cout << "预期输出:false" << endl;
cout << "实际输出:" << (result5 ? "true" : "false") << endl;
cout << "测试结果:" << (result5 == false ? "通过" : "失败") << endl << endl;
// 自定义测试用例6:5个节点有环 → [10,20,30,40,50],pos=3 → 预期true
ListNode* node10 = new ListNode(10);
ListNode* node20 = new ListNode(20);
ListNode* node30 = new ListNode(30);
ListNode* node40 = new ListNode(40);
ListNode* node50 = new ListNode(50);
node10->next = node20;
node20->next = node30;
node30->next = node40;
node40->next = node50;
node50->next = node40; // 环在pos=3
bool result6 = solution.hasCycle(node10);
cout << "自定义测试用例6(5个节点有环):" << endl;
cout << "输入链表:[10,20,30,40,50],环位置pos=3" << endl;
cout << "预期输出:true" << endl;
cout << "实际输出:" << (result6 ? "true" : "false") << endl;
cout << "测试结果:" << (result6 == true ? "通过" : "失败") << endl;
// 注意:C++中用new创建的节点,需要手动释放内存(避免内存泄漏,了解即可)
delete node1, node2, node3, node4;
delete node_a, node_b;
delete node_c;
delete node_d, node_e, node_f;
delete node10, node20, node30, node40, node50;
return 0; // 主函数结束
}
6. C++ 代码运行流程详解(和Python一致,对照看)
还是以"测试用例1"为例,C++的运行逻辑和Python完全一样,只是语法不同:
-
用new创建4个节点,获取节点地址(node1、node2等是指针,存储的是节点的地址);
-
连接节点,制造环(node4->next = node2);
-
调用solution.hasCycle(node1),传入的是node1(头节点的地址);
-
函数内初始化:visited是空的unordered_set,current = node1(指向头节点地址);
-
循环遍历,每次判断current指向的节点地址是否在visited中,不在就加入,然后移动指针(current = current->next);
-
当current再次指向node2时,node2的地址已经在visited中,返回true;
-
主函数输出结果,和预期一致,测试通过。
补充:C++和Python的核心区别(不用深究,了解即可):
-
Python用"引用"表示节点的指向,C++用"指针"(地址)表示;
-
Python的集合可以直接存节点,C++的集合存的是节点的地址(ListNode*);
-
C++需要手动释放new创建的节点(避免内存泄漏),Python有自动垃圾回收,不用管。
四、解法二:快慢指针法(最优解,O(1)空间,满足进阶要求)
1. 核心思路(龟兔赛跑比喻,秒懂)
想象两个人在链表上"跑步",一个跑得快(快指针),一个跑得慢(慢指针):
-
慢指针(slow):每次跑1步(每次移动1个节点);
-
快指针(fast):每次跑2步(每次移动2个节点);
-
如果链表无环:快指针会先跑到链表尾部(指向NULL),此时循环结束,返回无环;
-
如果链表有环:快指针会先进入环,然后在环里"绕圈",因为快指针速度比慢指针快,最终一定会"追上"慢指针(两个指针指向同一个节点),此时就可以判断有环。
关键疑问:为什么有环的情况下,快指针一定能追上慢指针?
通俗解释:就像两个人在环形跑道上跑步,快的人速度是慢的2倍,不管慢的人在前面哪个位置,快的人一定会一圈一圈追上慢的人(不会永远追不上)。比如慢的人跑1步,快的人跑2步,每跑一次,两人之间的距离就减少1步,最终一定会相遇。
2. 复杂度说明(满足进阶要求)
-
时间复杂度:O(n)------最多遍历所有节点1次(即使有环,快指针追上慢指针时,也不会超过n步);
-
空间复杂度:O(1)------只用了2个指针(slow和fast),不管链表有多少节点,都只占用固定的内存(常量级),满足题目进阶要求。
3. Python 完整代码(逐句注释,和哈希集合法对比)
python
from typing import Optional
# 题目固定的节点类(和之前一样,不用修改)
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
# 特殊情况处理:空链表(head是None)或只有一个节点(head.next是None),一定无环
# 可以想:空链表没有节点,肯定无环;只有一个节点,next是空,也无环
if not head or not head.next:
return False
# 1. 初始化快慢指针,都从头部节点开始
slow = head # 慢指针:每次走1步
fast = head # 快指针:每次走2步
# 2. 循环条件:快指针不为空,且快指针的下一个节点也不为空
# 为什么要判断fast.next?因为fast每次走2步,如果fast.next是None,fast.next.next会报错(空指针异常)
# 比如fast指向最后一个节点(next是None),再走2步就会越界,所以必须判断fast和fast.next都不为空
while fast and fast.next:
# 慢指针走1步:指向当前节点的下一个节点
slow = slow.next
# 快指针走2步:先指向当前节点的下一个节点,再指向那个节点的下一个节点
fast = fast.next.next
# 3. 判断快慢指针是否相遇(指向同一个节点)
if slow == fast:
# 相遇 → 有环,返回True
return True
# 4. 循环结束 → 快指针走到尾部(fast或fast.next是None) → 无环,返回False
return False
# 测试主函数(和哈希集合法的测试用例完全一样,直接复用,看结果是否一致)
def test_hasCycle_fast_slow():
solution = Solution()
# 测试用例1:官方示例1 → 预期True
node1 = ListNode(3)
node2 = ListNode(2)
node3 = ListNode(0)
node4 = ListNode(-4)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node2
result1 = solution.hasCycle(node1)
print("测试用例1(官方示例1):")
print(f"输入链表:[3,2,0,-4],环位置pos=1")
print(f"预期输出:True,实际输出:{result1},测试结果:{'通过' if result1 else '失败'}\n")
# 测试用例2:官方示例2 → 预期True
node_a = ListNode(1)
node_b = ListNode(2)
node_a.next = node_b
node_b.next = node_a
result2 = solution.hasCycle(node_a)
print("测试用例2(官方示例2):")
print(f"输入链表:[1,2],环位置pos=0")
print(f"预期输出:True,实际输出:{result2},测试结果:{'通过' if result2 else '失败'}\n")
# 测试用例3:官方示例3 → 预期False
node_c = ListNode(1)
result3 = solution.hasCycle(node_c)
print("测试用例3(官方示例3):")
print(f"输入链表:[1],环位置pos=-1")
print(f"预期输出:False,实际输出:{result3},测试结果:{'通过' if not result3 else '失败'}\n")
# 自定义测试用例4:空链表 → 预期False
result4 = solution.hasCycle(None)
print("自定义测试用例4(空链表):")
print(f"输入链表:[],环位置pos=-1")
print(f"预期输出:False,实际输出:{result4},测试结果:{'通过' if not result4 else '失败'}\n")
# 自定义测试用例5:3个节点无环 → 预期False
node_d = ListNode(5)
node_e = ListNode(6)
node_f = ListNode(7)
node_d.next = node_e
node_e.next = node_f
result5 = solution.hasCycle(node_d)
print("自定义测试用例5(3个节点无环):")
print(f"输入链表:[5,6,7],环位置pos=-1")
print(f"预期输出:False,实际输出:{result5},测试结果:{'通过' if not result5 else '失败'}\n")
# 自定义测试用例6:5个节点有环 → 预期True
node10 = ListNode(10)
node20 = ListNode(20)
node30 = ListNode(30)
node40 = ListNode(40)
node50 = ListNode(50)
node10.next = node20
node20.next = node30
node30.next = node40
node40.next = node50
node50.next = node40
result6 = solution.hasCycle(node10)
print("自定义测试用例6(5个节点有环):")
print(f"输入链表:[10,20,30,40,50],环位置pos=3")
print(f"预期输出:True,实际输出:{result6},测试结果:{'通过' if result6 else '失败'}")
# 执行测试
if __name__ == "__main__":
test_hasCycle_fast_slow()
4. Python 快慢指针代码运行流程(逐步模拟,必看)
还是以"测试用例1([3,2,0,-4],pos=1)"为例,看快慢指针怎么相遇:
-
创建链表(node1→node2→node3→node4→node2),调用hasCycle(node1);
-
特殊情况判断:head(node1)不为空,head.next(node2)也不为空,不返回False;
-
初始化:slow = node1,fast = node1;
-
进入循环(fast=node1≠None,fast.next=node2≠None):
-
slow = slow.next → slow = node2;
-
fast = fast.next.next → fast = node2.next.next = node3.next = node4;
-
判断slow(node2)和fast(node4)是否相等 → 不相等,继续循环;
-
-
第二次循环(fast=node4≠None,fast.next=node2≠None):
-
slow = slow.next → slow = node3;
-
fast = fast.next.next → fast = node2.next = node3;
-
判断slow(node3)和fast(node3)是否相等 → 相等!返回True,函数结束;
-
-
测试输出"实际输出:True",测试通过。
补充:无环情况的运行流程(以测试用例5 [5,6,7] 为例):
-
初始化slow=node_d(5),fast=node_d(5);
-
第一次循环:slow=node_e(6),fast=node_f(7);
-
第二次循环:fast=node_f.next = None → 循环条件(fast and fast.next)不满足,循环结束;
-
返回False,测试通过。
5. C++ 完整代码(逐句注释,和Python对应)
cpp
#include <iostream>
using namespace std;
// 题目固定的节点结构体(不用修改)
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
bool hasCycle(ListNode *head) {
// 特殊情况:空链表(head==NULL)或只有一个节点(head->next==NULL),无环
if (head == NULL || head->next == NULL) {
return false;
}
// 初始化快慢指针,都指向头节点
ListNode* slow = head; // 慢指针,每次走1步
ListNode* fast = head; // 快指针,每次走2步
// 循环条件:快指针不为空,且快指针的下一个节点不为空(避免越界)
while (fast != NULL && fast->next != NULL) {
slow = slow->next; // 慢指针走1步
fast = fast->next->next; // 快指针走2步
// 快慢指针相遇 → 有环
if (slow == fast) {
return true;
}
}
// 循环结束 → 无环
return false;
}
};
// 测试主函数(和哈希集合法的测试用例一致,复用即可)
int main() {
Solution solution;
// 测试用例1:官方示例1 → 预期true
ListNode* node1 = new ListNode(3);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(0);
ListNode* node4 = new ListNode(-4);
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = node2;
bool result1 = solution.hasCycle(node1);
cout << "测试用例1(官方示例1):" << endl;
cout << "输入链表:[3,2,0,-4],环位置pos=1" << endl;
cout << "预期输出:true,实际输出:" << (result1 ? "true" : "false") << endl;
cout << "测试结果:" << (result1 ? "通过" : "失败") << endl << endl;
// 测试用例2:官方示例2 → 预期true
ListNode* node_a = new ListNode(1);
ListNode* node_b = new ListNode(2);
node_a->next = node_b;
node_b->next = node_a;
bool result2 = solution.hasCycle(node_a);
cout << "测试用例2(官方示例2):" << endl;
cout << "输入链表:[1,2],环位置pos=0" << endl;
cout << "预期输出:true,实际输出:" << (result2 ? "true" : "false") << endl;
cout << "测试结果:" << (result2 ? "通过" : "失败") << endl << endl;
// 测试用例3:官方示例3 → 预期false
ListNode* node_c = new ListNode(1);
bool result3 = solution.hasCycle(node_c);
cout << "测试用例3(官方示例3):" << endl;
cout << "输入链表:[1],环位置pos=-1" << endl;
cout << "预期输出:false,实际输出:" << (result3 ? "true" : "false") << endl;
cout << "测试结果:" << (!result3 ? "通过" : "失败") << endl << endl;
// 自定义测试用例4:空链表 → 预期false
bool result4 = solution.hasCycle(NULL);
cout << "自定义测试用例4(空链表):" << endl;
cout << "输入链表:[],环位置pos=-1" << endl;
cout << "预期输出:false,实际输出:" << (result4 ? "true" : "false") << endl;
cout << "测试结果:" << (!result4 ? "通过" : "失败") << endl << endl;
// 自定义测试用例5:3个节点无环 → [5,6,7] → 预期false
ListNode* node_d = new ListNode(5);
ListNode* node_e = new ListNode(6);
ListNode* node_f = new ListNode(7);
node_d->next = node_e;
node_e->next = node_f;
bool result5 = solution.hasCycle(node_d);
cout << "自定义测试用例5(3个节点无环):" << endl;
cout << "输入链表:[5,6,7],环位置pos=-1" << endl;
cout << "预期输出:false,实际输出:" << (result5 ? "true" : "false") << endl;
cout << "测试结果:" << (!result5 ? "通过" : "失败") << endl << endl;
// 自定义测试用例6:5个节点有环 → [10,20,30,40,50],pos=3 → 预期true
ListNode* node10 = new ListNode(10);
ListNode* node20 = new ListNode(20);
ListNode* node30 = new ListNode(30);
ListNode* node40 = new ListNode(40);
ListNode* node50 = new ListNode(50);
node10->next = node20;
node20->next = node30;
node30->next = node40;
node40->next = node50;
node50->next = node40; // 环在pos=3,尾部节点指向第4个节点
bool result6 = solution.hasCycle(node10);
cout << "自定义测试用例6(5个节点有环):" << endl;
cout << "输入链表:[10,20,30,40,50],环位置pos=3" << endl;
cout << "预期输出:true,实际输出:" << (result6 ? "true" : "false") << endl;
cout << "测试结果:" << (result6 ? "通过" : "失败") << endl;
// 手动释放所有new创建的节点,避免内存泄漏(了解即可)
delete node1, node2, node3, node4;
delete node_a, node_b;
delete node_c;
delete node_d, node_e, node_f;
delete node10, node20, node30, node40, node50;
return 0; // 主函数正常结束
}
6. C++ 快慢指针代码运行流程详解(对照Python看)
和Python快慢指针的运行逻辑完全一致,仅语法有差异,以测试用例1([3,2,0,-4],pos=1)为例,逐步模拟:
-
创建4个节点,用new获取节点地址,连接成"node1→node2→node3→node4→node2"(有环);
-
调用solution.hasCycle(node1),传入头节点地址node1;
-
特殊情况判断:head(node1)不为空,head->next(node2)也不为空,不返回false;
-
初始化:slow = node1,fast = node1(两个指针都指向头节点地址);
-
进入循环(fast≠NULL,fast->next≠NULL): slow = slow->next → slow指向node2的地址;
-
fast = fast->next->next → fast先指向node2,再指向node3,最终指向node4的地址;
-
判断slow和fast地址是否相等 → 不相等,继续循环;
-
第二次循环(fast≠NULL,fast->next≠NULL): slow = slow->next → slow指向node3的地址;
-
fast = fast->next->next → fast先指向node2,再指向node3的地址;
-
判断slow和fast地址相等 → 相遇,返回true,函数结束;
-
主函数输出实际结果为true,与预期一致,测试通过。
补充:无环情况(测试用例5 [5,6,7])运行流程:
-
初始化slow=node_d(5的地址),fast=node_d(5的地址);
-
第一次循环:slow指向node_e(6的地址),fast指向node_f(7的地址);
-
第二次循环:fast->next = NULL(node_f的next为空),循环条件不满足,循环结束;
-
返回false,测试通过。
五、两种解法对比(必看,快速选择)
为了方便快速选择适合自己的解法,整理了清晰的对比表格,不用记复杂理论,看表格就能懂:
| 对比维度 | 解法一:哈希集合法 | 解法二:快慢指针法 |
|---|---|---|
| 核心思路 | 用集合"登记"遍历过的节点,判断是否重复出现 | 快慢指针"赛跑",有环则快指针追上慢指针 |
| 理解难度 | 低(生活化比喻,一眼懂) | 中(需要理解"环形跑道追及"逻辑) |
| 时间复杂度 | O(n)(最多遍历所有节点1次) | O(n)(追上时不超过n步) |
| 空间复杂度 | O(n)(需要存储所有节点,不满足进阶要求) | O(1)(仅用2个指针,满足进阶要求) |
| 适用场景 | 入门、快速解题、不追求最优空间 | 面试优选、要求常量内存、追求最优解 |
| 代码复杂度 | 简单(逻辑直接,代码量稍多) | 简洁(代码量少,需注意循环条件) |
六、常见问题(避坑指南)
1. 为什么不能用"节点数值"判断环?
很多会误以为"数值重复就是有环",这是典型错误!比如链表为[2,2,2](无环,三个节点数值都是2),用数值判断会误判为有环;而有环链表的节点数值也可能不重复(比如[3,2,0,-4],环位置pos=1,数值都不重复)。
核心原因:判断环的本质是"是否重复遇到同一个节点",而不是"是否遇到数值相同的节点"------节点的核心是"地址/引用",不是"数值",哪怕数值相同,也是不同的节点。
2. 快慢指针法中,为什么快指针每次走2步,不是3步、4步?
可以走3步、4步,但不推荐:
-
走2步是最优选择:逻辑最简单,且一定能追上(每轮距离减少1步,不会跳过慢指针);
-
走3步、4步:可能会"跳过"慢指针(比如快指针从慢指针旁边经过,没相遇),需要更复杂的循环条件,且时间复杂度还是O(n),没必要增加难度。
3. C++中为什么要手动释放节点?Python不用?
因为两种语言的"内存管理方式"不同:
-
C++:用new创建的节点在"堆内存"中,系统不会自动回收,需要手动用delete释放,否则会造成"内存泄漏"(占用的内存一直不释放,影响程序运行);
-
Python:有"自动垃圾回收机制",当节点不再被使用时(比如链表遍历结束,没有指针指向节点),系统会自动回收节点占用的内存,不用手动操作。
4. 空链表、只有一个节点的链表,为什么一定无环?
-
空链表:没有任何节点,自然没有环(环需要至少2个节点才能形成);
-
只有一个节点:节点的next默认是空,无法指向其他节点,无法形成环(环需要"尾部节点指向前面的节点",只有一个节点时,前面没有其他节点)。
七、总结
这道题的核心是"判断链表是否有环",记住两个核心解法,就能应对所有场景:
-
入门解法(哈希集合):用集合登记节点,重复出现即有环,易理解、好上手,适合入门;
-
最优解法(快慢指针):快慢指针赛跑,相遇即有环,O(1)空间,面试必用,记住"慢1快2"的核心逻辑。
补充:不管是Python还是C++,解题的核心逻辑完全一致,只是语法不同------Python用"引用",C++用"指针",对照着看代码,很快就能掌握两种语言的写法。
最后记住:判断环的关键是"节点是否重复出现",不是"数值是否重复";空链表和单个节点一定无环;快慢指针法的循环条件必须判断"fast和fast.next都不为空",避免越界报错。
八、拓展练习
如果想进一步巩固这道题的知识点,可以尝试以下拓展练习(都是同类型高频题):
-
LeetCode 142. 环形链表II:不仅判断是否有环,还要找到环的入口节点(快慢指针法的延伸);
-
LeetCode 160. 相交链表:判断两个链表是否相交,和环形链表的"节点判断"逻辑相通;
-
尝试用快慢指针法,手动模拟不同环位置的链表,加深对"追及逻辑"的理解。