力扣hot100-链表专题-刷题笔记(一)

链表专题 刷题笔记

专题标签:链表 - 指针操作 / 递归专题核心考点:链表的遍历、指针修改、结构操作(反转 / 合并 / 拆分)、特殊场景(环 / 相交 / 随机指针)

题目 1:相交链表(双指针解法)(难度:简单)

1、题目核心定义

  1. 相交定义:两个链表存在「地址相同的公共节点链」(尾部完全重合),而非"节点值相同"或"位置对应";

  2. 输入:两个无环链表的头节点 headA、headB;

  3. 输出:相交节点(无相交则返回 null)。

2、双指针解法核心逻辑(底层原理)

核心思想:强制让两个指针走相同总路程(m+n,m、n为两链表长度),同步移动下要么中途相遇(相交),要么终点碰头(均为null,不相交),以此抵消长度差(隐式对齐)。

两种最终场景:

  • 场景1(相交):指针会在「公共节点链入口」相遇(总路程未到m+n时);

  • 场景2(不相交):指针会在「总路程刚好m+n时」同时指向null(无公共节点可走)。

  • 场景 1:相交(前提:存在地址相同的公共节点)
    • 子场景 1:两链表「不相交节点数相同」→ 双指针同步移动,无需走满 m+n 就相遇在公共节点;
    • 子场景 2:两链表「不相交节点数不同」→ 双指针走满 m+n 前,必会在公共节点相遇;
    • 核心:只要有公共节点,指针必然在「公共节点入口」碰头,不会走满全程
  • 场景 2:不相交(本质:无任何公共节点,平行关系)
    • 子场景 1:两链表长度不同 → 双指针走满 m+n 总路程,同时指向 null
    • 子场景 2:两链表长度相同 → 双指针同步遍历完自身链表,同时指向 null
    • 核心:无公共节点时 ,指针最终必同时为 null,不会中途相遇;
  • 关键规则
    • 循环条件:判断 pA == pB(指针本身相等),绝对不能用 .next 判断 → 否则不相交场景会触发无限循环;
    • 相交判定:仅看「节点引用(地址)」,而非 val(避免值同节点不同的误判)。

3、标准模板代码(Java版,面试直接写)

全程仅分两种场景,且前置必做边界判断

  1. 前置边界 :先判断 headA/headB 是否为空 → 任一为空则直接返回 null(空链表无相交可能,无需进入核心逻辑);

    // 1. 链表节点定义(题目已给,无需手写,了解即可)
    class ListNode {
    int val;
    ListNode next;
    ListNode(int x) {
    val = x;
    next = null;
    }
    }

    // 2. 核心解题方法
    public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    // 边界条件:任一链表为空,直接无相交
    if (headA == null || headB == null) {
    return null;
    }

    复制代码
         // 初始化双指针,分别指向两链表头
         ListNode pA = headA;
         ListNode pB = headB;
         
         // 循环条件:指针未相遇(相遇则返回,包括均为null的情况)
         while (pA != pB) {
             // 指针不为空则后移,为空则跳转到另一链表头(补全路程差)
             pA = (pA != null) ? pA.next : headB;
             pB = (pB != null) ? pB.next : headA;
         }
         
         // 最终:pA == pB,要么是相交节点,要么是null
         return pA;
     }

    }

4、代码关键细节

  1. 循环条件:判断 pA == pB(指针本身相等),绝对不能用 .next 判断 → 否则不相交场景会触发无限循环;

  2. 指针跳转逻辑:必须是「为空时才跳转」,而非"走完自己链表就跳转"。比如pA遍历完A链表(到null)后再跳B,而非没到null就跳,避免漏走A的最后一个节点,导致路程计算错误。

  3. 避免无限循环误区:无需担心循环无法终止。因为两指针总路程固定为m+n,最多遍历m+n步后,要么相遇(相交节点),要么同时为null(不相交),必然触发pA==pB的终止条件。

  4. 链表有环的边界:该解法仅适用于「无环链表」。若链表可能有环,需先判断链表是否有环、找到环入口,再结合长度差逻辑判断相交,直接套用此模板会出错。

