diff --git a/链表/25-环形链表.md b/链表/25-环形链表.md
new file mode 100644
index 0000000..3e67a81
--- /dev/null
+++ b/链表/25-环形链表.md
@@ -0,0 +1,386 @@
+---
+tags: ["LeetCode", "链表", "双指针", "哈希表", "Floyd判圈", "简单"]
+create time: 2026-05-16 15:00
+---
+
+# 25-环形链表
+
+## 题面
+
+给你一个链表的头节点 `head`,判断链表中是否有环。
+
+如果链表中有某个节点,可以通过连续跟踪 `next` 指针再次到达,则链表中存在环。
+
+为了表示给定链表中的环,评测系统内部使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 0 开始)。**注意:pos 不作为参数进行传递。**仅仅是为了标识链表的实际情况。
+
+如果链表中存在环,则返回 `true`。否则,返回 `false`。
+
+**示例 1:**
+
+```
+输入:head = [3,2,0,-4], pos = 1
+输出:true
+解释:链表中有一个环,其尾部连接到第二个节点。
+```
+
+**示例 2:**
+
+```
+输入:head = [1,2], pos = 0
+输出:true
+解释:链表中有一个环,其尾部连接到第一个节点。
+```
+
+**示例 3:**
+
+```
+输入:head = [1], pos = -1
+输出:false
+解释:链表中没有环。
+```
+
+**提示:**
+
+- 链表中节点的数目范围是 `[0, 10^4]`
+- `-10^5 <= Node.val <= 10^5`
+- `pos` 为 `-1` 或者链表中的一个有效索引
+
+**进阶:你能用 O(1)(即,常量)内存解决此问题吗?**
+
+---
+
+## 思路
+
+> [!question] 💡 核心直觉
+> 想象两个人在同一条跑道上赛跑——一个人跑得快,另一个人跑得慢。如果他们都在同一条**封闭跑道**上,快的迟早会从后面追上慢的。但如果是在**直线跑道**上,快的最终会冲到前面再也不回头。
+>
+> **把这个想法映射到链表上:** 如果有环,快慢两个指针就会像跑道上的运动员一样"相遇";如果没有环,快的会先走到终点 `nil`。
+
+> [!info] 🧩 两种基本思路
+
+| 方法 | 时间复杂度 | 空间复杂度 | 能否满足进阶要求? |
+|------|-----------|-----------|-----------------|
+| 哈希集合记录已访问节点 | O(n) | O(n) | ❌ |
+| **快慢指针(Floyd 判圈算法)** | **O(n)** | **O(1)** | ✅ |
+
+---
+
+### 方法一:哈希集合(直觉方案)
+
+维护一个已经遍历过的节点的集合。每到一个新节点就检查是否已经在集合中出现过:出现过说明有环;没出现过就加入集合并继续前进;走到 `nil` 说明无环。
+
+```mermaid
+flowchart TD
+ START(["从头节点出发"]) --> CHECK{"节点已在集合中?"}
+ CHECK -->|是| HASLOOP["🔄 发现环 → 返回 true"]
+ CHECK -->|否| ADD["加入集合"]
+ ADD --> NILCHECK{"节点是否为 nil?"}
+ NILCHECK -->|否| NEXT["移动到下一个节点"]
+ NEXT --> CHECK
+ NILCHECK -->|是| NOLOOP["✅ 走到末尾 → 返回 false"]
+
+ style HASLOOP fill:#f99,stroke:#333
+ style NOLOOP fill:#4c4,stroke:#333
+```
+
+**步骤拆解:**
+
+| 步骤 | 动作 | 说明 |
+|------|------|------|
+| 1 | 初始化空哈希集合 | 存储已访问的节点引用(地址) |
+| 2 | 遍历链表,对每个节点查集合 | 若存在则有环 |
+| 3 | 不存在则加入集合,继续走 | 直到节点为 `nil` |
+| 4 | 走到 `nil` 则无环 | 返回 `false` |
+
+**时间复杂度:O(n)** — 每个节点最多访问一次,哈希集合的插入和查找都是 O(1)。
+**空间复杂度:O(n)** — 最坏情况下需要存储所有 n 个节点的引用。
+
+> [!note] 🤔 这个方法的问题是什么?
+> 完全能正确检测环,但空间复杂度为 O(n),不满足进阶的 O(1) 空间要求。而且它需要额外的数据结构开销。有没有可能只靠指针本身来完成检测?
+
+---
+
+### 方法二:快慢指针 / Floyd 判圈算法 ⭐(最优,O(1) 空间)⭐
+
+> [!warning] ⚠️ 核心类比
+> 这是本道题的灵魂思想,建议反复体会:
+>
+> - **直线跑道(无环):** 快指针每次走两步,慢指针每次走一步 → 快指针先到终点 `nil`
+> - **环形跑道(有环):** 快指针进入环后绕圈,慢指针也在环里慢慢走 → 快指针从后面**追上**慢指针
+
+#### 算法原理
+
+设定两个指针:`slow` 每次走一步,`fast` 每次走两步。让它们在链表中同时前进。
+
+**为什么一定能在环内相遇?**
+
+> [!question] 💡 数学直觉
+> 当两个指针都进入环之后,它们之间的距离变化是怎样的?
+
+假设某一时刻两指针都在环内,相距 k 步(沿着移动方向计量)。每一步操作中:
+- 慢指针向前走 1 步
+- 快指针向前走 2 步
+- 两者距离缩短 1 步
+
+```
+初始状态: S · · · · F (相距 k=4)
+第1步后: S · · · F · (相距 k=3)
+第2步后: S · · F · · (相距 k=2)
+第3步后: S · F · · · (相距 k=1)
+第4步后: S F · · · · (相遇!k=0)
+```
+
+因此,**只要两指针都在环内,最多经过 k 步(k 为入环后的距离),快指针就能追上慢指针。**
+
+> [!danger] ⛔ 常见误区澄清
+> **快指针不会"跳过"慢指针!** 有人担心快指针一步跨过慢指针所在的位置而永远不会相等。但实际上在离散的一步移动中,快指针只能比慢指针多走 1 步,所以它会恰好落在慢指针所在的那个节点上,不可能跳过。
+
+#### 边界情况处理
+
+```mermaid
+flowchart LR
+ subgraph "空链表"
+ A1["head == nil"] --> R1["直接返回 false ✓"]
+ end
+
+ subgraph "单节点、无环"
+ A2["head.Next == nil"] --> R2["fast=head.Next=nil\n不进入循环\n返回 false ✓"]
+ end
+
+ subgraph "两节点、有环(连回自身)"
+ A3["1→2→1→..."] --> R3["一轮相遇!\nslow=2, fast=2 ✓"]
+ end
+
+ subgraph "普通有环"
+ A4["长链表+环"] --> R4["快追慢 → 环内某处相遇 ✓"]
+ end
+
+ style R1 fill:#4c4,stroke:#333
+ style R2 fill:#4c4,stroke:#333
+ style R3 fill:#4c4,stroke:#333
+ style R4 fill:#4c4,stroke:#333
+```
+
+**关键细节:** 循环条件为 `fast != nil && fast.Next != nil`,确保 `fast.Next.Next` 安全。
+
+> [!info] 🧮 为什么 fast 要检查 fast.Next != nil?
+> 因为 `fast` 每次要走两步(`fast = fast.Next.Next`)。如果只检查 `fast != nil` 而不检查 `fast.Next != nil`,当 `fast` 指向最后一个节点时,`fast.Next` 为 `nil`,`fast.Next.Next` 就会 panic。
+
+#### 完整流程图解
+
+以 `3→2→0→-4→(回到索引1的节点2)` 为例:
+
+```
+初始: slow=3(head), fast=3(head)
+
+第1轮: slow→2, fast→0 (slow 走1步, fast 走2步)
+第2轮: slow→0, fast→-4 (slow 走1步, fast 走2步)
+第3轮: slow→-4, fast→2 (slow 走1步, fast 走2步, fast 进入环)
+第4轮: slow→2, fast→2 (SLOW == FAST → 有环!✓)
+```
+
+```mermaid
+flowchart TD
+ START(["head = 3→2→0→-4(连回索引1)"]) --> INIT["slow=3, fast=3"]
+
+ INIT --> LOOP{"fast!=nil && fast.Next!=nil"}
+ LOOP -->|是| STEP1["slow→下一步"]
+ STEP1 --> STEP2["fast→下两步"]
+ STEP2 --> MEET{"slow==fast?"}
+ MEET -->|是| HASLOOP["🔄 有环 → true ✓"]
+ MEET -->|否| LOOP
+ LOOP -->|否| NOLOOP["✅ 无环 → false ✗"]
+
+ style HASLOOP fill:#f99,stroke:#333
+ style NOLOOP fill:#4c4,stroke:#333
+```
+
+**时间复杂度:O(n)** — 如果存在环,慢指针最多走环外部分 + 一圈环内,快指针最多走环外部分 + 两圈环内。总步数不超过 2n。
+**空间复杂度:O(1)** — 只用了 slow、fast 两个指针变量,完美满足进阶要求!
+
+> [!success] 🎯 Floyd 判圈算法的历史
+> 这个算法由美国科学家 Robert W. Floyd 于 1967 年提出,是图论中经典的环路检测方法。它的精妙之处在于**不需要任何额外存储空间**,仅通过指针的速度差来探测拓扑结构,被誉为计算机科学中最优雅的算法之一。
+
+---
+
+## 代码提示
+
+### 伪代码模板
+
+```
+// ① 边界检查:空链表直接无环
+if head == nil {
+ return false
+}
+
+// ② 初始化双指针
+slow := head // 每次走一步
+fast := head // 每次走两步
+
+// ③ 同步推进 —— 快指针能走两步时才继续
+for fast != nil && fast.Next != nil {
+ slow = slow.Next // 慢指针前进一步
+ fast = fast.Next.Next // 快指针前进两步
+
+ if slow == fast { // 在环内相遇了!
+ return true
+ }
+}
+
+// ④ 快指针触底,无路可走 → 无环
+return false
+```
+
+**Go 语言技巧:**
+
+- Go 支持匿名多赋值,可以用一行完成两个指针的移动,但分写更清晰易读
+- 利用 Go 的 `&&` 短路特性:`fast != nil && fast.Next != nil` 保证安全
+- Go 的指针语义天然适合此算法——比较的是节点引用(地址),而非值
+
+---
+
+## 技巧
+
+> [!tip] 🔑 核心模式:Floyd 判圈算法(Tortoise and Hare)
+> 双指针速度差法 —— 慢指针一步、快指针两步。这是检测链表中是否存在环的标准 O(1) 空间解法。
+>
+> **记忆口诀:"一快一慢,有环必遇;快碰底线,无环无疑。"**
+
+> [!tip] 🔑 延伸变体:如果有环,如何找到入口节点?
+> 这是一个非常有趣的扩展(对应 LeetCode 142. 环形链表 II)。证明如下:
+>
+> 设:
+> - x = 从 head 到环入口的距离
+> - y = 从环入口到相遇点的距离
+> - r = 环的周长
+> - L = 相遇时慢指针走过的总步数 = x + y
+>
+> 此时快指针走了 2L = x + y + n×r(n 为快指针在环内多走的圈数)。
+> 化简得:**x + y = n×r**,即 **x = n×r - y**。
+>
+> **这意味着:从相遇点再走 (n-1) 圈 + 从 head 走 x 步,等于从环入口走 (n-1) 圈再走 y 步。**
+>
+> 因此只需在第一次相遇后,把其中一个指针重新放回首部,两者各走一步,第二次相遇点就是**环的入口节点**。
+>
+> ```go
+> // 第一次相遇后的入口查找
+> ptr1 := head
+> ptr2 := slow // 或 fast,两者此刻相等
+> for ptr1 != ptr2 {
+> ptr1 = ptr1.Next
+> ptr2 = ptr2.Next
+> }
+> return ptr1 // 环的入口
+> ```
+
+> [!note] 🐹 Go 中的链表定义
+> LeetCode 的 Go 环境内置如下结构体定义:
+
+```go
+type ListNode struct {
+ Val int
+ Next *ListNode
+}
+```
+
+不需要手动定义,直接在解题中使用即可。
+
+> [!info] 📊 两种方法对比
+
+| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
+|------|-----------|-----------|------|------|
+| 哈希集合 | O(n) | O(n) | 思路直观,容易想到 | 占用额外空间 |
+| **Floyd 判圈 ⭐** | **O(n)** | **O(1)** | **最优,面试标配** | 逻辑稍有门槛 |
+
+> [!success] ✅ 相关题目串联
+> - [[26-环形链表 II]] — 进阶版:找环的入口(同一组快慢指针技术,加一步推导)
+> - [22-相交链表](./22-相交链表.md) — 同样是双指针的经典应用
+> - [24-回文链表](./24-回文链表.md) — 也用到了快慢指针找中点的技巧
+
+> [!warning] ⚠️ 面试注意事项
+> - Go 中不用显式释放资源,垃圾回收会自动处理,**不需要也不应该手动置 nil 断链**
+> - 快慢指针初始化都应该从 `head` 开始,不要错误地把 fast 初始化为 `head.Next`(那是回文链表的写法,不适用于本题)
+> - 空链表 `head == nil` 的情况需要考虑,虽然 LeetCode 测试用例通常不包含,但良好的习惯应该加上
+
+---
+
+## 代码
+
+### 方法一:哈希集合(O(n) 空间)
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func hasCycle(head *ListNode) bool {
+ seen := make(map[*ListNode]bool)
+ for node := head; node != nil; node = node.Next {
+ if seen[node] {
+ return true // 该节点已出现过 → 有环
+ }
+ seen[node] = true
+ }
+ return false // 走到末尾也没重复 → 无环
+}
+```
+
+---
+
+### 方法二:Floyd 判圈算法 ⭐(最优 O(1) 空间)⭐
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func hasCycle(head *ListNode) bool {
+ // 空链表不可能有环
+ if head == nil {
+ return false
+ }
+
+ slow := head // 慢指针:每次走一步
+ fast := head // 快指针:每次走两步
+
+ // 快指针能连续走两步才继续推进
+ for fast != nil && fast.Next != nil {
+ slow = slow.Next // 慢指针前进一步
+ fast = fast.Next.Next // 快指针前进两步
+
+ if slow == fast { // 快慢指针相遇 → 有环
+ return true
+ }
+ }
+
+ // 快指针触底 → 无环
+ return false
+}
+```
+
+> [!tip] 🔑 精简写法(竞赛向)
+> 利用 Go 的 `for` 不带初始化语句的特性,可以让代码更紧凑:
+
+```go
+func hasCycle(head *ListNode) bool {
+ for slow, fast := head, head; fast != nil && fast.Next != nil; {
+ slow = slow.Next
+ fast = fast.Next.Next
+ if slow == fast {
+ return true
+ }
+ }
+ return false
+}
+```
+
+> [!success] ✅ 运行验证
+> 这是 LeetCode 第 141 题,通过率约 48%。看似简单的一道题,但其背后的 Floyd 判圈算法却是图论中一个经典而优雅的技巧——仅凭两个指针的速度差异就能判断整个图的拓扑结构。掌握这个模式,不仅能秒杀环形链表系列题目,也为后续学习图论中的环路检测打下基础。
diff --git a/链表/26-环形链表 II.md b/链表/26-环形链表 II.md
new file mode 100644
index 0000000..d447800
--- /dev/null
+++ b/链表/26-环形链表 II.md
@@ -0,0 +1,440 @@
+---
+tags: ["LeetCode", "链表", "双指针", "Floyd判圈", "环形链表"]
+create time: 2026-05-16 15:30
+---
+
+# 26-环形链表 II
+
+## 题面
+
+给定一个链表的头节点 `head`,返回链表开始入环的第一个节点。**如果链表无环,则返回 `nil`。**
+
+如果链表中有某个节点,可以通过连续跟踪 `next` 指针再次到达,则链表中存在环。
+
+为了表示给定链表中的环,评测系统内部使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 0 开始)。**注意:pos 不作为参数进行传递。**仅仅是为了标识链表的实际情况。
+
+**不允许修改链表。**
+
+**示例 1:**
+
+```
+输入:head = [3,2,0,-4], pos = 1
+输出:返回索引为 1 的链表节点
+解释:链表中有一个环,其尾部连接到第二个节点。
+```
+
+**示例 2:**
+
+```
+输入:head = [1,2], pos = 0
+输出:返回索引为 0 的链表节点
+解释:链表中有一个环,其尾部连接到第一个节点。
+```
+
+**示例 3:**
+
+```
+输入:head = [1], pos = -1
+输出:返回 null
+解释:链表中没有环。
+```
+
+**提示:**
+
+- 链表中节点的数目范围在 `[0, 10^4]` 内
+- `-10^5 <= Node.val <= 10^5`
+- `pos` 的值为 `-1` 或者链表中的一个有效索引
+
+**进阶:你是否可以使用 O(1) 空间解决此题?**
+
+---
+
+## 思路
+
+> [!question] 💡 承上启下
+> 你已经掌握了 [25-环形链表](./25-环形链表.md)——用 Floyd 快慢指针判断环的存在性。现在问题是:**如果已经有环,如何找到入口节点?**
+
+> [!tip] 🔑 结论先行
+> 仍然使用快慢指针。第一次相遇后,把其中一个指针放回 `head`,两个指针改为**同速各走一步**,它们第二次相遇的位置就是环的入口。
+> **时间复杂度 O(n),空间复杂度 O(1)**。
+
+### 前置回顾:第一轮——检测环
+
+```mermaid
+flowchart LR
+ subgraph S ["慢指针 slow(每次 1 步)"]
+ S1["从头出发"]
+ end
+ subgraph F ["快指针 fast(每次 2 步)"]
+ F1["从头出发"]
+ end
+
+ S1 --> PULL{"两指针都进环后,快追慢"}
+ F1 --> PULL
+ PULL -->|每步缩小 1
最多 r 步| MEET["🔄 环内相遇点 M"]
+
+ style MEET fill:#f99,stroke:#333
+```
+
+此时我们已经确认有环,但相遇点 M 通常**不是**入口 E。我们需要推导一个关系式来定位 E。
+
+### 核心推导:第二轮——寻找入口
+
+> [!question] 💡 设未知数
+> 用三个变量描述链表的拓扑结构,你能建立它们之间的关系吗?
+
+定义三个关键距离(沿链表前进方向计量):
+
+| 符号 | 含义 |
+|------|------|
+| **x** | 从 `head` 到环入口的距离(入口前非环部分的长度) |
+| **y** | 从环入口到相遇点的距离 |
+| **r** | 环的周长 |
+
+**慢指针走过的总路程**(设为 L):
+
+$$L = x + y$$
+
+> [!info] ℹ️ 为什么是 x + y?
+> 慢指针速度为 1,先进入环,然后沿着环走到相遇点。它在进入环之后最多走不满一圈就遇到了快指针(因为快指针在后面追),所以路径就是"头→入口" + "入口→相遇点" = x + y。
+
+**快指针走过的总路程**(设为 2L):
+
+$$2L = x + y + n \cdot r \quad (n \geq 1)$$
+
+> [!info] 🧮 理解 n · r
+> 快指针速度是慢指针的两倍,所以它除了走完和慢指针一样的 x + y 之外,还在环里多跑了若干整圈(n 圈)。由于快指针必须至少比慢指针多跑一圈才能追上它,所以 n ≥ 1。
+
+将 L = x + y 代入消元:
+
+$$2(x + y) = x + y + n \cdot r$$
+
+化简得:
+
+$$x + y = n \cdot r$$
+
+移项得到关键公式:
+
+$$\boxed{x = n \cdot r - y}$$
+
+> [!warning] ⛔ 常见误区
+> 这里 n 可以是 1、2、3……不一定等于 1。但无论如何,**这个等式的几何意义不变**:
+
+### 公式解读:从两个方向走相同距离
+
+$$x = n \cdot r - y = (n - 1) \cdot r + (r - y)$$
+
+这意味着什么?让我们从两个视角看:
+
+**视角 1 — 从 head 出发走 x 步**:直达环入口 E。
+
+**视角 2 — 从相遇点 M 出发走同样的距离**:
+
+```
+M → E 的距离拆解:
+ 先走完环的剩余部分 M→E:距离 = r - y
+ 再绕 (n-1) 整圈回到 E:距离 = (n-1) · r
+
+ 合计 = (n-1) · r + (r - y) = x ✅
+```
+
+> [!success] 🎯 关键洞察
+> **从 head 走 x 步到达入口 E;从相遇点 M 也走 x 步同样到达入口 E。**
+> 两者步数相同!所以我们只需要启动两个新指针——一个从 head 开始,一个从相遇点 M 开始——以相同速度同步前进,它们的第二次相遇点必然是环入口 E。
+
+```mermaid
+flowchart TD
+ HEAD["head"] --"第1步"--> A["第1个节点"]
+ A --"第2步"--> B["第2个节点"]
+ B --"第x步"--> ENTRY["⭐ 环入口 E ← 两指针在此相遇"]
+
+ ENTRY -.-> RING["环中各节点..."]
+ RING -.-> ENTRY
+
+ MEET["相遇点 M"] --"走同样步数"--> ENTRY
+
+ style HEAD fill:#bbf,stroke:#333
+ style MEET fill:#fbf,stroke:#333
+ style ENTRY fill:#fd4,stroke:#333,stroke-width:3px
+```
+
+**示例完整演示**:以 `3→2→0→-4→(回索引1)` 为例
+
+| 阶段 | 参数 | 值 |
+|------|------|-----|
+| 非环段 | x | 1(head 直接就是入口) |
+| 环入口到相遇点 | y | 3(入口 2 → 0 → -4 → 2) |
+| 环周长 | r | 4(2→0→-4→2) |
+| 验证 | x + y = n × r | 1 + 3 = 4 = 1 × 4 ✓ |
+| 入口公式 | x = n×r - y | 1 = 4 - 3 ✓ |
+
+两轮过程:
+
+**第一轮(快慢指针检测):**
+
+```
+初始: slow=3(head), fast=3(head)
+
+第1轮: slow→2(E), fast→0 (slow 走1步, fast 走2步)
+第2轮: slow→0, fast=-4 (slow 走1步, fast 走2步)
+第3轮: slow→-4, fast→2(E) (slow 走1步, fast 走2步)
+第4轮: slow→2(E), fast→2(E) ← SLOW == FAST,相遇于 E ✗(巧合!)
+
+⚠️ 本例恰好相遇于入口,但不代表总是如此。
+ 换一组数据就能看出差异。
+```
+
+> [!note] 🤔 为什么本例的特殊情况会让人误解?
+> 因为 x = 1(head 就是入口),慢指针只走了 4 步(刚好一圈)就和快指针相遇了,相遇点恰好是入口。让我们构造一个更一般的例子来观察:
+>
+> 假设链表 `1→2→3→4→5→(回2)`:
+> - x = 1(head[1]→入口[2])
+> - 慢指针走 1+1=2 步到节点 3
+> - 快指针走 4 步:1→3→5→3→? 最终也会在节点 3 相遇
+> - 但从 head 走 x=1 步到 2,从相遇点 3 走 x=1 步到 4 ≠ 2
+>
+> 等等,让我重新计算:
+>
+> 慢指针 L = x + y = 1 + y,快指针 2L = 1 + y + n·r(r=4)
+> 2(1+y) = 1+y+4n → 1+y = 4n → y = 4n-1
+> n=1 时 y=3,即相遇点在入口往后数第3个节点 = 节点5
+>
+> 验证:慢指针走 1+3=4 步到节点5,快指针走8步也到节点5。✓
+>
+> **第二轮:** ptr1 从 head(1) 走 x=1 步到节点2(入口);ptr2 从相遇点(5) 走 1 步到节点2(5→2,因为是环)。二者在节点2相遇!完美。
+
+### 整体算法流程
+
+```mermaid
+flowchart TD
+ START(["输入 head"]) --> BOUNDARY{"head == nil?"}
+ BOUNDARY -->|是| NOLOOP["返回 nil"]
+ BOUNDARY -->|否| ROUND1["⏩ 第一轮:快慢指针
slow 每步1次, fast 每步2次"]
+
+ ROUND1 --> HASMEET{"是否相遇?"}
+ HASMEET -->|否| NOLOOP2["fast 触底 nil
→ 无环,返回 nil"]
+ HASMEET -->|是| MEETPT["记录相遇点 meetNode"]
+
+ MEETPT --> ROUND2["⏩ 第二轮:双指针同速
ptr1 = head, ptr2 = meetNode"]
+ ROUND2 --> SYNCLOOP{"ptr1 != ptr2?"}
+ SYNCLOOP -->|是| MOVE["ptr1++, ptr2++"]
+ MOVE --> SYNCLOOP
+ SYNCLOOP -->|否| ENTRY["🎯 ptr1/ptr2 就是环入口!"]
+
+ style NOLOOP fill:#ccc,stroke:#333
+ style NOLOOP2 fill:#ccc,stroke:#333
+ style ENTRY fill:#fd4,stroke:#333,stroke-width:3px
+```
+
+| 轮次 | 操作 | 时间复杂度 | 说明 |
+|------|------|-----------|------|
+| 第一轮 | 快指针每次 2 步,慢指针每次 1 步 | O(n) | 找到是否有环及相遇点 |
+| 第二轮 | 两指针各 1 步,从 head 和相遇点出发 | O(n) | 找到环入口 |
+| **合计** | — | **O(n)** | **空间 O(1)** |
+
+> [!info] 📐 正确性证明总结
+
+| 步骤 | 命题 | 依据 |
+|------|------|------|
+| 1 | 若有环,快慢指针必在环内相遇 | 每步距离缩短 1,最多 r 步 |
+| 2 | 相遇时满足 x + y = n × r | 快指针速度是慢指针的两倍 |
+| 3 | 从 head 走 x 步与从相遇点走 x 步汇于同一节点 | x = (n-1)r + (r-y) |
+| 4 | 该节点即为环入口 | 从 head 往前走 x 步正好到入口 |
+
+---
+
+## 代码提示
+
+```go
+// ======== 第一轮:检测环,找相遇点 ========
+slow, fast := head, head
+for fast != nil && fast.Next != nil {
+ slow = slow.Next // 每次 1 步
+ fast = fast.Next.Next // 每次 2 步
+ if slow == fast { // 相遇!进入第二轮
+ break
+ }
+}
+
+// 没相遇 → 无环
+if fast == nil || fast.Next == nil {
+ return nil
+}
+
+// ======== 第二轮:找入口 ========
+ptr1 := head // 从头部出发
+ptr2 := slow // 从相遇点出发
+for ptr1 != ptr2 {
+ ptr1 = ptr1.Next
+ ptr2 = ptr2.Next
+}
+return ptr1 // 或 ptr2 —— 就是环入口
+```
+
+**Go 语言技巧:**
+
+- Go 不支持 Python 风格的多赋值同时推进不同目标,需要分开写
+- 利用 Go 的 `nil` 语义安全地检查边界条件
+- 可以将第一轮的 `break` 后的检查合并为一行:`if slow == fast { ... } else { return nil }`
+
+> [!tip] 🔑 Go 一行流写法(竞赛向)
+> 利用 Go 的 for 语句特点,可以将两段逻辑合并:
+
+```go
+func detectCycle(head *ListNode) *ListNode {
+ for slow, fast := head, head; fast != nil && fast.Next != nil; {
+ slow = slow.Next
+ fast = fast.Next.Next
+ if slow == fast { // 相遇后立即切换模式
+ for head != slow { // ptr1=head, ptr2=slow 同速走
+ head = head.Next
+ slow = slow.Next
+ }
+ return head
+ }
+ }
+ return nil // fast 触底 → 无环
+}
+```
+
+---
+
+## 技巧
+
+> [!tip] 🔑 核心模式:Floyd 环检测 + 对称映射
+> 这道题的本质是利用代数推导出的**距离对称性**:从 head 到入口的距离 = 从相遇点到入口的距离。这可以看作一种"镜像翻转"——把线性前缀映射到了环形剩余路径上。
+>
+> **记忆口诀:"一快一慢有环必遇,一回一遇入口必现。"**
+
+> [!question] 🧠 深入思考:如果快指针每次走 3 步呢?
+> 快指针走 k 步(k > 2),慢指针走 1 步,那么每步距离缩短 k-1。
+> 相遇时有:(k-1)(x+y) = n·r。当 k=2 时,(x+y)=n·r,推导出 x = nr-y——简洁优美。
+> 当 k=3 时,2(x+y)=n·r,推导出 x = nr/2 - y——可能不是整数,且无法直接使用"同速两步对走"的简单策略。
+> 这也是为什么 Floyd 选 k=2(快2慢1)是最优设计的原因。
+
+> [!danger] ⚠️ 常见错误
+>
+> 1. **忘记处理无环的情况**:第一轮结束后需要检查是否真的相遇了(`fast == nil || fast.Next == nil`),否则会把 `nil` 当成相遇点进入死循环。
+> 2. **第二轮只用一个指针移动**:必须两个指针各走一步。
+> 3. **把 `ptr1` 初始化成 `slow.Next`**:应该都是严格的一步一步走,`ptr1 = head`, `ptr2 = slow`。
+
+> [!note] 🐹 Go 中的链表定义
+> LeetCode 的 Go 环境内置如下结构体定义:
+
+```go
+type ListNode struct {
+ Val int
+ Next *ListNode
+}
+```
+
+不需要手动定义,直接在解题中使用即可。
+
+> [!info] 📊 两种方法对比
+
+| 方法 | 时间复杂度 | 空间复杂度 | 备注 |
+|------|-----------|-----------|------|
+| 哈希集合记录访问过的节点 | O(n) | O(n) | 遍历每个节点,查 set |
+| **Floyd 双指针 ⭐** | **O(n)** | **O(1)** | **最优,面试标配** |
+
+> [!success] ✅ 相关题目串联
+> - [25-环形链表](./25-环形链表.md) — 基础版:判断环的存在性
+> - [22-相交链表](./22-相交链表.md) — 同样是双指针的经典应用
+> - 剑指 Offer 23 — 环形链表的入口(完全相同的题目)
+
+> [!warning] ⚠️ 面试注意事项
+> - 面试时建议先推导一遍 x = n·r - y 的过程,展示你的分析能力
+> - 面试官可能追问"如果没有环怎么办"——务必加上边界检查
+> - 有些面试官会要求画图解法,提前准备好图示思维
+> - 如果面试官要求你构建测试用例,至少准备:空链表、单节点有环/无环、head 就是入口、相遇点恰好在入口
+
+---
+
+## 代码
+
+### 方法一:哈希集合(O(n) 空间,不推荐)
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func detectCycle(head *ListNode) *ListNode {
+ seen := make(map[*ListNode]bool)
+ for node := head; node != nil; node = node.Next {
+ if seen[node] {
+ return node // 该节点已出现过 → 这是环入口
+ }
+ seen[node] = true
+ }
+ return nil // 走到末尾也没重复 → 无环
+}
+```
+
+---
+
+### 方法二:Floyd 环检测 + 对称映射 ⭐(最优 O(1) 空间)
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func detectCycle(head *ListNode) *ListNode {
+ // ======== 第一轮:快慢指针检测环 ========
+ slow, fast := head, head
+ for fast != nil && fast.Next != nil {
+ slow = slow.Next // 慢指针:每次走一步
+ fast = fast.Next.Next // 快指针:每次走两步
+
+ if slow == fast { // 在环内相遇了!
+ break // 跳出循环,进入第二轮
+ }
+ }
+
+ // 没相遇 → fast 触底 → 无环
+ if fast == nil || fast.Next == nil {
+ return nil
+ }
+
+ // ======== 第二轮:找环入口 ========
+ ptr1 := head // 指针1:从头节点出发
+ ptr2 := slow // 指针2:从相遇点出发
+ for ptr1 != ptr2 {
+ ptr1 = ptr1.Next
+ ptr2 = ptr2.Next
+ }
+
+ return ptr1 // ptr1 == ptr2,即为环入口节点
+}
+```
+
+> [!tip] 🔑 精简写法(竞赛向)
+
+```go
+func detectCycle(head *ListNode) *ListNode {
+ for slow, fast := head, head; fast != nil && fast.Next != nil; {
+ slow = slow.Next
+ fast = fast.Next.Next
+ if slow == fast { // 相遇后切换模式
+ for head != slow {
+ head = head.Next
+ slow = slow.Next
+ }
+ return head
+ }
+ }
+ return nil
+}
+```
+
+> [!success] ✅ 运行验证
+> 这是 LeetCode 第 142 题,通过率约 46%,属于经典中等题。这道题的精妙之处在于**纯数学推导驱动算法设计**——没有复杂的技巧,仅靠一个简单的等式 x = n·r - y 就完成了从"检测环"到"定位入口"的跨越。建议在纸上画图推导一遍这个公式,理解后会感受到数学之美。
diff --git a/链表/27-合并两个有序链表.md b/链表/27-合并两个有序链表.md
new file mode 100644
index 0000000..115ee13
--- /dev/null
+++ b/链表/27-合并两个有序链表.md
@@ -0,0 +1,360 @@
+---
+tags: ["LeetCode", "链表", "双指针", "递归", "简单"]
+create time: 2026-05-16 14:40
+---
+
+# 27-合并两个有序链表
+
+## 题面
+
+将两个 **升序** 链表合并为一个新的 **升序** 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
+
+**示例 1:**
+
+```
+输入:l1 = [1,2,4], l2 = [1,3,4]
+输出:[1,1,2,3,4,4]
+```
+
+**示例 2:**
+
+```
+输入:l1 = [], l2 = []
+输出:[]
+```
+
+**示例 3:**
+
+```
+输入:l1 = [], l2 = [0]
+输出:[0]
+```
+
+**提示:**
+
+- 两个链表的节点数目范围是 `[0, 50]`
+- `-100 <= Node.val <= 100`
+- `l1` 和 `l2` 均按 **非递减顺序** 排列
+
+---
+
+## 思路
+
+> [!question] 💡 引导思考
+> 假设你面前有两张已经排好序的扑克牌,你会如何把它们合并成一张更大的有序牌堆?——每次比较两张牌的**最上面**那一张,取较小的放入结果中。这就是本题的核心思想。
+
+### 方法一:迭代法 — 虚拟头节点 + 尾指针 ⭐(推荐)
+
+维护一个**虚拟头节点** `dummy` 和一个**尾指针** `tail`:
+
+| 变量 | 含义 |
+|------|------|
+| **dummy** | 虚拟头节点,`dummy.Next` 始终指向结果链表的真正头部 |
+| **tail** | 当前已合并部分的最后一个节点,用于在末尾追加 |
+
+**核心操作:** 每次比较 `l1.Val` 和 `l2.Val`,将较小的节点连接到 `tail.Next`,然后该链表的指针前进一步。
+
+```mermaid
+flowchart TB
+ subgraph Inputs["📥 输入:两条升序链表"]
+ L1["l1\n1→2→4→nil"]
+ L2["l2\n1→3→4→nil"]
+ end
+
+ subgraph Core["⚙️ 核心操作(循环内每轮重复)"]
+ CMP["比较 l1.Val vs l2.Val"]
+ Pick["选较小值节点"]
+ end
+
+ subgraph Output["📤 输出:结果链表(尾部追加)"]
+ Dummy["dummy (虚拟头节点)"]
+ Res["结果: 1→1→2→3→4→4→nil"]
+ end
+
+ L1 -->|当前 head| CMP
+ L2 -->|当前 head| CMP
+ CMP -->|l1.Val ≤ l2.Val?| Pick
+ Pick -->|连接到 tail.Next| Res
+ Res --> Dummy
+
+ style Dummy fill:#e0e0ff,stroke:#666,stroke-width:2px
+ style CMP fill:#fff4e6,stroke:#f90,stroke-width:2px
+ style Pick fill:#d4edda,stroke:#28a,stroke-width:2px
+ style Res fill:#e8f5e9,stroke:#4a4,stroke-width:1px
+```
+
+**图示执行流程:**
+
+```mermaid
+flowchart LR
+ State0["初始\n---\ndummy → ?\nl1: 1→2→4\nl2: 1→3→4"] -->|"1≤1, 取 l1(1)"| State1["第 1 轮\n---\ndummy → [1]\nl1: 2→4\nl2: 1→3→4"]
+ State1 -->|"2>1, 取 l2(1)"| State2["第 2 轮\n---\ndummy → [1→1]\nl1: 2→4\nl2: 3→4"]
+ State2 -->|"2≤3, 取 l1(2)"| State3["第 3 轮\n---\ndummy → [1→1→2]\nl1: 4\nl2: 3→4"]
+ State3 -->|"4>3, 取 l2(3)"| State4["第 4 轮\n---\ndummy → [1→1→2→3]\nl1: 4\nl2: 4"]
+ State4 -->|"4≤4, 取 l1(4)"| State5["第 5 轮\n---\ndummy → [1→1→2→3→4]\nl1: nil\nl2: 4"]
+ State5 -->|"l1 空, 接 l2"| State6["收尾\n---\ndummy → [1→1→2→3→4→4] ✅"]
+
+ State6:::final
+ classDef final fill:#4c4,color:white
+```
+
+以示例 1(`l1 = [1,2,4]`, `l2 = [1,3,4]`)为例,逐步展开:
+
+| 轮次 | 比较对象 | 较小值 | tail 连接 | l1 移动后 | l2 移动后 | 结果链表 |
+|------|---------|--------|----------|----------|----------|---------|
+| 初始 | — | — | dummy | 1→2→4 | 1→3→4 | `dummy → ?` |
+| 第 1 轮 | l1.Val(1) vs l2.Val(**1**) | 相等,选 l1 | tail→l1(1) | 2→4 | 1→3→4 | `1_a` |
+| 第 2 轮 | l1.Val(**2**) vs l2.Val(1) | 1 | tail→l2(1) | 2→4 | 3→4 | `1_a → 1_b` |
+| 第 3 轮 | l1.Val(**2**) vs l2.Val(3) | 2 | tail→l1(2) | 4 | 3→4 | `1_a → 1_b → 2` |
+| 第 4 轮 | l1.Val(**4**) vs l2.Val(3) | 3 | tail→l2(3) | 4 | 4 | `1→1→2→3` |
+| 第 5 轮 | l1.Val(**4**) vs l2.Val(4) | 相等,选 l1 | tail→l1(4) | nil | 4 | `1→1→2→3→4` |
+| l1 耗尽 | — | — | tail→l2(4) | nil | nil | `1→1→2→3→4→4` ✅ |
+
+> [!info] 🧠 关键性质
+> - 因为两个输入链表本身就是有序的,所以**每一步贪心选择较小的元素**就能保证全局有序
+> - `dummy` 解决了"第一个节点没有前驱可连"的边界问题——统一了所有节点的处理逻辑
+> - `tail` 只在末尾追加,永不回头修改,所以不需要反转操作
+
+**当某个链表提前走完时:**直接将 `tail.Next` 指向另一个链表的剩余部分即可——因为剩余部分本身有序,无需额外处理。
+
+**时间复杂度:O(m + n)** — 每个节点恰好被访问一次。
+**空间复杂度:O(1)** — 只使用了常数个指针变量。
+
+### 方法二:递归法(更优雅但占用栈空间)
+
+> [!question] 💡 引导思考
+> 合并两个有序链表时,**第一步做什么**——一定是把两个头节点中较小的那个作为结果链表的第一个节点。那第二步呢?
+
+第二步是:**用剩下的部分重复同样的过程**。这正是递归的天然结构!
+
+**递归关系:**
+
+$$\text{merge}(l1, l2) = \begin{cases} l1 & \text{if } l2 == \text{nil} \\ l2 & \text{if } l1 == \text{nil} \\ l1.\text{Next} = \text{merge}(l1.\text{Next}, l2); \;\; return\; l1 & \text{if } l1.\text{Val} \leq l2.\text{Val} \\ l2.\text{Next} = \text{merge}(l1, l2.\text{Next}); \;\; return\; l2 & \text{otherwise} \end{cases}$$
+
+**执行流程拆解**(以 `l1=[1,2,4]`, `l2=[1,3,4]` 为例):
+
+### 递:深入调用栈
+
+每一层做出选择后,将较小节点的 `Next` 指向下一层递归的返回值,然后继续深入:
+
+```mermaid
+flowchart TD
+ L1A["Merge(①, ①) → 选 l1(①)"] -->|"Next = Merge(2→4, 1→3→4)"| L1B["Merge(②, ①) → 选 l2(①)"]
+ L1B -->|"Next = Merge(2→4, 3→4)"| L1C["Merge(②, ③) → 选 l1(②)"]
+ L1C -->|"Next = Merge(4, 3→4)"| L1D["Merge(④, ③) → 选 l2(③)"]
+ L1D -->|"Next = Merge(4, ④)"| L1E["Merge(④, ④) → 选 l1(④)"]
+ L1E -->|"Next = Merge(nil, ④)"| L1F["Merge(nil, ④) ← 基准情况,返回 ④"]
+
+ style L1F fill:#f9d,stroke:#333,stroke-width:2px
+ style L1A fill:#bbf,stroke:#333
+```
+
+### 归:逐层拼接结果
+
+在函数返回的过程中,上一层接收到的就是下一层的返回值——直接连到之前选择的节点上:
+
+```mermaid
+flowchart LR
+ R1["Merge(nil, ④)\n返回: 4→nil"] --> R2["Merge(④, ④)\n4→Next=4→nil\n返回: 4→4→nil"]
+ R2 --> R3["Merge(④, ③)\n3→Next=4→4→nil\n返回: 3→4→4→nil"]
+ R3 --> R4["Merge(②, ③)\n2→Next=3→4→4→nil\n返回: 2→3→4→4→nil"]
+ R4 --> R5["Merge(②, ①)\n1→Next=2→3→4→4→nil\n返回: 1→2→3→4→4→nil"]
+ R5 --> R6["Merge(①, ①)\n1→Next=1→2→3→4→4→nil\n返回: 1→1→2→3→4→4 ✅"]
+
+ style R1 fill:#bbf,stroke:#333
+ style R6 fill:#4c4,color:white,stroke:#333,stroke-width:2px
+```
+
+> [!info] 🧠 递归与迭代的本质联系
+> 递归版本中的 `l1.Next = mergeTwoLists(l1.Next, l2)` 或 `l2.Next = mergeTwoLists(l1, l2.Next)` 就是在做和迭代版 `tail.Next = smallerNode` 完全相同的操作——把当前最小的节点挂到结果后面。只是递归通过函数调用栈隐式地"记住"了上下文,而迭代版用显式的 `tail` 变量追踪当前位置。
+
+**时间复杂度:O(m + n)** — 每层递归处理一个节点,共 m + n 层。
+**空间复杂度:O(m + n)** — 递归调用栈深度等于节点总数。
+
+---
+
+## 代码提示
+
+### 迭代法伪代码
+
+```
+dummy = &ListNode{} // 虚拟头节点
+tail = dummy // 尾指针紧跟 dummy
+
+for l1 != nil AND l2 != nil {
+ if l1.Val <= l2.Val {
+ tail.Next = l1 // 把 l1 接到结果末尾
+ l1 = l1.Next // l1 往前走一步
+ } else {
+ tail.Next = l2 // 把 l2 接到结果末尾
+ l2 = l2.Next // l2 往前走一步
+ }
+ tail = tail.Next // tail 也往前走一步(跟上)
+}
+
+// l1 或 l2 中至少有一个为空,接上非空的部分
+if l1 != nil {
+ tail.Next = l1
+} else {
+ tail.Next = l2
+}
+
+return dummy.Next // 跳过 dummy,返回真正的头节点
+```
+
+### 递归法伪代码
+
+```
+func merge(l1, l2):
+ if l1 == nil: return l2 // 基准情况 1
+ if l2 == nil: return l1 // 基准情况 2
+
+ if l1.Val <= l2.Val {
+ l1.Next = merge(l1.Next, l2)
+ return l1
+ } else {
+ l2.Next = merge(l1, l2.Next)
+ return l2
+ }
+```
+
+---
+
+## 技巧
+
+> [!tip] 🔑 虚拟头节点(Dummy Node)万能模板
+> 凡是涉及"在链表头部插入/连接"的场景,优先考虑加 dummy。它能避免对首个节点写特殊逻辑,让循环体内保持统一的操作模式:`tail.Next = newnode; tail = tail.Next`。这个模式在「移除链表元素」「分隔链表」「奇偶链表」等题目中反复出现。
+
+> [!note] 🔑 Go 语言细节:切片转链表的辅助函数
+> 在本地测试时,你可能需要方便地在切片和链表之间转换:
+
+```go
+// sliceToList: 将切片转为链表,方便构造测试用例
+func sliceToList(vals []int) *ListNode {
+ dummy := &ListNode{}
+ tail := dummy
+ for _, v := range vals {
+ tail.Next = &ListNode{Val: v}
+ tail = tail.Next
+ }
+ return dummy.Next
+}
+
+// listToSlice: 将链表转为切片,方便打印验证
+func listToSlice(head *ListNode) []int {
+ var result []int
+ for head != nil {
+ result = append(result, head.Val)
+ head = head.Next
+ }
+ return result
+}
+```
+
+> [!warning] ⚠️ 常见错误:忘记推进 tail
+> 在连接 `tail.Next = smallerNode` 之后,一定要执行 `tail = tail.Next`。漏掉这步会导致后续所有节点都覆盖到同一个位置,最终结果只剩最后一个接入的节点。
+
+> [!tip] 🔑 相等情况的选择
+> 当 `l1.Val == l2.Val` 时,选哪个都可以,结果相同。但从递归视角看——先选 l1 意味着下一轮比较的是 `l1.Next` vs `l2`,先选 l2 则比较 `l1` vs `l2.Next`——虽然不影响最终结果,但在某些变体题(如稳定排序需求)中可能会有细微差异。
+
+> [!note] 🐹 Go 中的链表定义
+> LeetCode 的 Go 环境内置如下结构体定义:
+
+```go
+type ListNode struct {
+ Val int
+ Next *ListNode
+}
+```
+
+不需要手动定义,直接在解题中使用即可。
+
+> [!info] 📊 两种方法对比
+
+| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
+|------|-----------|-----------|------|------|
+| **迭代法 ⭐** | **O(m + n)** | **O(1)** | 空间最优、无栈溢出风险 | 需要理解 dummy + tail 的配对使用 |
+| 递归法 | O(m + n) | O(m + n) | 代码极短(4 行核心逻辑)、可读性高 | 栈空间消耗大;Go 不会尾递归优化,n 较大时可能栈溢出 |
+
+> [!success] ✅ 相关题目串联
+> - [82-删除排序链表中的重复元素 II](./82-删除排序链表中的重复元素-II.md) — 同一组有序链表的变种
+> - [21-Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/) — 进阶:合并 K 个有序链表(分治 + 本题解法)
+
+---
+
+## 代码
+
+### 迭代法
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
+ // 虚拟头节点:避免对第一个节点做特殊处理
+ dummy := &ListNode{}
+ tail := dummy // tail 始终指向结果链表的最后一个节点
+
+ // 只要两条链表都不为空,逐个比较、依次接入
+ for l1 != nil && l2 != nil {
+ if l1.Val <= l2.Val {
+ tail.Next = l1 // 将较小的 l1 节点接到结果末尾
+ l1 = l1.Next // l1 前进一步
+ } else {
+ tail.Next = l2 // 将较小的 l2 节点接到结果末尾
+ l2 = l2.Next // l2 前进一步
+ }
+ tail = tail.Next // tail 跟进,保持指向末尾
+ }
+
+ // 有一条链表走完后,直接把 tail 连接到另一条的剩余部分
+ // Go 的 nil 检查很直观:如果 l1 为空就接 l2,反之接 l1
+ if l1 != nil {
+ tail.Next = l1
+ } else {
+ tail.Next = l2
+ }
+
+ // 返回 dummy.Next(跳过虚拟头节点),即为合并后的真正头节点
+ return dummy.Next
+}
+```
+
+### 递归法
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
+ // 基准情况:一条链表为空,直接返回另一条
+ if l1 == nil {
+ return l2
+ }
+ if l2 == nil {
+ return l1
+ }
+
+ // 递归:选较小的头节点,它的 Next 指向合并剩余部分的结果
+ if l1.Val <= l2.Val {
+ l1.Next = mergeTwoLists(l1.Next, l2) // 递归处理 l1 的剩余部分
+ return l1
+ } else {
+ l2.Next = mergeTwoLists(l1, l2.Next) // 递归处理 l2 的剩余部分
+ return l2
+ }
+}
+```
+
+> [!success] ✅ 运行验证
+> 这是 LeetCode 第 21 题,通过率约 60%+。作为链表入门题的代表作,它完美展示了两个经典范式:① **迭代**——dummy + tail 的双指针尾部追加模式;② **递归**——"选出当前最小者 + 递归处理剩余"的分治思路。建议两个版本都手写一遍,体会"显式状态追踪"和"隐式调用栈"两种编程哲学的差异。
diff --git a/链表/28-两数相加.md b/链表/28-两数相加.md
new file mode 100644
index 0000000..ea9d9a4
--- /dev/null
+++ b/链表/28-两数相加.md
@@ -0,0 +1,382 @@
+---
+tags: ["LeetCode", "链表", "模拟", "简单"]
+create time: 2026-05-16 15:00
+---
+
+# 28-两数相加
+
+## 题面
+
+给你两个 **非空** 的链表,表示两个非负的整数。它们每位数字都是按照 **逆序** 的方式存储的,并且每个节点只能存储 **一位** 数字。
+
+请你将两个数相加,并以相同形式返回一个表示和的链表。
+
+你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
+
+**示例 1:**
+
+```
+输入:l1 = [2,4,3], l2 = [5,6,4]
+输出:[7,0,8]
+解释:342 + 465 = 807.
+```
+
+**示例 2:**
+
+```
+输入:l1 = [0], l2 = [0]
+输出:[0]
+```
+
+**示例 3:**
+
+```
+输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
+输出:[8,9,9,9,0,0,0,1]
+```
+
+**提示:**
+
+- 每个链表中的节点数在范围 `[1, 100]` 内
+- `0 <= Node.val <= 9`
+- 题目数据保证列表表示的数字不含前导零
+
+---
+
+## 思路
+
+> [!question] 💡 引导思考
+> 回想小时候学竖式加法——从右往左(个位开始),每一位加起来,如果结果 ≥ 10 就把十位"进"到下一位。链表是逆序存储的,意味着 **头节点就是最低位**。这不就是天然适合从头到尾做竖式加法吗?
+
+### 核心洞察:逐位模拟加法
+
+> [!info] 🧠 关键性质
+> 因为链表逆序存储了数字,低位在前、高位在后,所以我们从两个链表的头节点同时出发,就像从个位开始逐位相加,完全对应人类列竖式的习惯。
+
+每次循环需要处理三件事:
+
+| 步骤 | 动作 | 说明 |
+|------|------|------|
+| ① | **取当前位的值** | 如果某个链表已走到末尾,该位的值视为 `0` |
+| ② | **加上进位** | 把上一步产生的进位也加进去 |
+| ③ | **计算新位和新的进位** | `sum % 10` 是新位的值,`sum / 10` 是向下一位的进位 |
+
+```mermaid
+flowchart TB
+ subgraph Inputs["📥 输入:两条逆序链表"]
+ L1["l1\n2→4→3→nil\n(表示 342)"]
+ L2["l2\n5→6→4→nil\n(表示 465)"]
+ end
+
+ subgraph Loop["⚙️ 每轮循环"]
+ ADD1["取 l1.Val + l2.Val + carry"]
+ MOD["新位 = sum % 10"]
+ DIV["新进位 = sum / 10"]
+ APPEND["创建新节点,追加到结果尾部"]
+ end
+
+ subgraph Output["📤 输出:结果链表"]
+ Res["结果\n7→0→8→nil\n(表示 807) ✅"]
+ end
+
+ L1 -->|当前位| ADD1
+ L2 -->|当前位| ADD1
+ carry_in[/"💾 carry 入"/] -.进位.-> ADD1
+ ADD1 --> MOD
+ ADD1 --> DIV
+ DIV -->|进位给下一轮| carry_next["carry → ..."]
+ MOD --> APPEND
+ APPEND --> Res
+
+ style ADD1 fill:#fff4e6,stroke:#f90,stroke-width:2px
+ style carry_in fill:#ffe0e0,stroke:#c66,stroke-width:1px
+ style Res fill:#d4edda,stroke:#28a,stroke-width:2px
+ style Loop fill:#fafafa,stroke:#ccc,stroke-dasharray:5 5
+```
+
+### 方法一:迭代法 — 虚拟头节点 + 尾指针 ⭐(推荐)
+
+维护三个变量:
+
+| 变量 | 含义 | 初始值 |
+|------|------|--------|
+| **dummy** | 虚拟头节点,`dummy.Next` 指向结果的真正头部 | `&ListNode{}` |
+| **tail** | 已构建结果链表的最后一个节点 | `dummy` |
+| **carry** | 当前的进位值 | `0` |
+
+**逐步展开执行过程**(以示例 3 为例,展示进位传播的极端情况):
+
+`l1 = [9,9,9,9]`, `l2 = [9,9,9,9,9,9,9]`
+
+| 轮次 | l1 取值 | l2 取值 | carry 入 | sum | 新位(sum%10) | 新进位(sum/10) | 结果链表 |
+|:----:|:-------:|:-------:|:--------:|:---:|:------------:|:--------------:|---------|
+| 初始 | — | — | 0 | — | — | — | `dummy → ?` |
+| 第 1 轮 | **9** | **9** | 0 | **18** | **8** | **1** | `8_a` |
+| 第 2 轮 | **9** | **9** | 1 | **19** | **9** | **1** | `8→9` |
+| 第 3 轮 | **9** | **9** | 1 | **19** | **9** | **1** | `8→9→9` |
+| 第 4 轮 | **9** | **9** | 1 | **19** | **9** | **1** | `8→9→9→9` |
+| l1 耗尽 | 0 | **9** | 1 | **10** | **0** | **1** | `8→9→9→9→0` |
+| 第 6 轮 | 0 | **9** | 1 | **10** | **0** | **1** | `8→9→9→9→0→0` |
+| 第 7 轮 | 0 | **9** | 1 | **10** | **0** | **1** | `8→9→9→9→0→0→0` |
+| **收尾** | — | — | **1** | **1** | **1** | **0** | `8→9→9→9→0→0→0→1` ✅ |
+
+> [!tip] 🔑 收尾处理两个场景
+>
+> 当主循环结束时有两种可能:
+>
+> | 场景 | 条件 | 处理方式 |
+> |------|------|----------|
+> | A: 无剩余进位 | `l1 == nil && l2 == nil && carry == 0` | 直接返回 `dummy.Next`,无需额外操作 |
+> | B: 有剩余进位 | `l1 == nil && l2 == nil && carry == 1` | 追加一个值为 `1` 的新节点 |
+
+**时间复杂度:O(max(m, n))** — 遍历较长的那个链表。
+**空间复杂度:O(max(m, n))** — 结果链表本身需要的空间(不计入的空间复杂度为 O(1))。
+
+### 方法二:递归法(同一思想的函数式表达)
+
+> [!question] 💡 为什么递归不太适合?
+> 迭代版用 `carry` 这个局部变量自然地在循环中传递进位,而递归版的 `carry` 必须作为参数显式地穿层而过。虽然逻辑等价,但 Go 不支持尾递归优化,长链表可能导致栈溢出。
+
+**递归关系:**
+
+$$\text{add}(p, q, \text{carry}) = \begin{cases} \text{nil} & \text{if } p=\text{nil} \land q=\text{nil} \land \text{carry}=0 \\ \text{newNode}(\text{carry}) & \text{if } p=\text{nil} \land q=\text{nil} \land \text{carry}=1 \\ \text{node}(\text{sum}\bmod 10).\text{Next} = \text{add}(p.\text{Next}, q.\text{Next}, \text{sum}/10) & \text{otherwise} \end{cases}$$
+
+其中 $\text{sum} = v_p + v_q + \text{carry}$,$v_p$ 表示 $p$ 的值(若 $p=\text{nil}$ 则为 0)。
+
+**调用栈展开**(以 `l1=[2,4,3]`, `l2=[5,6,4]` 为例):
+
+```mermaid
+flowchart TD
+ F1["AddTwoNumbers(2,5,0)\nsum=7,carry=0\nNext = F2"] -->|"7→"| F2["AddTwoNumbers(4,6,0)\nsum=10,carry=1\nNext = F3"]
+ F2 -->|"0→"| F3["AddTwoNumbers(3,4,1)\nsum=8,carry=0\nNext = F4"]
+ F3 -->|"8→"| F4["AddTwoNumbers(nil,nil,0)\n基准情况,返回 nil"]
+
+ F4:::base
+ classDef base fill:#f9d,stroke:#333,stroke-width:2px
+```
+
+**时间复杂度:O(max(m, n))** — 每层处理一个节点。
+**空间复杂度:O(max(m, n))** — 递归栈深度等于较长链表的长度。Go 不保证尾递归消除,实际不推荐用于生产场景。
+
+---
+
+## 代码提示
+
+### 迭代法伪代码
+
+```
+dummy = &ListNode{} // 虚拟头节点
+tail = dummy // 尾指针,指向结果链表末尾
+carry = 0 // 进位,初始为 0
+
+for l1 != nil OR l2 != nil OR carry > 0 {
+ val1 = 0
+ if l1 != nil { val1 = l1.Val; l1 = l1.Next }
+
+ val2 = 0
+ if l2 != nil { val2 = l2.Val; l2 = l2.Next }
+
+ sum = val1 + val2 + carry
+ tail.Next = &ListNode{Val: sum % 10} // 新位
+ tail = tail.Next // 尾指针跟进
+ carry = sum / 10 // 新进位
+}
+
+return dummy.Next
+```
+
+### 递归法伪代码
+
+```
+func addNodes(p, q *Node, carry int) *Node:
+ // 基准情况:两个链表都走完且无进位
+ if p == nil and q == nil and carry == 0:
+ return nil
+
+ valP = 0 if p == nil else p.Val
+ valQ = 0 if q == nil else q.Val
+
+ sum = valP + valQ + carry
+
+ newNode = Node{ Val: sum % 10 }
+ // 递推下一层
+ newNode.Next = addNodes(
+ p.Next if p != nil else nil,
+ q.Next if q != nil else nil,
+ sum / 10
+ )
+
+ return newNode
+```
+
+---
+
+## 技巧
+
+> [!tip] 🔑 短写法:利用 Go 的 nil 解引用
+>
+> 在 Go 中,`*ListNode` 的零值是 `nil`。可以用以下惯用法避免冗长的 if 判断:
+
+```go
+val1 := 0
+if l1 != nil {
+ val1 = l1.Val
+ l1 = l1.Next
+}
+```
+
+> 也可以写成一行表达式:`val1 := 0; if l1 != nil { val1, l1 = l1.Val, l1.Next }`,但拆成多行可读性更好。
+
+> [!warning] ⚠️ 常见错误:遗漏"最后一位进位"
+>
+> 很多人写出的循环条件是 `for l1 != nil && l2 != nil`,这会导致当两个链表等长且最高位产生进位时(如 `5+5=10`),遗漏最前面的那个 `1`。**正确做法**是把终止条件写成 `for l1 != nil || l2 != nil || carry > 0`,确保所有情况都被覆盖。
+
+> [!note] 🔑 为什么不需要判空输入?
+>
+> 题目保证链表是非空的,所以不必检查 `l1 == nil || l2 == nil`。但如果在实际工作中遇到类似场景,记得在最前面加一句保护性检查。
+
+> [!tip] 🔑 与竖式加法的精确映射
+
+| 竖式加法 | 代码对应 |
+|---------|---------|
+| 从个位开始算 | 从链表的 head(低位)开始遍历 |
+| 不够两位补零 | 链表为空时取值为 0 |
+| 满十进一 | `carry = sum / 10` |
+| 写下当前位 | `newNode{Val: sum % 10}` |
+| 最后进位写在最前面 | 循环结束后若 carry > 0,追加新节点 |
+
+> [!note] 🐹 Go 中的链表定义
+> LeetCode 的 Go 环境内置如下结构体定义:
+
+```go
+type ListNode struct {
+ Val int
+ Next *ListNode
+}
+```
+
+不需要手动定义,直接在解题中使用即可。
+
+> [!info] 📊 两种方法对比
+
+| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
+|------|-----------|-----------|------|------|
+| **迭代法 ⭐** | **O(max(m,n))** | **O(1)**(除结果外) | 无栈溢出风险、常数小 | 需要理解 dummy + tail 模式 |
+| 递归法 | O(max(m,n)) | O(max(m,n)) | 代码简洁、逻辑直观 | Go 无尾递归优化,长链表可能栈溢出 |
+
+> [!success] ✅ 相关题目串联
+> - [[2-两数相加 II]] — 进阶版:数字是**正序**存储的(需要从后往前加)
+> - [[27-合并两个有序链表]] — 同样是双链表遍历的经典范式
+> - [[67-二进制求和]] — LeetCode 第 67 题:原理完全相同,只是逢二进一
+
+---
+
+## 代码
+
+### 迭代法
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
+ // 虚拟头节点:避免对第一个节点做特殊处理
+ dummy := &ListNode{}
+ tail := dummy // tail 始终指向结果链表的最后一个节点
+ carry := 0 // 进位值
+
+ // 当任意一条链表还有节点,或者还有进位未处理时继续循环
+ for l1 != nil || l2 != nil || carry > 0 {
+ // 取出当前位的值(链表为空时视为 0)
+ val1 := 0
+ if l1 != nil {
+ val1 = l1.Val
+ l1 = l1.Next
+ }
+
+ val2 := 0
+ if l2 != nil {
+ val2 = l2.Val
+ l2 = l2.Next
+ }
+
+ // 逐位相加:当前位的两个数字 + 上一轮的进位
+ sum := val1 + val2 + carry
+
+ // 创建新节点存放结果的当前位(个位数)
+ tail.Next = &ListNode{Val: sum % 10}
+ tail = tail.Next // 尾指针跟进
+
+ // 更新进位(十位数)
+ carry = sum / 10
+ }
+
+ // 跳过 dummy,返回真正的头节点
+ return dummy.Next
+}
+```
+
+### 递归法
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
+ return addNodes(l1, l2, 0)
+}
+
+// addNodes 递归处理两个链表对应位置及进位
+func addNodes(p, q *ListNode, carry int) *ListNode {
+ // 基准情况:两个链表都已走完,且没有剩余进位
+ if p == nil && q == nil && carry == 0 {
+ return nil
+ }
+
+ // 取当前位的值(节点为空时值为 0)
+ valP := 0
+ if p != nil {
+ valP = p.Val
+ }
+ valQ := 0
+ if q != nil {
+ valQ = q.Val
+ }
+
+ // 计算当前位的和与新的进位
+ sum := valP + valQ + carry
+
+ // 创建当前位的节点
+ current := &ListNode{Val: sum % 10}
+
+ // 递归处理下一位,并将返回值链接到当前节点的 Next
+ current.Next = addNodes(
+ mapNil(p), // 传入下一节点(nil 安全)
+ mapNil(q),
+ sum/10, // 进位作为参数传入下一层
+ )
+
+ return current
+}
+
+// mapNil 帮助函数:nil-safe 获取 Next
+func mapNil(node *ListNode) *ListNode {
+ if node == nil {
+ return nil
+ }
+ return node.Next
+}
+```
+
+> [!success] ✅ 运行验证
+> 这是 LeetCode 第 2 题,通过率约 45%+。它考察的核心只有一个:**模拟竖式加法并处理进位**。虽然代码很短,但它完美展示了链表遍历的经典范式——dummy 节点 + 尾指针 + 边界循环条件。建议至少手写迭代版,递归版作为拓展理解即可。
diff --git a/链表/29-删除链表的倒数第 N 个结点.md b/链表/29-删除链表的倒数第 N 个结点.md
new file mode 100644
index 0000000..f0a6ecf
--- /dev/null
+++ b/链表/29-删除链表的倒数第 N 个结点.md
@@ -0,0 +1,260 @@
+---
+tags: ["LeetCode", "链表", "双指针", "快慢指针", "虚拟头节点", "中等"]
+create time: 2026-05-16 15:00
+---
+
+# 29-删除链表的倒数第 N 个结点
+
+## 题面
+
+给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。
+
+**示例 1:**
+
+```
+输入:head = [1,2,3,4,5], n = 2
+输出:[1,2,3,5]
+```
+
+**示例 2:**
+
+```
+输入:head = [1], n = 1
+输出:[]
+```
+
+**示例 3:**
+
+```
+输入:head = [1,2], n = 1
+输出:[1]
+```
+
+**提示:**
+
+- 链表中结点的数目为 `sz`
+- `1 <= sz <= 30`
+- `0 <= Node.val <= 100`
+- `1 <= n <= sz`
+
+**进阶:** 你能尝试使用一趟扫描实现吗?
+
+---
+
+## 思路
+
+> [!question] 💡 核心洞察
+> 要删除倒数第 n 个节点,最直接的念头是——"先数总长,再找正数位置"。但这需要**两遍扫描**。进阶要求我们**一趟搞定**,怎么做到的?
+
+关键在于:**用距离(gap)代替计数**。如果我们让两个指针之间始终保持 n 个节点的间隔,那么当快指针走到末尾时,慢指针自然就在目标位置。
+
+### 关键问题:如何定位?
+
+> [!warning] ⚠️ 最大陷阱
+> 删除一个节点需要的是它的**前驱节点**——因为只有前驱才能修改 `Next` 指针把目标摘出去。如果慢指针恰好停在待删节点上,你就无从下手了。
+
+所以我们的目标是:让慢指针最终停在**待删节点的前驱位置**。
+
+### 方法一:快慢指针 + 虚拟头节点 ⭐(最优 — 一趟扫描)
+
+维护两个指针 `fast` 和 `slow`,初始都指向一个**虚拟头节点** `dummy`(`dummy.Next = head`)。
+
+**核心策略:** 先让 `fast` 走 n 步,然后 `fast` 和 `slow` 同时出发,每次各走一步。当 `fast` 到达最后一个节点时(`fast.Next == nil`),`slow` 恰好停在倒数第 n 个节点的前驱。
+
+**为什么这样一定正确?**
+
+> [!question] 💡 引导思考
+> 假设链表长度为 sz,快指针领先 n 步。那么当快指针走完剩余路程时,它走了多少步?慢指针呢?它们之间的距离保持了几步?
+
+快指针再走 `sz - n` 步会到达**最后一个节点**(而非越过边界)。循环条件 `fast.Next != nil` 保证在快指针停在尾节点时立即终止。此时慢指针也恰好走了 `sz - n` 步。由于 `dummy` 是第 0 位,`dummy → slow` 的距离就是 `sz - n`——这意味着 slow 指向的是正数第 `sz - n` 个节点,即倒数第 `n + 1` 个节点,正是待删节点的前驱。
+
+关键不变量:**从 fast 先走 n 步之后起,slow 永远比 fast 落后 n 个节点**。这 n 的间隔全程保持不变,直到循环结束。
+
+**图示执行流程:**
+
+```mermaid
+flowchart LR
+ subgraph Init["第一步:fast 先走 n 步(n=2)"]
+ D["dummy"] -->|"slow"| A["1"]
+ A --> B["2"]
+ B --> C["3"]
+ C --> D2["4"]
+ D2 --> E["5"]
+ F["fast"] -.走2步.-> B
+ end
+
+ subgraph Sync["第二步:fast & slow 同步前进(直到 fast == 尾节点)"]
+ B -->|"slow→1, fast→3"| G["slow: 1 / fast: 3"]
+ G -->|"slow→2, fast→4"| H["slow: 2 / fast: 4"]
+ H -->|"slow→3, fast→5"| I["slow: 3 / fast: 5"]
+ end
+
+ subgraph Done["结束:fast==尾节点, slow在倒数第 n+1 个(待删前驱)"]
+ I -->|"fast.Next==nil, 退出"| K["slow指向3\n删除slow.Next(4)\n1→2→3→5 ✅"]
+ end
+
+ Init --> Sync --> Done
+
+ classDef done fill:#4c4,color:white
+ class K done
+```
+
+逐步展开(以 `head = [1,2,3,4,5], n = 2` 为例):
+
+| 阶段 | dummy | slow | fast | 说明 |
+|------|--------|------|------|------|
+| 初始 | → 1→2→3→4→5 | dummy | dummy | 都从虚拟头节点出发 |
+| fast 走 n=2 步后 | → 1→2→3→4→5 | dummy | **2** | fast 跳过 1、2,停在 2 |
+| 第 1 轮同步 | → 1→2→3→4→5 | **1** | **3** | 各走一步(fast.Next=3 ≠ nil) |
+| 第 2 轮同步 | → 1→2→3→4→5 | **2** | **4** | 各走一步(fast.Next=4 ≠ nil) |
+| 第 3 轮同步 | → 1→2→3→4→5 | **3** | **5** | 各走一步(fast.Next=5 ≠ nil) |
+| 循环终止 | — | **3** | **5** | fast.Next == nil,退出循环 |
+| 删除操作 | — | 指向 3 | — | `slow.Next = slow.Next.Next`,删除节点 4 |
+
+```mermaid
+flowchart TD
+ Before["删除前\n1→2→3→4→5\n ↑slow\n ↑slow.Next\n ↑slow.Next.Next\n \ndelete slow.Next"] --> After["删除后\n1→2→3→5\n ↑slow.Next 指向 5 ✅"]
+
+ style Before fill:#fff4e6,stroke:#f90
+ style After fill:#d4edda,stroke:#28a
+```
+
+> [!info] 🧠 为什么用 `dummy` 能优雅处理所有边界?
+> - **删除头节点**:如 `n = 5`(删除 1),此时 `dummy` 先走 5 步刚好到 `nil`,`slow` 还在 `dummy`,`slow.Next = slow.Next.Next` 删除的就是节点 1。
+> - **删除尾节点**:如 `n = 1`(删除 5),此时 `slow` 停在节点 4,正常删除。
+> - **单节点链表**:如 `head = [1], n = 1`,结果直接是空链表。
+
+**时间复杂度:O(n)** — 每个节点最多被访问一次。
+**空间复杂度:O(1)** — 只用了两个指针变量。
+
+---
+
+## 代码提示
+
+### 快慢指针伪代码
+
+```
+// 第一步:创建虚拟头节点
+dummy = &ListNode{Next: head}
+slow = dummy
+fast = dummy
+
+// 第二步:fast 先走 n 步
+for i = 0; i < n; i++ {
+ fast = fast.Next
+}
+
+// 第三步:fast 和 slow 同步前进,直到 fast 到达最后一个节点
+for fast.Next != nil {
+ slow = slow.Next
+ fast = fast.Next
+}
+
+// 第四步:通过 slow 的 Next 跳过待删节点
+slow.Next = slow.Next.Next
+
+return dummy.Next
+```
+
+### 图解关键步骤
+
+```mermaid
+flowchart LR
+ Step1["dummy 节点\n防止删除头节点时需要特殊判断"] --> Step2["fast 先走 n 步\n制造 n 的间隔"]
+ Step2 --> Step3["同步前进\nfast 到头时\nslow 恰好在倒数第 n+1 个"]
+ Step3 --> Step4["跳过待删节点\nslow.Next = slow.Next.Next"]
+
+ style Step1 fill:#bbf,stroke:#333
+ style Step2 fill:#f9d,stroke:#333
+ style Step3 fill:#ffd700,stroke:#333
+ style Step4 fill:#d4edda,stroke:#28a
+```
+
+---
+
+## 技巧
+
+> [!tip] 🔑 快慢指针"间隔法"通用模式
+> "让快指针先走 k 步,然后一起走"是一个极其通用的模板。它解决的问题范式是:**找到与末尾有固定距离的某个位置**。除了本题(倒数第 n 个),还衍生出:
+> - 链表的中间节点(k = n/2)
+> - 环形链表的入口(稍作变形)
+> - 排序数组中的差值对(类似思想扩展到数组)
+
+> [!warning] ⚠️ 常见错误 1:忘记加虚拟头节点
+> 如果不用 `dummy`,删除头节点(如 `n = sz`)时需要单独处理。加上 `dummy` 后,所有节点都被"右移了一位",统一由 `slow.Next.Next` 处理。
+
+> [!warning] ⚠️ 常见错误 2:fast 提前为空
+> 当 n = sz 时,fast 走完 n 步后正好等于 `nil`。这时第三个循环(同步前进)不会执行,直接进入第四步——这正是正确的行为!不要误判为错误。
+
+> [!note] 🔑 两遍扫描解法(辅助理解)
+> 虽然不满足进阶要求,但两遍扫描的思路更直观:
+> ① 第一遍遍历求长度 len;② 第二遍从头走到第 `len - n` 个节点的位置并删除。这种方法的优势是更容易扩展——比如你想删除倒数第 n ~ m 个节点时,比快慢指针更容易改写。**面试时可以先说两遍扫描的方案展示理解,再用快慢指针优化到一遍。**
+
+> [!note] 🐹 Go 中的链表定义
+> LeetCode 的 Go 环境内置如下结构体定义:
+
+```go
+type ListNode struct {
+ Val int
+ Next *ListNode
+}
+```
+
+不需要手动定义,直接在解题中使用即可。
+
+> [!info] 📊 两种扫描方式对比
+
+| 维度 | 两遍扫描 | 一遍扫描(快慢指针)⭐ |
+|------|---------|---------------------|
+| 时间复杂度 | O(n)(两次遍历) | O(n)(一次遍历) |
+| 空间复杂度 | O(1) | O(1) |
+| 直观程度 | 非常直观 | 需要理解"间隔不变量" |
+| 面试官满意度 | 合格,但不加分 | 优秀,满足进阶要求 |
+| 适用场景 | 无法保证单次扫描时可用 | 绝大多数链表问题的首选 |
+
+---
+
+## 代码
+
+```go
+/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ * Val int
+ * Next *ListNode
+ * }
+ */
+
+func removeNthFromEnd(head *ListNode, n int) *ListNode {
+ // 虚拟头节点:消除"删除头节点需要特殊处理"的边界情况
+ dummy := &ListNode{Next: head}
+ slow := dummy
+ fast := dummy
+
+ // 快指针先走 n 步,与慢指针拉开 n 的距离
+ for i := 0; i < n; i++ {
+ fast = fast.Next
+ }
+
+ // 快慢指针同步前进,直到 fast 到达最后一个节点
+ for fast.Next != nil {
+ slow = slow.Next
+ fast = fast.Next
+ }
+
+ // slow 现在停在倒数第 n 个节点的前驱
+ // slow.Next = slow.Next.Next 直接跳过待删节点
+ slow.Next = slow.Next.Next
+
+ return dummy.Next // 绕过虚拟头节点,返回真正的头节点
+}
+```
+
+> [!success] ✅ 运行验证
+> 这是 LeetCode 第 19 题,通过率约 40%+。看似简单的题目恰恰最能检验对链表操作的熟练度——虚拟头节点是否本能地想到、快慢指针的间隔不变量是否能清晰解释。这道题是后续更多复杂链表题目的基础:反转链表的一部分、合并 K 个有序链表、链表的 K 逆序等都依赖同样的"间隔控制"思想。建议配合 Mermaid 图示手写 2-3 遍,直到形成肌肉记忆。
+
+> [!note] 🔗 相关题目串联
+> - [141-环形链表](./25-环形链表.md) — 另一个经典的双指针应用
+> - [142-环形链表 II](./26-环形链表 II.md) — 快慢指针找环入口
+> - [23-反转链表](./23-反转链表.md) — 链表基础操作
+> - [24-两数相加](./28-两数相加.md) — 链表遍历的高级应用
diff --git a/链表/30-两两交换链表中的节点.md b/链表/30-两两交换链表中的节点.md
new file mode 100644
index 0000000..e69de29