LeetCode 热题 100-----25.回文链表

一、前置知识

在做这道题之前,你必须先搞懂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. 补充:遍历链表的基本逻辑(解题基础)

遍历就是"从头走到尾(或走到循环处)",核心步骤:

  1. 定义一个"指针"(比如current),一开始指向链表的"头节点"(head)------相当于你站在队伍最前面,准备开始排队;

  2. 判断当前指针(current)是不是空:如果是空,说明走到头了(无环);如果不是空,就做相应操作(比如记录节点、移动指针);

  3. 移动指针:current = current.next------相当于你往前走一步,走到下一个人(下一个节点);

  4. 重复步骤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)"为例,逐步看代码怎么运行:

  1. 创建节点:node1(3)、node2(2)、node3(0)、node4(-4),并连接成"node1→node2→node3→node4→node2"(有环);

  2. 创建Solution对象(solution),调用solution.hasCycle(node1),传入的head是node1;

  3. 函数内初始化:visited = 空集合,current = node1(当前指向第一个节点3);

  4. 进入循环(current=node1,不是None):

    1. 判断node1是否在visited(空集合)→ 不在;

    2. 将node1加入visited(现在集合里有{node1});

    3. current = current.next → current指向node2(下一个节点2);

  5. 循环继续(current=node2,不是None):

    1. 判断node2是否在visited({node1})→ 不在;

    2. 将node2加入visited(现在集合里有{node1, node2});

    3. current = current.next → current指向node3(节点0);

  6. 循环继续(current=node3,不是None):

    1. 判断node3是否在visited({node1, node2})→ 不在;

    2. 将node3加入visited(现在集合里有{node1, node2, node3});

    3. current = current.next → current指向node4(节点-4);

  7. 循环继续(current=node4,不是None):

    1. 判断node4是否在visited({node1, node2, node3})→ 不在;

    2. 将node4加入visited(现在集合里有{node1, node2, node3, node4});

    3. current = current.next → current指向node2(因为node4.next=node2);

  8. 循环继续(current=node2,不是None):

    1. 判断node2是否在visited({node1, node2, node3, node4})→ 在!;

    2. 返回True,函数结束;

  9. 测试主函数输出"实际输出: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完全一样,只是语法不同:

  1. 用new创建4个节点,获取节点地址(node1、node2等是指针,存储的是节点的地址);

  2. 连接节点,制造环(node4->next = node2);

  3. 调用solution.hasCycle(node1),传入的是node1(头节点的地址);

  4. 函数内初始化:visited是空的unordered_set,current = node1(指向头节点地址);

  5. 循环遍历,每次判断current指向的节点地址是否在visited中,不在就加入,然后移动指针(current = current->next);

  6. 当current再次指向node2时,node2的地址已经在visited中,返回true;

  7. 主函数输出结果,和预期一致,测试通过。

补充: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)"为例,看快慢指针怎么相遇:

  1. 创建链表(node1→node2→node3→node4→node2),调用hasCycle(node1);

  2. 特殊情况判断:head(node1)不为空,head.next(node2)也不为空,不返回False;

  3. 初始化:slow = node1,fast = node1;

  4. 进入循环(fast=node1≠None,fast.next=node2≠None):

    1. slow = slow.next → slow = node2;

    2. fast = fast.next.next → fast = node2.next.next = node3.next = node4;

    3. 判断slow(node2)和fast(node4)是否相等 → 不相等,继续循环;

  5. 第二次循环(fast=node4≠None,fast.next=node2≠None):

    1. slow = slow.next → slow = node3;

    2. fast = fast.next.next → fast = node2.next = node3;

    3. 判断slow(node3)和fast(node3)是否相等 → 相等!返回True,函数结束;

  6. 测试输出"实际输出:True",测试通过。