5、复杂度分析(面试必说)

  1. 时间复杂度:O(m + n),最坏情况需遍历两链表各一次(总路程m+n);

  2. 空间复杂度:O(1),仅用2个指针变量,无额外空间开销(最优解)。

6、典型场景验证(快速理解)

场景1:相交(长度不等)

A:A1→A2→C3→C4(m=4);B:B1→C3→C4(n=3);公共节点C3→C4

指针移动路径:

pA:A1→A2→C3→C4→null→B1→C3(第7步相遇C3);

pB:B1→C3→C4→null→A1→A2→C3(第7步相遇C3);

总路程:4+3=7,刚好在公共节点入口相遇。

场景2:不相交(长度不等)

A:A1→A2→A3(m=3);B:B1→B2(n=2);无公共节点

指针移动路径:

pA:A1→A2→A3→null→B1→B2→null(第6步);

pB:B1→B2→null→A1→A2→A3→null(第6步);

总路程:3+2=5?实际移动6步(到null),两指针同时为null,返回null。

场景3:相交/不相交(长度相等)
  • 相交:同步移动直接在公共节点相遇;

  • 不相交:同步移动同时到null,返回null。

7.面试答题话术

"我采用双指针解法,核心逻辑是让两个指针走相同总路程m+n,抵消长度差:

  1. 初始化双指针分别指向两链表头,边界条件:任一为空则无相交;

  2. 指针不为空则后移,为空则跳转到另一链表头,循环直到两指针相等;

  3. 最终两指针要么指向相交节点,要么均为null(不相交)。

该解法时间O(m+n)、空间O(1),是最优解,且逻辑能覆盖所有场景。"

题目 2:反转链表(双指针解法)(难度:简单)

1、题目核心定义

  1. 反转定义:将链表的「节点指向关系完全逆序」(原头节点变为尾节点,原尾节点变为头节点,所有节点的next指针指向原前驱节点);
  2. 输入:无环单链表的头节点head
  3. 输出:反转后的链表头节点(原链表的尾节点)。

2、双指针解法核心逻辑(底层原理)

核心思想:用 3 个指针(前一个、当前、临时)遍历链表,逐个反转节点的指向,通过 "先保存后续节点→再反转当前指向→后移指针" 的步骤,实现线性时间内的原地反转。

两种最终场景:

  • 场景 1(链表非空):遍历完所有节点后,pre指针指向原尾节点(反转后的头节点);
  • 场景 2(链表为空 / 只有 1 个节点):直接返回原头节点(无需反转)。

3、标准模板代码(Java 版,面试直接写)

复制代码
// 1. 链表节点定义(题目已给,无需手写,了解即可)
class ListNode {
    int val;
    ListNode next;
    ListNode(int x) {
        val = x;
        next = null;
    }
}

// 2. 核心解题方法
public class Solution {
    public ListNode reverseList(ListNode head) {
        // 边界条件:空链表或只有1个节点,直接返回head
        if (head == null || head.next == null) {
            return head;
        }
        
        // 初始化3个指针:pre(前一个节点)、cur(当前节点)、temp(临时保存后续节点)
        ListNode pre = null;
        ListNode cur = head;
        ListNode temp = null;
        
        // 循环条件:cur != null(遍历所有节点,直到cur走到null)
        while (cur != null) {
            temp = cur.next;   // 1. 保存后续节点,避免断链
            cur.next = pre;    // 2. 反转当前节点的指向(指向pre)
            pre = cur;         // 3. pre后移,指向当前节点
            cur = temp;        // 4. cur后移,指向保存的后续节点
        }
        
        // 循环结束,pre是反转后的头节点(原尾节点)
        return pre;
    }
}

4、代码关键细节

  1. 循环条件:必须是cur != null (而非cur.next != null)→ 若用cur.next != null会漏掉最后一个节点的反转,导致链表断链
  2. 指针操作顺序:严格遵循保存 temp→反转 cur.next→后移 pre→后移 cur」→ 若先反转再保存 temp,会丢失后续节点,链表直接断链;
  3. 边界条件处理:必须先判断「空链表 / 只有 1 个节点」→ 避免无意义的遍历,同时覆盖特殊输入场景;
  4. 无环约束:该解法仅适用于「无环单链表」→ 若链表有环,需先处理环的逻辑,直接套用会陷入无限循环。

5、复杂度分析(面试必说)

  1. 时间复杂度:O(n)

    • n是链表的节点总数;
    • 只需遍历链表一次while循环执行n次),每个节点仅做 4 步简单赋值操作,时间复杂度为线性的O(n)
    • 最好 / 最坏情况复杂度一致(必须遍历所有节点才能完成反转)。
  2. 空间复杂度:O(1)

    • 仅使用precurtemp3 个指针变量,均为 "指向节点的引用",未新建任何ListNode实例;
    • 不管链表长度如何,空间开销固定为 3 个指针(常数级),是空间最优解(对比递归解法O(n)的栈开销)。

6、典型场景验证(快速理解)

以链表1→2→3→4→5为例,指针移动路径:

循环次数 pre cur temp 操作后链表状态
初始 null 1 null 1→2→3→4→5
1 null 1 2 1(null) ← 2→3→4→5
2 1 2 3 1←2 ← 3→4→5
3 2 3 4 1←2←3 ← 4→5
4 3 4 5 1←2←3←4 ← 5
5 4 5 null 1←2←3←4←5(null)
循环结束 5 null null 返回 pre=5(反转后头节点)

7、面试答题话术

"我采用双指针迭代法 反转链表,核心逻辑是用 3 个指针逐个反转节点指向

  1. 处理边界条件:空链表或只有 1 个节点直接返回;
  2. 初始化 pre(前节点)、cur(当前节点)、temp(临时保存后续节点),遍历链表时先保存后续节点,再反转当前指向,最后后移指针;
  3. 循环条件是 cur != null,确保遍历所有节点;
  4. 最终 pre 就是反转后的头节点。

该解法时间复杂度 O (n)、空间复杂度 O (1),是原地反转的最优解,同时覆盖所有无环单链表场景。"

题目3:回文链表(双指针找中点 + 反转法)(难度:简单)

1、题目核心定义

  1. 回文定义:链表正向遍历与反向遍历的元素序列完全一致;
  2. 输入:无环单链表的头节点 head;
  3. 输出:布尔值(true = 是回文,false = 不是回文)。

2、核心逻辑(底层原理)

核心思想:"找中点→反转后半段→比较两段→恢复原链表"

  1. 双指针找中点:slow 走 1 步、fast 走 2 步,fast 到终点时 slow 指向链表中间节点;
  2. 反转后半段:以 slow.next 为头,反转后半段链表;
  3. 比较两段:前半段(head 开始)与反转后的后半段逐一比较值;
  4. 恢复原链表:将反转的后半段再反转回去(避免修改原链表)。

两种关键场景:

  • 场景 1(链表长度为偶):slow 指向 "前半段最后一个节点",后半段长度 = 前半段;
  • 场景 2(链表长度为奇):slow 指向 "中间节点",后半段长度 = 前半段 - 1(中间节点不影响回文)。

3、标准模板代码(Java 版,面试直接写)

复制代码
// 1. 链表节点定义(题目已给,了解即可)
class ListNode {
    int val;
    ListNode next;
    ListNode(int x) {
        val = x;
        next = null;
    }
}

// 2. 核心解题方法
public class Solution {
    public boolean isPalindrome(ListNode head) {
        // 边界条件:空链表/只有1个节点,直接是回文
        if (head == null || head.next == null) {
            return true;
        }

        // 步骤1:双指针找中间节点(slow最终指向中间)
        ListNode slow = head;
        ListNode fast = head;
        // 循环条件:必须先判fast.next≠null,再判fast.next.next(避免空指针)
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next; // slow走1步
            fast = fast.next.next; // fast走2步
        }