补充:无环情况的运行流程(以测试用例5 [5,6,7] 为例):

  1. 初始化slow=node_d(5),fast=node_d(5);

  2. 第一次循环:slow=node_e(6),fast=node_f(7);

  3. 第二次循环:fast=node_f.next = None → 循环条件(fast and fast.next)不满足,循环结束;

  4. 返回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)为例,逐步模拟:

  1. 创建4个节点,用new获取节点地址,连接成"node1→node2→node3→node4→node2"(有环);

  2. 调用solution.hasCycle(node1),传入头节点地址node1;

  3. 特殊情况判断:head(node1)不为空,head->next(node2)也不为空,不返回false;

  4. 初始化:slow = node1,fast = node1(两个指针都指向头节点地址);

  5. 进入循环(fast≠NULL,fast->next≠NULL): slow = slow->next → slow指向node2的地址;

  6. fast = fast->next->next → fast先指向node2,再指向node3,最终指向node4的地址;

  7. 判断slow和fast地址是否相等 → 不相等,继续循环;

  8. 第二次循环(fast≠NULL,fast->next≠NULL): slow = slow->next → slow指向node3的地址;

  9. fast = fast->next->next → fast先指向node2,再指向node3的地址;

  10. 判断slow和fast地址相等 → 相遇,返回true,函数结束;

  11. 主函数输出实际结果为true,与预期一致,测试通过。

补充:无环情况(测试用例5 [5,6,7])运行流程:

  1. 初始化slow=node_d(5的地址),fast=node_d(5的地址);

  2. 第一次循环:slow指向node_e(6的地址),fast指向node_f(7的地址);

  3. 第二次循环:fast->next = NULL(node_f的next为空),循环条件不满足,循环结束;

  4. 返回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默认是空,无法指向其他节点,无法形成环(环需要"尾部节点指向前面的节点",只有一个节点时,前面没有其他节点)。

七、总结

这道题的核心是"判断链表是否有环",记住两个核心解法,就能应对所有场景:

  1. 入门解法(哈希集合):用集合登记节点,重复出现即有环,易理解、好上手,适合入门;

  2. 最优解法(快慢指针):快慢指针赛跑,相遇即有环,O(1)空间,面试必用,记住"慢1快2"的核心逻辑。

补充:不管是Python还是C++,解题的核心逻辑完全一致,只是语法不同------Python用"引用",C++用"指针",对照着看代码,很快就能掌握两种语言的写法。

最后记住:判断环的关键是"节点是否重复出现",不是"数值是否重复";空链表和单个节点一定无环;快慢指针法的循环条件必须判断"fast和fast.next都不为空",避免越界报错。

八、拓展练习

如果想进一步巩固这道题的知识点,可以尝试以下拓展练习(都是同类型高频题):

  1. LeetCode 142. 环形链表II:不仅判断是否有环,还要找到环的入口节点(快慢指针法的延伸);

  2. LeetCode 160. 相交链表:判断两个链表是否相交,和环形链表的"节点判断"逻辑相通;

  3. 尝试用快慢指针法,手动模拟不同环位置的链表,加深对"追及逻辑"的理解。

相关推荐
ʚ希希ɞ ྀ4 小时前
单词拆分----dp
算法
智者知已应修善业5 小时前
【51单片机LED闪烁10次数码管显示0-9】2023-12-14
c++·经验分享·笔记·算法·51单片机
智者知已应修善业5 小时前
【51单片机2按键控制1个敞亮LED灯闪烁和熄灭】2023-11-3
c++·经验分享·笔记·算法·51单片机
AI算法沐枫5 小时前
大模型 | 大模型之机器学习基本理论
人工智能·python·神经网络·学习·算法·机器学习·计算机视觉
吃着火锅x唱着歌5 小时前
LeetCode 1019.链表中的下一个更大节点
算法·leetcode·链表
凌波粒5 小时前
LeetCode--404.左叶子之和(二叉树)
算法·leetcode·职场和发展
paeamecium6 小时前
【PAT甲级真题】- A+B in Hogwarts
c++·算法·pat考试·pat
青山师6 小时前
二叉树与BST深度解析:遍历算法与平衡策略
数据结构·算法·面试·二叉树·算法与数据结构·java面试·数据结构与算法分析