        // 步骤2:反转后半段链表(后半段头是slow.next)
        ListNode secondHalfHead = reverseList(slow.next);

        // 步骤3:比较前半段与反转后的后半段
        ListNode p1 = head; // 前半段指针
        ListNode p2 = secondHalfHead; // 反转后半段指针
        boolean isPalin = true; // 回文标记
        // 循环条件:p2≠null即可(后半段长度≤前半段)
        while (isPalin && p2 != null) {
            if (p1.val != p2.val) {
                isPalin = false;
            }
            p1 = p1.next;
            p2 = p2.next;
        }

        // 步骤4:恢复原链表(将后半段反转回去)
        slow.next = reverseList(secondHalfHead);

        return isPalin;
    }

    // 【复用反转链表模板】双指针法反转链表
    private ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode pre = null;
        ListNode cur = head;
        ListNode temp = null;
        while (cur != null) { // 循环条件必须是cur≠null(覆盖所有节点)
            temp = cur.next; // 1. 保存后续节点
            cur.next = pre;  // 2. 反转当前指向
            pre = cur;       // 3. pre后移
            cur = temp;      // 4. cur后移
        }
        return pre; // pre是反转后的头节点
    }
}

4、代码关键细节

  1. 找中点的循环条件:必须是fast.next != null && fast.next.next != null
    • 先判fast.next再判fast.next.next,避免空指针;
    • 若用fast != null会导致 slow 多走一步,中点错误。
  2. 反转链表的循环条件:必须是cur != null,否则会漏掉最后一个节点的反转。
  3. 恢复原链表:必须将反转的后半段再反转回去(避免修改原链表的结构)。
  4. 比较的循环条件:只需判p2 != null(后半段长度≤前半段,p2 走完则比较完成)。

5、复杂度分析(面试必说)

  1. 时间复杂度:O (n)
    • 找中点:O (n/2);反转后半段:O (n/2);比较两段:O (n/2);恢复链表:O (n/2);
    • 总操作数为线性级,时间复杂度 O (n)。
  2. 空间复杂度:O (1)
    • 仅使用 slow、fast、p1、p2 等有限指针变量,未额外开辟空间,是空间最优解。

6、典型场景验证(快速理解)

以链表1→2→3→2→1为例:

  1. 找中点:slow 最终指向3,后半段是2→1
  2. 反转后半段:2→1变为1→2
  3. 比较:前半段1→2→3与反转后半段1→2,值一致;
  4. 恢复原链表:将1→2反转回2→1,原链表结构不变。

7、面试答题话术

"我采用双指针找中点 + 后半段反转的方法判断回文链表:

  1. 先处理边界条件:空链表或只有 1 个节点直接返回 true;
  2. 用 slow/fast 双指针找中点,循环条件要先判 fast.next 再判 fast.next.next 避免空指针;
  3. 反转后半段链表(复用双指针反转模板),然后比较前半段与反转后的后半段;
  4. 最后将后半段反转回原状态,保证不修改原链表。该方法时间复杂度 O (n)、空间复杂度 O (1),是空间最优的回文链表判断方法。"

题目4:环形链表(双指针)

1.核心定义

  1. 环形链表:单链表中某个节点的 next 指针指向链表中已存在的节点 (而非 null),形成闭环;
  2. 核心问题:
  • 判断链表是否存在环;
  1. 输入:无环 / 有环单链表的头节点 head
  2. 输出:基础版返回布尔值(true= 有环,false= 无环)

2、核心逻辑(底层原理)

2.1 基础版(判环):龟兔赛跑算法

核心思想:快慢指针遍历链表,有环则必相遇,无环则快指针先到终点

  • 慢指针(slow):每次走 1 步;
  • 快指针(fast):每次走 2 步;
  • 关键规则:指针比较的是内存地址(而非节点值),地址相同 = 指向同一个节点 = 有环。

3、标准模板代码

3.1 基础版(仅判环)
复制代码
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        // 边界条件:空链表/只有1个节点,无环
        if (head == null || head.next == null) {
            return false;
        }

        ListNode slow = head;
        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;
    }
}

4、代码关键细节

4.1 空指针防护核心

fast != null && fast.next != null 是双层防护:

  1. fast != null:防止 fastnull 时执行 fast.next 报空指针;
  2. fast.next != null:防止 fast.nextnull 时执行 fast.next.next 报空指针;
  • 只要满足其中一个不成立,说明快指针到终点,链表无环。
4.2 易踩坑点
  1. 初始值错误:若 slow/fast 都初始化为 head,需先移动指针再判断相遇(避免初始相等跳过循环);
  2. 比较对象错误:必须比较 slow == fast(地址),而非 slow.val == fast.val(值);

5、复杂度分析

场景 时间复杂度 空间复杂度 说明
基础版(判环) O(n) O(1) 最多遍历链表 1 次
6、典型场景验证
6.1 有环链表(1→2→3→4→2)
  • 基础版:快慢指针相遇在 3(地址相同),返回 true
6.2 无环链表(1→2→3→null)
  • 基础版:快指针走到 3 后,fast.next=null,退出循环返回 false

7、面试答题话术

基础版(判环)

"我采用快慢指针(龟兔赛跑)算法判断环:慢指针走 1 步、快指针走 2 步,若链表有环,快慢指针必相遇(地址相同);若无环,快指针会先到终点(fast/fast.nextnull)。该方法时间复杂度 O (n)、空间复杂度 O (1),是最优解。"

总结
  1. 环形链表核心:快慢指针的地址比较是判环关键,值比较无意义;
  2. 空指针防护:fast != null && fast.next != null 是遍历安全的核心;
  3. 功能区分:基础版仅判环,进阶版需记录相遇节点 + 同速指针找入口;
  4. 性能优势:快慢指针法是环形链表问题的最优解(空间 O (1)),优于哈希表法(空间 O (n))。

题目5:环形链表(寻找入环节点)

1.核心定义

  1. 核心问题:在单链表中判断是否存在环,若存在则返回环的入口节点 ,无环则返回 null
  2. 输入:单链表头节点 head(可能无环 / 有环);
  3. 输出:环的入口节点(无环返回 null);
  4. 关键规则:指针比较的是节点内存地址 (而非 val 值),地址相同 = 指向同一个节点。

2、核心逻辑(底层原理)

2.1 核心思想(一步到位版)

将 "判环" 和 "找入口" 逻辑嵌套,利用快慢指针的双重特性:

  1. 判环阶段:慢指针(slow)走 1 步、快指针(fast)走 2 步,有环则必相遇(地址相同);
  2. 找入口阶段:相遇后将慢指针重置到链表头 ,快慢指针同速走 1 步,再次相遇时即为环入口(数学推导:链表头到入口距离 = 相遇点绕环到入口距离)。
2.2 数学推导(简化版)

设:链表头到入口距离 a,入口到相遇点距离 b,环长度 L

  • 慢指针路程:a + b
  • 快指针路程:a + b + n*Ln 为绕环圈数);
  • 2*(a+b) = a+b+n*L 得:a = n*L - b → 头指针走 a 步 = 相遇点指针走 n*L - b 步(即绕环到入口)。

3、标准模板代码(简洁版)

复制代码
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 边界条件:空链表直接无环
        if (head == null) {
            return null;
        }

        ListNode slow = head;  // 慢指针(龟)
        ListNode fast = head;  // 快指针(兔)

        // 外层循环:快慢指针判环(防空指针)
        while (fast != null && fast.next != null) {
            slow = slow.next;       // 慢指针走1步
            fast = fast.next.next;  // 快指针走2步

            // 相遇=有环,进入找入口逻辑
            if (slow == fast) {
                slow = head;        // 慢指针重置到链表头
                // 嵌套循环:同速走,相遇即入口
                while (slow != fast) {
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow; // 返回环入口节点
            }
        }

        // 退出外层循环=无环,返回null
        return null;
    }
}

4、代码关键细节

4.1 空指针防护

fast != null && fast.next != null 是双层防护:

  1. fast != null:防止 fastnull 时执行 fast.next 报空指针;
  2. fast.next != null:防止 fast.nextnull 时执行 fast.next.next 报空指针;
  • 只要满足一个不成立,说明快指针到终点,链表无环。
4.2 核心优化点(简洁版专属)
  1. 无中间变量:省去 meetNode,相遇后直接复用 slow/fast 指针,代码更精简;
  2. 逻辑嵌套:判环和找入口合并在一个循环中,无冗余分支;
  3. 指针重置:相遇后必须将 slow 重置为 head,且嵌套循环中快慢指针均走 1 步(核心规则)。
4.3 易踩坑点
  1. 初始值:slow/fast 均初始化为 head,需先移动指针再判断相遇(避免初始相等跳过循环);
  2. 比较对象:必须用 slow == fast(地址),而非 slow.val == fast.val(值);
  3. 边界条件:head == null 即可覆盖空链表场景,无需额外判断 head.next == null(外层循环已处理)。

5、复杂度分析4

维度 复杂度 说明
时间复杂度 O(n) 判环 + 找入口总遍历≤2n 次
空间复杂度 O(1) 仅使用 2 个指针,无额外空间
6、典型场景验证
6.1 有环链表(1→2→3→4→2)
  • 判环阶段:快慢指针相遇在 4(地址相同);
  • 找入口阶段:slow 重置为 1,同速走后相遇在 2(环入口),返回节点 2。
6.2 无环链表(1→2→3→null)
  • 外层循环:快指针走到 3 后,fast.next=null,退出循环返回 null

7、面试答题话术

"我采用快慢指针的紧凑版写法解决环形链表找入口问题:

  1. 先通过快慢指针(慢 1 步、快 2 步)判环,若相遇则说明有环;
  2. 相遇后将慢指针重置到链表头,快慢指针同速走 1 步,再次相遇的节点即为环入口;
  3. 该方法通过数学推导保证逻辑正确性,时间复杂度 O (n)、空间复杂度 O (1),是最优解,且代码精简无冗余。"
总结
  1. 简洁版核心优势:代码短、无中间变量、逻辑紧凑,是面试最优写法;
  2. 核心规则不变:判环靠快慢指针相遇(地址比较),找入口靠 "头指针 + 相遇点指针同速走";
  3. 关键细节:空指针防护、指针重置、同速走 1 步,是避免错误的核心;
  4. 性能等价:与拆分版相比,执行效率和复杂度完全一致,仅代码结构更精简。
相关推荐
難釋懷1 小时前
Redis数据结构介绍
数据结构·数据库·redis
xlp666hub2 小时前
Linux 设备模型学习笔记(2)之 kobject
linux·面试
码农胖虎-java2 小时前
【高频面试题】MySQL高频面试&实战:慢查询排查+索引底层(B+树/联合索引)全解析
b树·mysql·面试
千寻girling2 小时前
面试官 : “ 说一下 ES6 模块与 CommonJS 模块的差异 ? ”
前端·javascript·面试
indexsunny2 小时前
互联网大厂Java面试实战:核心技术与微服务架构解析
java·数据库·spring boot·缓存·微服务·面试·消息队列
Pluchon2 小时前
硅基计划4.0 算法 优先级队列
数据结构·算法·排序算法
程序员清风2 小时前
贝壳一面:Spring是怎么实现的?谈谈你的理解?
java·后端·面试
清 澜2 小时前
大模型扫盲式面试知识复习 (一)
人工智能·面试·大模型
上海物联网2 小时前
Prism Regions-自定义区域适配器实现开发者将任意 WPF 控件转换为可动态加载视图的区域容器
面试·wpf