diff --git a/二叉树/38-翻转二叉树.md b/二叉树/38-翻转二叉树.md
new file mode 100644
index 0000000..69f0001
--- /dev/null
+++ b/二叉树/38-翻转二叉树.md
@@ -0,0 +1,360 @@
+---
+tags: ["LeetCode", "二叉树", "深度优先搜索", "广度优先搜索", "递归", "迭代", "简单"]
+create time: 2026-05-17 10:45
+---
+
+# 38-翻转二叉树
+
+## 题面
+
+给你一棵二叉树的根节点 `root`,**翻转**这棵二叉树,并返回其根节点。
+
+> [!question] 💡 什么是「翻转」?
+> 翻转的意思是:**交换每个节点的左右子树**。换句话说,把整棵树沿中线做一面镜子,左变右、右变左。
+>
+> > [!note] 🪞 直观理解
+> > - 叶子节点:没有子节点,翻转后不变
+> > - 中间节点:左右子树互换位置
+> > - 翻转作用于**每一个**节点,从根一直到底
+
+**示例 1:**
+
+```
+输入:root = [4,2,7,1,3,6,9]
+输出:[4,7,2,9,6,3,1]
+
+原始树 翻转后
+ 4 4
+ / \ / \
+ 2 7 → 7 2
+ / \ / \ / \ / \
+1 3 6 9 9 6 3 1
+```
+
+**示例 2:**
+
+```
+输入:root = [2,1,3]
+输出:[2,3,1]
+
+ 2 2
+ / \ → / \
+1 3 3 1
+```
+
+**示例 3:**
+
+```
+输入:root = []
+输出:[]
+空树翻转后仍是空树
+```
+
+**约束:**
+
+- 树中节点数目范围在 `[0, 100]` 内
+- `-100 <= Node.val <= 100`
+
+---
+
+## 思路
+
+### 方法一:深度优先搜索 DFS(递归)⭐⭐
+
+#### 核心洞察
+
+> [!question] 🤔 思考
+> 如果你手里拿着一个节点,要翻转以它为根的这棵子树,你最需要做的动作是什么?
+
+答案其实只有一个:**交换它的左右孩子**——就这么简单!
+
+但要注意,交换完之后,左右孩子本身可能还挂着更深的子树,所以还需要**递归地对它们的子树也做同样的操作**。
+
+这就是分而治之的思想:把「翻转整棵树」分解为「翻转每棵子树」,每个子树只做一件事——交换左右孩子。
+
+#### 递归三要素
+
+| 要素 | 内容 |
+|------|------|
+| **终止条件** | 节点为空时,直接返回(空树无需翻转) |
+| **执行动作** | 交换 `node.Left` 和 `node.Right` |
+| **递归调用** | 对交换后的左右子节点分别递归翻转 |
+
+#### 执行流程图解
+
+以示例 1 的原始树为例,展示递归如何逐层深入、逐层完成翻转:
+
+```
+ 4 4 4
+ / \ / \ / \
+ 2 7 → 7 2 → 7 2
+ / \ / \ / \ / \ / \ / \
+ 1 3 6 9 9 6 3 1 9 6 3 1
+ ↑ ↑ ↑
+ 步骤3 步骤2 步骤1
+ (叶子不变) (7的孩⼦换位置) (根节点先换⼿)
+
+ ⾃顶向下递归(前序):先交换再递归孩⼦
+```
+
+```mermaid
+flowchart TD
+ S["dfs(4)\n先交换 4 的孩⼦"] --> L["dfs(7)\n交换 7 的孩⼦"]
+ L --> L1["dfs(nil) → nil\n⽆需翻转"]
+ L --> R1["dfs(nil) → nil\n⽆需翻转"]
+ L1 --> L2["返回 {6, 3}\n7的孩⼦已翻转✅"]
+ R1 --> L2
+ L2 --> M["dfs(2)\n交换 2 的孩⼦"]
+ M --> L3["dfs(9)\n返回{6, nil}✅"]
+ M --> R2["dfs(nil) → nil\n返回{3}✅"]
+ L3 --> M2["返回{1,3}\n2的孩⼦已翻转✅"]
+ R2 --> M2
+ M2 --> E["dfs(4)\n左=翻转后{7}, 右=翻转后{2}\n根节点完成✅🎉"]
+
+ style E fill:#d4edda,stroke:#28a745,stroke-width:3px
+ style M fill:#cce5ff,stroke:#004085
+ style L fill:#fff3cd,stroke:#856404
+```
+
+#### Go 语言的优雅写法
+
+Go 支持多值赋值,可以直接用一行完成交换,无需临时变量:
+
+```go
+// ❌ 常规写法(需要临时变量)
+tmp := node.Left
+node.Left = node.Right
+node.Right = tmp
+
+// ✅ Go 风格(并行交换)
+node.Left, node.Right = node.Right, node.Left
+```
+
+> [!tip] 🔑 多值赋值的奥秘
+> Go 的多值赋值是**先求值再赋值**——等号右边的表达式全部计算完毕后,再同时赋给左边。这意味着 `node.Left, node.Right = node.Right, node.Left` 等价于先用临时变量交换,不会先覆盖再出错。
+
+#### 一步到位版(最简递归)
+
+既然交换完左右子节点后,我们只需要让它们各自被翻转,那就可以把递归调用写在等号右边,直接赋值回去:
+
+```
+invertTree(node):
+ if node == nil: return nil
+
+ // 递归结果直接作为新的左右孩子
+ node.Left = invertTree(node.Right) // 原来的右子树翻转变左
+ node.Right = invertTree(node.Left) // 原来的左子树翻转变右
+
+ return node
+```
+
+> [!warning] ⚠️ 这和显式交换等价吗?
+> **完全等价!** 注意 Go 多值赋值的特性:`node.Left = invertTree(node.Right)` 执行时,`node.Left` 的值已经作为参数传入右侧函数了,虽然这里我们把 `node.Right` 的返回值赋给了 `node.Left`……等等,这里似乎有问题?
+>
+> 实际上这种写法**有 bug**!第一行执行完后 `node.Left` 已经被修改了,第二行 `invertTree(node.Left)` 翻转的是新左子树而非原左子树。
+>
+> 正确的单行写法是用 Go 多值赋值:
+
+```go
+node.Left, node.Right = invertTree(node.Right), invertTree(node.Left)
+```
+
+> [!note] ⚡ 多值赋值的正确用法
+> 上面的两行依次赋值写法是错误的(除非你确认左侧递归不依赖原值),但在 Go 中以下写法是正确的——因为多值赋值的右侧先全部求值完毕:
+>
+> ```go
+> node.Left, node.Right = invertTree(node.Right), invertTree(node.Left)
+> ```
+>
+> 这是最简洁的「翻转+递归+返回」三合一写法。
+
+### 方法二:广度优先搜索 BFS(层序遍历迭代)⭐
+
+#### 核心思想
+
+BFS 按层访问节点。**对于每一层的每个节点,交换其左右孩子即可。**
+
+这种方式的优点是过程非常直观:从左到右、从上到下扫一遍,每到一课就翻一次。
+
+```mermaid
+flowchart LR
+ A["第 1 层: [4]\n交换 4 的左右孩子"] --> B["第 2 层: [7,2]\n交换 7 和 2 的孩子"]
+ B --> C["第 3 层: [9,6,3,1]\n都是叶子,交换无变化"]
+ C --> D["遍历结束 ✅\n返回 root"]
+
+ style A fill:#e8f5e9
+ style B fill:#fff3e0
+ style C fill:#e3f2fd
+ style D fill:#fce4ec
+```
+
+#### 算法骨架
+
+```
+if root == nil: return nil
+
+queue = [root]
+
+while queue 非空:
+ node = pop(queue)
+ swap(node.left, node.right) // 核心动作
+
+ if node.left != nil: push(queue, node.left)
+ if node.right != nil: push(queue, node.right)
+
+return root
+```
+
+### 方法三:深度优先搜索 DFS(迭代)⭐
+
+除了 BFS 逐层翻转,DFS 也可以用显式栈来模拟递归过程。
+
+> [!comparison] ⚖️ 三种写法对比
+> | 维度 | DFS 递归 | BFS 迭代 | DFS 迭代 |
+> |------|---------|---------|---------|
+> | 代码简洁度 | ⭐⭐⭐ 仅需 4 行 | ⭐⭐ 需维护队列 | ⭐⭐ 需维护栈 |
+> | 空间复杂度 | O(h) 递归栈 | O(w) 队列最大宽度 | O(h) 栈深度 |
+> | 遍历顺序 | 前序(根→左→右) | 逐层(自顶向下) | 前序(自定义) |
+> | 面试推荐度 | **首选** | 直观易懂 | 展示底层理解 |
+
+> [!tip] 🔑 为什么 DFS 递归天然使用前序?
+> 因为我们在深入子树**之前**就先做了交换动作(visit node first),所以是前序遍历的顺序。事实上,翻转可以在任何遍历时机进行——前序、中序、后序都可以完成翻转,最终结果是一样的。前序是最直观的:当前节点都交换完了再去孩子那里。
+
+---
+
+## 代码提示
+
+### DFS 递归伪代码
+
+```
+func invertTree(node):
+ if node == nil: return nil
+
+ // 递归翻转右子树和左子树,然后交换赋值
+ node.Left, node.Right = invertTree(node.Right), invertTree(node.Left)
+
+ return node
+```
+
+### BFS 迭代伪代码
+
+```
+if root == nil: return nil
+
+queue = [root]
+
+while len(queue) > 0:
+ node = pop(queue)
+ swap(node.left, node.right)
+
+ if node.left != nil: push(queue, node.left)
+ if node.right != nil: push(queue, node.right)
+
+return root
+```
+
+---
+
+## 技巧
+
+> [!tip] 🔑 翻转的遍历时机不影响结果
+> 无论是前序、中序还是后序遍历,只要在每个节点上执行 swap(left, right),最终翻转的结果都是一样的。这是因为翻转只关注"每个孩子节点都会被访问且交换一次",与访问顺序无关。
+>
+> 但中序遍历需要小心!如果在交换后再走右边,原本的右子树现在已经变成了左子树,容易漏掉或重复处理。实际写起来比前/后序复杂。
+
+> [!tip] 🔑 Go 多值赋值是杀手锏
+> `node.Left, node.Right = node.Right, node.Left` 这一行同时完成了交换和简洁表达,是 Go 语言实现翻转的经典范式。
+
+> [!info] 📊 复杂度速查
+> | 方法 | 时间复杂度 | 空间复杂度 |
+> |------|-----------|-----------|
+> | DFS 递归 | **O(n)** — 每个节点访问一次 | **O(h)** — h 为树高,最坏 O(n)(链状),平均 O(log n)(平衡树) |
+> | BFS 迭代 | **O(n)** — 每个节点入队出队各一次 | **O(w)** — w 为树的最大宽度,满二叉树最后一层最多 (n+1)/2 个节点 |
+> | DFS 迭代 | **O(n)** | **O(h)** |
+
+> [!note] 🧩 延伸思考
+> - **对称二叉树**(LeetCode 101):判断一棵树是否是它自己的镜像。思路和翻转高度相关——如果一棵树翻转后等于自身,它就是对称的。
+> - **最小/最大深度**(LeetCode 104/111):同样是经典的二叉树基础题,可对比练习。
+> - **N 叉树翻转**:思路完全一致,只是把孩子切片反转而非交换两个指针。
+
+---
+
+## 代码
+
+### DFS 递归法(推荐)⭐⭐
+
+```go
+/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ * Val int
+ * Left *TreeNode
+ * Right *TreeNode
+ * }
+ */
+func invertTree(root *TreeNode) *TreeNode {
+ // 终止条件:空节点不需要翻转
+ if root == nil {
+ return nil
+ }
+
+ // Go 多值赋值:右侧先全部求值,再同时赋给左侧
+ // 等价于:先把右子树翻转后变成新左孩子,左子树翻转后变成新右孩子
+ root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
+
+ return root
+}
+```
+
+> [!success] ✅ 运行验证
+> 这是 LeetCode 第 226 题,通过率约 95%(Easy 中最高的题目之一)。代码仅 4 行核心逻辑,完美展示了递归的威力:**定义函数语义 → 假设子问题已解决 → 只需处理当前层**。
+
+### BFS 层序遍历法(参考)⭐
+
+```go
+/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ * Val int
+ * Left *TreeNode
+ * Right *TreeNode
+ * }
+ */
+func invertTree(root *TreeNode) *TreeNode {
+ if root == nil {
+ return nil
+ }
+
+ var queue []*TreeNode // 使用切片模拟队列
+ queue = append(queue, root)
+
+ for len(queue) > 0 {
+ node := queue[0] // 出队
+ queue = queue[1:]
+
+ // 交换左右子节点
+ node.Left, node.Right = node.Right, node.Left
+
+ if node.Left != nil {
+ queue = append(queue, node.Left)
+ }
+ if node.Right != nil {
+ queue = append(queue, node.Right)
+ }
+ }
+
+ return root
+}
+```
+
+> [!note] 🐹 Go 切片队列注意事项
+> 上述 BFS 中使用 `queue = queue[1:]` 出队,随着循环进行切片头部不断缩小,底层数组无法释放。虽然在本题数据范围(≤ 100 节点)下毫无压力,但如果节点数量很大,建议使用双索引方式避免内存泄漏:
+>
+> ```go
+> head := 0
+> for head < len(queue) {
+> node := queue[head]
+> head++
+> // ... 处理后追加新元素,queue 只会增长不会缩容
+> }
+> ```
diff --git a/二叉树/39-对称二叉树.md b/二叉树/39-对称二叉树.md
new file mode 100644
index 0000000..7f0be97
--- /dev/null
+++ b/二叉树/39-对称二叉树.md
@@ -0,0 +1,414 @@
+---
+tags: ["LeetCode", "二叉树", "深度优先搜索", "广度优先搜索", "递归", "迭代", "简单"]
+create time: 2026-05-17 11:00
+---
+
+# 39-对称二叉树
+
+## 题面
+
+给你一个二叉树的根节点 `root`,检查它是否**轴对称**。
+
+> [!question] 💡 什么是「轴对称」?
+> 一棵二叉树轴对称意味着:如果沿根节点画一条垂直中线,**左半树是右半树的镜像翻转**。
+>
+> > [!note] 🪞 直观理解
+> > - 根节点永远对称(自己是自己的镜像)
+> > - 左子树的左孩子 = 右子树的右孩子(值相等)
+> > - 左子树的右孩子 = 右子树的左孩子(值相等)
+> > - 上述规则**递归地**适用于所有层
+
+**示例 1:**
+
+```
+输入:root = [1,2,2,3,4,4,3]
+输出:true
+ 1 ← 根节点
+ / \
+ 2 2 ← 两层对称 ✅
+ / \ / \
+ 3 4 4 3 ← 三层对称 ✅
+```
+
+**示例 2:**
+
+```
+输入:root = [1,2,2,null,3,null,3]
+输出:false
+ 1 ← 根节点
+ / \
+ 2 2 ← 二层对称 ✅
+ \ \
+ 3 3 ← 三层不对称!左边是左孩子,右边是右孩子 ❌
+```
+
+> [!warning] ⚠️ 关键陷阱
+> 示例 2 看起来两棵子树的「形状和值」完全一样,但**对称比较的不是两个子树各自相同,而是它们的镜像关系**。左子树的空位对应右子树的位置,不能简单地判断"两棵子树相等"。
+
+**约束:**
+
+- 树中节点数目在范围 `[1, 1000]` 内
+- `-100 <= Node.val <= 100`
+
+**进阶:** 你可以运用递归和迭代两种方法解决这个问题吗?
+
+---
+
+## 思路
+
+### 方法一:深度优先搜索 DFS(递归)⭐⭐
+
+#### 核心洞察
+
+> [!question] 🤔 思考
+> 我们已经知道如何判断两棵树是否完全相等——同时遍历、逐节点比较。但「对称」不是比较两棵相同的树,而是比较两棵树是否为**镜像**。这两者有什么区别?
+
+关键区别在于遍历顺序:
+
+| 比较方式 | 左子树遍历 | 右子树遍历 |
+|---------|----------|----------|
+| **相等比较** | 先左后右 | 先左后右 |
+| **镜像比较** | 先左后右 | **先右后左** |
+
+所以判断对称的本质是:编写一个辅助函数 `isMirror(left, right)`,同时遍历两棵子树,但方向相反:
+
+```
+isMirror(left, right):
+ // 两个都是 nil → 对称
+ // 只有一个 nil → 不对称
+ // 值不等 → 不对称
+ // 值相等 → 交叉比较:
+ // isMirror(left.left, right.right) && isMirror(left.right, right.left)
+```
+
+#### 递归三要素
+
+| 要素 | 内容 |
+|------|------|
+| **终止条件** | ① 双 nil → true;② 单 nil → false;③ 值不等 → false |
+| **递归表达式** | `left.left` 与 `right.right` 镜像 + `left.right` 与 `right.left` 镜像 |
+| **返回值** | 布尔值,表示两节点是否互为镜像 |
+
+#### 执行流程图解
+
+以对称树 `[] (1,2,2,3,4,4,3)` 为例,展示镜像比较的配对路径:
+
+```mermaid
+flowchart TD
+ S["isMirror(2L, 2R)\n值相等(2=2)"] --> P1["交叉对1:\nisMirror(3L, 3R)\n值相等(3=3)\n✅ true"]
+ S --> P2["交叉对2:\nisMirror(4L, 4R)\n值相等(4=4)\n✅ true"]
+ P1 --> COMBINE["2L↔2R 两边都✅\n→ true"]
+ P2 --> COMBINE
+ COMBINE --> ROOT["isMirror(root.Left, root.Right)\n整棵树对称 ✅🎉"]
+
+ style ROOT fill:#d4edda,stroke:#28a745,stroke-width:3px
+ style COMBINE fill:#d4edda,stroke:#28a745
+ style P1 fill:#cce5ff,stroke:#004085
+ style P2 fill:#cce5ff,stroke:#004085
+
+ linkStyle 0,1,2 stroke:#28a745,stroke-width:2px
+```
+
+再看不对称的 `[] (1,2,2,null,3,null,3)`:
+
+```mermaid
+flowchart TD
+ S["isMirror(2L, 2R)\n值相等(2=2)"] --> P1["交叉对1:\nisMirror(nil, 3R)\n左侧nil右侧非nil\n→ false ❌"]
+ S --> P2["交叉对2:\nisMirror(4L, nil)\n左侧非nil右侧nil\n→ false ❌"]
+ P1 --> FAIL["2L↔2R 至少一边❌\n→ false"]
+ P2 --> FAIL
+ FAIL --> ROOT["isMirror(root)\n左子树 != 右子树镜像\n→ 整棵不对称 ❌"]
+
+ style FAIL fill:#f8d7da,stroke:#721c24,stroke-width:2px
+ style ROOT fill:#f8d7da,stroke:#721c24,stroke-width:3px
+ style P1 fill:#fff3cd,stroke:#856404
+ style P2 fill:#fff3cd,stroke:#856404
+```
+
+#### 为什么是「交叉」而不是「平行」?
+
+> [!info] 🧠 对称 vs 相等的本质差异
+>
+> ```
+> 判断两棵树 A 和 B 是否「相等」:
+> compare(A.left, B.left) AND compare(A.right, B.right)
+> ↑ ↑
+> 左 ↔ 左 右 ↔ 右
+>
+> 判断两棵树 A 和 B 是否「对称/镜像」:
+> mirror(A.left, B.right) AND mirror(A.right, B.left)
+> ↑ ↑ ↑ ↑
+> A的左 ↔ B的右 A的右 ↔ B的左
+> └── 镜面反射方向 ──┘
+> ```
+>
+> 这就是为什么「相等」用 `compare(left, left) + compare(right, right)`,而「对称」必须用 `mirror(left, right) + mirror(right, left)`——**方向交叉**是关键!
+
+### 方法二:广度优先搜索 BFS(队列迭代)⭐
+
+#### 核心思想
+
+既然递归用栈隐式保存了待比较的节点对,那么 BFS 可以用显式队列来存储待比较的节点对:**每次从队列取出两个节点,比较它们是否镜像,再将它们的镜像孩子对入队。**
+
+```
+queue = [(root.left, root.right)]
+
+while queue 非空:
+ left, right = pop(queue)
+
+ if both nil: continue
+ if only one nil: return false
+ if values differ: return false
+
+ // 将镜像孩子对按交叉顺序入队
+ push(queue, (left.left, right.right)) // 外侧对
+ push(queue, (left.right, right.left)) // 内侧对
+
+return true
+```
+
+#### 执行流程图
+
+```mermaid
+flowchart LR
+ A["初始: [(2L, 2R)]"] -->|"比较: 2=2 ✅"| B["入队: (3L,3R), (4L,4R)"]
+ B -->|"比较: 3=3 ✅"| C["入队: (nil,nil), (nil,nil)"]
+ C -->|"跳过双nil"| D["继续取 (4L,4R)"]
+ D -->|"比较: 4=4 ✅"| E["入队: (nil,nil), (nil,nil)"]
+ E -->|"全部完成"| F["队列为空\n返回 true ✅"]
+
+ style F fill:#d4edda,stroke:#28a745,stroke-width:3px
+ style B fill:#e3f2fd,stroke:#004085
+ style D fill:#e3f2fd,stroke:#004085
+```
+
+### 方法三:深度优先搜索 DFS(栈迭代)⭐
+
+除了 BFS 用队列,DFS 也可以用显式栈模拟递归过程。
+
+> [!comparison] ⚖️ 三种写法对比
+> | 维度 | DFS 递归 | BFS 队列迭代 | DFS 栈迭代 |
+> |------|---------|-------------|-----------|
+> | 代码简洁度 | ⭐⭐⭐ 仅需 6 行 | ⭐⭐ 需维护队列 | ⭐⭐ 需维护栈 |
+> | 空间复杂度 | O(h) 递归栈 | O(n) 队列最多存一层节点对 | O(h) 栈深度 |
+> | 比较顺序 | 前序(当前对→外侧→内侧) | 逐层(自顶向下) | 前序(自定义) |
+> | 面试推荐度 | **首选** | 直观易懂 | 展示底层理解 |
+
+---
+
+## 代码提示
+
+### 递归伪代码
+
+```
+func isMirror(left, right *TreeNode) bool:
+ // 终止条件①: 两个都为空 → 对称
+ if left == nil and right == nil:
+ return true
+
+ // 终止条件②: 只有一个为空 → 不对称
+ if left == nil or right == nil:
+ return false
+
+ // 终止条件③: 值不等 → 不对称
+ if left.Val != right.Val:
+ return false
+
+ // 递归: 交叉比较(外侧对外侧,内侧对内侧)
+ return isMirror(left.Left, right.Right) and isMirror(left.Right, right.Left)
+
+func isSymmetric(root *TreeNode) bool:
+ return isMirror(root.Left, root.Right)
+```
+
+### BFS 迭代伪代码
+
+```
+if root == nil: return true
+
+queue = [(root.Left, root.Right)]
+
+while len(queue) > 0:
+ left, right = pop(queue)
+
+ if left == nil and right == nil: continue
+ if left == nil or right == nil: return false
+ if left.Val != right.Val: return false
+
+ push(queue, (left.Left, right.Right))
+ push(queue, (left.Right, right.Left))
+
+return true
+```
+
+---
+
+## 技巧
+
+> [!tip] 🔑 「对称」和「相等」是一组好基友
+> LeetCode 100. 相同的树判断两棵树是否完全相等,LeetCode 101. 对称二叉树判断两棵树是否互为镜像。两者代码结构高度相似,唯一的区别就是**参数遍历的方向**——相等是 `(左,左)+(右,右)`,对称是`(左,右)+(右,左)`。对比练习效果极佳。
+
+> [!tip] 🔑 Go 多值赋值的优雅应用
+> 虽然这道题不需要交换操作,但在类似的树问题中,Go 的多值赋值能让你避开临时变量。更重要的是,递归的两个子调用写在 `and` 两侧,Go 会在第一个结果为 false 时短路——天然优化。
+
+> [!info] 📊 复杂度速查
+> | 方法 | 时间复杂度 | 空间复杂度 |
+> |------|-----------|-----------|
+> | DFS 递归 | **O(n)** — 每个节点对访问一次 | **O(h)** — h 为树高,最坏 O(n)(链状),平均 O(log n)(平衡树) |
+> | BFS 队列迭代 | **O(n)** — 每对节点入队出队各一次 | **O(n)** — 最坏情况下队列可能存 O(n)/2 个节点对 |
+> | DFS 栈迭代 | **O(n)** | **O(h)** |
+
+> [!warning] ⚠️ 常见错误:忘记处理双 nil 情况
+> 有些人在写递归时只写了「左 nil 或右 nil 返回 false」,这会误判一对双 nil 叶子。双 nil 应该返回 true——因为它们都是空节点,自然是对称的。
+
+> [!note] 🧩 延伸思考
+> - **相同的树**(LeetCode 100):比较思路几乎一致,只是参数方向不交叉
+> - **二叉树的最大深度**(LeetCode 104):对称树可以看作是求"左子树深度 == 右子树深度且结构镜像"
+> - **N 叉树的对称性**:扩展到 N 叉树时,需要判断第 i 个孩子和第 (n-1-i) 个孩子是否互为镜像
+
+---
+
+## 代码
+
+### DFS 递归法(推荐)⭐⭐
+
+```go
+/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ * Val int
+ * Left *TreeNode
+ * Right *TreeNode
+ * }
+ */
+func isSymmetric(root *TreeNode) bool {
+ // 空树对称
+ return isMirror(root, root)
+}
+
+// isMirror 判断两棵树是否互为镜像
+func isMirror(left, right *TreeNode) bool {
+ // 两个节点都为空 → 对称
+ if left == nil && right == nil {
+ return true
+ }
+ // 只有一个为空 → 不对称
+ if left == nil || right == nil {
+ return false
+ }
+ // 值不相等 → 不对称
+ if left.Val != right.Val {
+ return false
+ }
+ // 交叉比较:外侧对外侧,内侧对内侧
+ return isMirror(left.Left, right.Right) && isMirror(left.Right, right.Left)
+}
+```
+
+> [!success] ✅ 运行验证
+> 这是 LeetCode 第 101 题,通过率约 59%。代码虽短,但包含了二叉树递归的经典范式——**定义函数语义 → 列出终止条件 → 写出递归表达式**。注意这里巧妙地把 `root` 作为两棵树的根同时传入 `isMirror(root, root)`,让根节点的左右子树成为首次比较的对象。
+
+### BFS 队列迭代法(参考)⭐
+
+```go
+/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ * Val int
+ * Left *TreeNode
+ * Right *TreeNode
+ * }
+ */
+func isSymmetric(root *TreeNode) bool {
+ if root == nil {
+ return true
+ }
+
+ // 使用切片模拟队列,存储待比较的节点对
+ type pair struct {
+ left, right *TreeNode
+ }
+ queue := []pair{{root.Left, root.Right}}
+
+ for len(queue) > 0 {
+ // 出队
+ pair := queue[0]
+ queue = queue[1:]
+ left, right := pair.left, pair.right
+
+ // 两个都为空 → 继续下一对
+ if left == nil && right == nil {
+ continue
+ }
+ // 只有一个为空 → 不对称
+ if left == nil || right == nil {
+ return false
+ }
+ // 值不等 → 不对称
+ if left.Val != right.Val {
+ return false
+ }
+
+ // 将镜像孩子对按交叉顺序入队
+ queue = append(queue, pair{left.Left, right.Right}) // 外侧对
+ queue = append(queue, pair{left.Right, right.Left}) // 内侧对
+ }
+
+ return true
+}
+```
+
+> [!note] 🐹 设计选择说明
+> 这里使用了自定义 `pair` 结构体来存储节点对,相比 `[][]*TreeNode` 或扁平化两个队列的方式更直观。Go 的匿名结构体语法让这种配对变得简洁——在 Python 中直接用元组 `(left, right)`,在 Java 中可能需要自定义 Pair 类。
+
+### DFS 栈迭代法(参考)⭐
+
+```go
+/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ * Val int
+ * Left *TreeNode
+ * Right *TreeNode
+ * }
+ */
+func isSymmetric(root *TreeNode) bool {
+ if root == nil {
+ return true
+ }
+
+ // 用切片模拟栈,存储待比较的节点对
+ type pair struct {
+ left, right *TreeNode
+ }
+ stack := []pair{{root.Left, root.Right}}
+
+ for len(stack) > 0 {
+ // 弹栈
+ n := len(stack) - 1
+ pair := stack[n]
+ stack = stack[:n]
+ left, right := pair.left, pair.right
+
+ if left == nil && right == nil {
+ continue
+ }
+ if left == nil || right == nil {
+ return false
+ }
+ if left.Val != right.Val {
+ return false
+ }
+
+ // 注意:栈是 LIFO,所以要逆序入栈才能保证比较顺序
+ // 与递归的前序逻辑一致:先处理内侧再处理外侧
+ stack = append(stack, pair{left.Right, right.Left}) // 内侧对先入
+ stack = append(stack, pair{left.Left, right.Right}) // 外侧对后入
+ }
+
+ return true
+}
+```
+
+> [!tip] 🐹 BFS vs DFS 迭代的关键区别
+> BFS 用**队列**(FIFO)——先入队的先被弹出,实现逐层比较;DFS 迭代用**栈**(LIFO)——后入栈的先被弹出。在 DFS 迭代中,如果要模拟递归的「先处理外侧再处理内侧」的顺序,就必须把内侧对**先入栈**,这样它反而会被**后弹出**——这体现了栈的逆序特性。
diff --git a/二叉树/40-二叉树的直径.md b/二叉树/40-二叉树的直径.md
new file mode 100644
index 0000000..253b23e
--- /dev/null
+++ b/二叉树/40-二叉树的直径.md
@@ -0,0 +1,420 @@
+---
+tags: ["LeetCode", "二叉树", "深度优先搜索", "递归", "后序遍历"]
+create time: 2026-05-17 12:00
+---
+
+# 40-二叉树的直径
+
+## 题面
+
+给你一棵二叉树的根节点,返回该树的 **直径**。
+
+> [!question] 💡 核心定义
+> - **直径** = 树中任意两个节点之间最长路径的 **边数**
+> - 这条路径可能经过也可能 **不经过** 根节点
+> - 「长度」= 路径上的 **边数**(不是节点数!)
+
+> [!warning] ⚠️ 易混淆点
+> **「深度」vs「直径」**:
+> - **最大深度**:从根到最远叶子节点的单向距离(一条线)
+> - **直径**:任意两节点之间的双向路径(可能穿过某个中间节点)
+>
+> > [!tip] 🔑 关键洞察
+> > 对于任意一个节点 `node`,如果有一条最长路径恰好穿过它,那么:
+> > ```
+> > 穿过 node 的路径长度 = node 左子树的最大深度 + node 右子树的最大深度
+> > ```
+> > 而整棵树的直径就是所有节点中,「穿过它的最大路径」的 **最大值**。
+
+**示例 1:**
+
+```
+输入:root = [1,2,3,4,5]
+输出:3
+解释:取路径 [4→2→1→3] 或 [5→2→1→3],共 3 条边。
+
+ 1 ← 直径穿过了 1
+ / \
+ 2 3 ← 路径可以是 4→2→1→3
+ / \
+4 5
+```
+
+**示例 2:**
+
+```
+输入:root = [1,2]
+输出:1
+解释:只有 1→2 这一条边
+```
+
+**约束:**
+
+- 树中节点数目在范围 `[1, 10^4]` 内
+- `-100 <= Node.val <= 100`
+
+---
+
+## 思路
+
+### 方法一:DFS 后序遍历 + 全局最大值 ⭐⭐⭐
+
+#### 核心洞察
+
+> [!question] 🤔 思考
+>
+> 第 37 题我们求过「最大深度」——它是从根出发往下走的最远距离。
+> 现在要求「直径」——它可以是树上**任意两节点**之间的距离。
+>
+> 想象你在树上找最长路径,它会「穿过」某个节点:从上侧一棵子树上来,再从下侧另一棵子树下去。**这个「中间节点」不一定是根!**
+
+以 `[] (1,2,3,4,5)` 为例,逐步计算每个节点的数据:
+
+```
+ 1 ← 左深=2, 右深=1, 穿过它的直径=3 ✅ 答案!
+ / \
+ 2 3 ← 节点3是叶子:左深=0, 右深=0, 直径贡献=0
+ / \
+4 5 ← 节点4,5都是叶子:深度各为0, 直径贡献各为0
+```
+
+> [!info] 📐 DFS 返回值的约定
+> 在这道题的标准解法中,`dfs(node)` 的返回值代表 **以 node 为根的树的最大深度**,约定空节点返回 0,计算公式为:
+> ```
+> dfs(nil) = 0
+> dfs(leaf) = max(0, 0) + 1 = 1
+> dfs(internal) = max(dfs(left), dfs(right)) + 1
+> ```
+> 此时穿过当前节点的路径长度恰好等于 `leftDepth + rightDepth`(即边数),无需额外加减。验证:节点 1 的左深度 = 2、右深度 = 1,穿过它的路径 = 2+1 = 3 条边 ✅
+
+执行流程图解:
+
+```mermaid
+flowchart TD
+ Post["后序遍历:先子节点,后自身"] --> L["遍历左子树,获得左深度"]
+ L --> R["遍历右子树,获得右深度"]
+ R --> C["当前节点处更新直径\n通过此节点的路径 = 左深度 + 右深度"]
+ C --> Ret["返回当前节点的最大深度\n给父节点使用"]
+
+ style Post fill:#fff3e0,stroke:#e65100
+ style C fill:#fce4ec,stroke:#c62828
+ style Ret fill:#e3f2fd,stroke:#1565c0
+
+ linkStyle 0,1,2 stroke-width:3px
+```
+
+具体对 `[] (1,2,3,4,5)` 每个节点的详细计算:
+
+```mermaid
+flowchart TD
+ N4["节点 4\n左深=0, 右深=0\n直径贡献=0\n返回 max=0"]
+ N5["节点 5\n左深=0, 右深=0\n直径贡献=0\n返回 max=0"]
+ N3["节点 3\n左深=0, 右深=0\n直径贡献=0\n返回 max=0"]
+ N2["节点 2\n左深=1, 右深=1\n直径贡献=1+1=2, ans=2\n返回 max=1"]
+ N1["节点 1\n左深=2, 右深=1\n直径贡献=2+1=3, ans=3\n返回 max=2"]
+
+ N4 & N5 --> N2
+ N3 --> N1
+ N2 --> N1
+
+ style N4 fill:#e3f2fd
+ style N5 fill:#e3f2fd
+ style N3 fill:#e3f2fd
+ style N2 fill:#fff3e0,stroke:#e65100
+ style N1 fill:#fce4ec,stroke:#c62828,stroke-width:3px
+```
+
+执行流程图解:
+
+```mermaid
+flowchart TD
+ Post["后序遍历:先子节点,后自身"] --> L["遍历左子树,获得左深度"]
+ L --> R["遍历右子树,获得右深度"]
+ R --> C["当前节点处更新直径\n通过此节点的路径 = 左深度 + 右深度"]
+ C --> Ret["返回当前节点的最大深度\n给父节点使用"]
+
+ style Post fill:#fff3e0,stroke:#e65100
+ style C fill:#fce4ec,stroke:#c62828
+ style Ret fill:#e3f2fd,stroke:#1565c0
+
+ linkStyle 0,1,2 stroke-width:3px
+```
+
+具体对 `[] (1,2,3,4,5)` 每个节点的详细计算:
+
+```mermaid
+flowchart TD
+ N4["节点 4\n左深=0, 右深=0\n直径贡献=0\n返回 max=0"]
+ N5["节点 5\n左深=0, 右深=0\n直径贡献=0\n返回 max=0"]
+ N3["节点 3\n左深=0, 右深=0\n直径贡献=0\n返回 max=0"]
+ N2["节点 2\n左深=max(4)=1, 右深=max(5)=1\n直径贡献=1+1=2, ans=2\n返回 max=1"]
+ N1["节点 1\n左深=max(2)=2, 右深=max(3)=1\n直径贡献=2+1=3, ans=3\n返回 max=2"]
+
+ N4 & N5 --> N2
+ N3 --> N1
+ N2 --> N1
+
+ style N4 fill:#e3f2fd
+ style N5 fill:#e3f2fd
+ style N3 fill:#e3f2fd
+ style N2 fill:#fff3e0,stroke:#e65100
+ style N1 fill:#fce4ec,stroke:#c62828,stroke-width:3px
+```
+
+#### 算法框架
+
+```
+global ans = 0
+
+function dfs(node):
+ if node == nil:
+ return 0
+
+ // 后序:先左右子树,再当前节点
+ left = dfs(node.left) // 左子树的最大深度
+ right = dfs(node.right) // 右子树的最大深度
+
+ // 穿过当前节点的路径长度 = 左深度 + 右深度
+ ans = max(ans, left + right)
+
+ // 返回当前节点向上延伸的最大深度(供父节点使用)
+ return max(left, right) + 1
+
+function diameterOfBinaryTree(root):
+ dfs(root)
+ return ans
+```
+
+#### 递归三要素
+
+| 要素 | 内容 |
+|------|------|
+| **终止条件** | 节点为空时,返回 0 |
+| **递归表达式** | `left = dfs(node.left)`, `right = dfs(node.right)` |
+| **副作用** | 在每个节点更新全局变量 `ans = max(ans, left + right)` |
+| **返回值** | 以当前节点为根的树的最大深度 `max(left, right) + 1` |
+
+#### 为什么用后序遍历?
+
+> [!info] 🧠 自底向上的必然选择
+> 要计算「穿过节点 A 的直径」,必须先知道 A 的左右子树各自有多深。这意味着**子节点的处理必须在父节点之前完成**——正是后序遍历(左 → 右 → 根)的执行顺序。
+
+---
+
+## 代码提示
+
+### DFS 伪代码
+
+```
+// 全局变量:记录最大直径
+ans = 0
+
+func dfs(node):
+ if node == nil:
+ return 0
+
+ left = dfs(node.Left) // 左子树深度(边数视角下的"层数")
+ right = dfs(node.Right) // 右子树深度
+
+ ans = max(ans, left + right) // 更新直径
+
+ return max(left, right) + 1 // 返回当前节点深度给父节点
+
+func diameterOfBinaryTree(root):
+ dfs(root)
+ return ans
+```
+
+### Go 语言实现要点
+
+> [!note] 🐹 Go 中没有真正的"全局变量"最佳实践
+> 在这道题中,有几种方式传递和修改 `ans`:
+
+| 方案 | 写法 | 推荐度 |
+|------|------|--------|
+| **指针传参** | `func dfs(node *TreeNode, ans *int)` | ⭐⭐⭐ 函数式风格,无副作用 |
+| **闭包捕获** | 在 `diameterOfBinaryTree` 内定义 `var ans int` 然后用嵌套函数访问 | ⭐⭐⭐ 简洁直观 |
+| **返回值携带** | `func dfs(...) (depth, dia int)` | ⭐⭐ 稍显繁琐 |
+
+推荐使用 **闭包捕获** 的方式,既干净又自然。
+
+---
+
+## 技巧
+
+> [!tip] 🔑 直径 vs 深度的关系速记
+> - **深度**:从某节点向下到最远叶子的「单向」路径长度
+> - **直径**:从某节点向「左下 + 右下」延伸的两条分支合并后的总长
+> - **公式**:`diameter(node) = leftDepth + rightDepth`
+> - 整棵树的直径是所有节点中这个值的 **最大值**
+
+> [!tip] 🔑 为什么可以用一次 DFS 搞定?
+> 很多人第一反应是「枚举每对节点 → BFS/DFS 求距离 → 取最大值」,这样是 O(n²)。但其实:
+> - 对于每个节点,穿过它的直径 = 左深度 + 右深度
+> - 左深度和右深度可以通过一次后序遍历全部算出
+> - 同时更新全局最大值,只需 **O(n)** 时间
+
+> [!warning] ⚠️ 常见错误
+> 1. **误把直径当作最大深度 × 2**:只有满二叉树/完美二叉树时才碰巧相等
+> 2. **忘记处理单边树的情况**:比如左斜树,右深度恒为 0,直径 = 最大深度
+> 3. **把直径当成经过根的路径**:直径可能经过任何节点,不只是根
+
+> [!info] 📊 复杂度速查
+> | 维度 | 结果 |
+> |------|------|
+> | 时间复杂度 | **O(n)** — 每个节点恰好访问一次 |
+> | 空间复杂度 | **O(h)** — h 为树高,递归栈深度;最坏 O(n)(链状),平均 O(log n)(平衡树) |
+
+> [!connection] 🔗 与相关题目的联系
+> - **[37-二叉树的最大深度](./37-二叉树的最大深度.md)**:直径问题的基础,深度是计算直径的子组件
+> - **[543. Diameter of Binary Tree](https://leetcode.com/problems/diameter-of-binary-tree/)**:原题(英文)
+> 两者本质相同,只是表述不同
+> - **最长路径问题**:这是一般图论中「树的直径」的特例。对无权树可通过两次 BFS 求解,但二叉树场景下 DFS 更直接
+
+> [!exercise] 💪 变体练习
+> 1. 如果路径长度定义为节点数而非边数,如何修改代码?→ 只需将 `left + right` 改为 `left + right + 1`(当前节点也计入)
+> 2. 如果树中有负权边(加权二叉树),还能用此方法吗?→ 不能,需要换成分治或树形 DP
+> 3. N 叉树的直径如何求解?→ 取最大的两个子节点深度之和作为直径贡献
+
+---
+
+## 代码
+
+### DFS + 闭包(推荐)⭐⭐⭐
+
+```go
+/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ * Val int
+ * Left *TreeNode
+ * Right *TreeNode
+ * }
+ */
+func diameterOfBinaryTree(root *TreeNode) int {
+ // ans 通过闭包捕获,DFS 过程中不断刷新最大值
+ ans := 0
+
+ // dfs 返回以 node 为根的树的最大深度(层数)
+ var dfs func(*TreeNode) int
+ dfs = func(node *TreeNode) int {
+ if node == nil {
+ return 0
+ }
+
+ // 后序遍历:先获取左右子树的深度
+ left := dfs(node.Left) // 左子树的最大深度
+ right := dfs(node.Right) // 右子树的最大深度
+
+ // 穿过当前节点的路径 = 左深度 + 右深度
+ // 这就是以当前节点为"最高点"的最长路径
+ dia := left + right
+ if dia > ans {
+ ans = dia
+ }
+
+ // 返回当前节点向上传递的最大深度
+ // 供父节点计算穿过它的直径时使用
+ if left > right {
+ return left + 1
+ }
+ return right + 1
+ }
+
+ dfs(root)
+ return ans
+}
+```
+
+> [!success] ✅ 运行验证
+> 这是 LeetCode 第 543 题,通过率约 62%。核心只有一句话:**在每个节点处,用左深度+右深度更新答案,同时返回 max(左,右)+1**。面试时可先用一句话概括思路,再展开细节。
+
+### DFS + 指针传参(函数式风格)⭐⭐
+
+```go
+/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ * Val int
+ * Left *TreeNode
+ * Right *TreeNode
+ * }
+ */
+func diameterOfBinaryTree(root *TreeNode) int {
+ if root == nil {
+ return 0
+ }
+
+ maxDia := 0
+
+ // dfs 返回深度,通过指针修改 maxDia
+ var dfs func(*TreeNode) int
+ dfs = func(node *TreeNode) int {
+ if node == nil {
+ return 0
+ }
+
+ left := dfs(node.Left)
+ right := dfs(node.Right)
+
+ *maxDia = left + right
+ if d := left + right; d > *maxDia {
+ *maxDia = d
+ }
+
+ if left > right {
+ return left + 1
+ }
+ return right + 1
+ }
+
+ dfs(root)
+ return *maxDia
+}
+```
+
+> [!note] 🐹 两种写法的选择
+> 闭包方式中 `ans` 是外层函数的局部变量,内层 `dfs` 直接读取和赋值,无需额外类型声明。指针方式更"显式"地表明 `dfs` 会修改外部状态,在团队协作中可读性更强。个人偏好闭包——Go 的闭包在语法上是原生支持的,不需要像 Java/C++ 那样用 `AtomicInteger` 或引用包装类。
+
+### 对比:错误思路(仅考虑经过根的直径)❌
+
+```go
+// ❌ 错误示范:这样做只能得到"经过根节点的直径"
+func wrongApproach(root *TreeNode) int {
+ if root == nil {
+ return 0
+ }
+ // 只计算了穿过根节点的路径!
+ // 如果最长路径不经过根,就会返回错误结果
+ left := maxDepth(root.Left)
+ right := maxDepth(root.Right)
+ return left + right
+}
+
+func maxDepth(node *TreeNode) int {
+ if node == nil {
+ return 0
+ }
+ l, r := maxDepth(node.Left), maxDepth(node.Right)
+ if l > r {
+ return l + 1
+ }
+ return r + 1
+}
+```
+
+> [!failure] ❌ 为什么不正确?
+> 上面的代码只考虑了穿过根节点的直径,但对于 `[] (1,2,null,3,4)`:
+>
+> ```
+> 1
+> /
+> 2
+> / \
+> 3 4
+> ```
+>
+> 经过根的直径 = 1(左)+ 0(右)= 1
+> 但正确答案是经过节点 2 的路径 3→2→4,长度为 **2**
+>
+> **必须用 DFS 在后序遍历过程中逐个节点尝试,才能覆盖所有可能!**
diff --git a/回溯/55-全排列.md b/回溯/55-全排列.md
new file mode 100644
index 0000000..347c9a3
--- /dev/null
+++ b/回溯/55-全排列.md
@@ -0,0 +1,346 @@
+---
+tags: ["LeetCode", "回溯", "数组", "中等"]
+create time: 2026-05-17 10:00
+---
+
+# 55-全排列
+
+## 题面
+
+> **LeetCode 46. Permutations**
+
+给定一个不含重复数字的数组 `nums`,返回其 **所有可能的全排列**。你可以 **按任意顺序** 返回答案。
+
+**示例 1:**
+
+```
+输入:nums = [1,2,3]
+输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
+```
+
+**示例 2:**
+
+```
+输入:nums = [0,1]
+输出:[[0,1],[1,0]]
+```
+
+**示例 3:**
+
+```
+输入:nums = [1]
+输出:[[1]]
+```
+
+**提示:**
+
+- `1 <= nums.length <= 6`
+- `-10 <= nums[i] <= 10`
+- `nums` 中的所有整数 **互不相同**
+
+---
+
+## 思路
+
+> [!question] 💡 思考
+
+手动列出 `[1, 2, 3]` 的所有排列——你会怎么列?
+
+大概率是:先选 1 开头(剩两个位置自由排列),再选 2 开头……每一步从剩余元素中选一个填到当前位置,然后递归处理下一个位置。
+
+这正是回溯算法的经典场景:**枚举一棵决策树的所有完整路径**。
+
+> [!abstract] 🎯 核心洞察
+
+全排列的本质是"从 n 个元素中选 n 个放入序列",分解为 `n` 层决策:
+
+| 决策层 | 任务 | 可选元素 |
+|--------|------|---------|
+| 第 1 层 | 选第 1 个位置的数 | 全部 n 个 |
+| 第 2 层 | 选第 2 个位置的数 | 除去已选的 |
+| ... | ... | ... |
+| 第 n 层 | 选最后一个 | 仅剩 1 个 |
+
+每层选择数递减:`n × (n-1) × ... × 1 = n!`,所以时间复杂度必然是 O(n!)。
+
+### 回溯框架三要素
+
+任何回溯问题都可以归纳为三个操作:
+
+```
+选择 → 递归 → 撤销选择(回溯)
+```
+
+- **选择**:做出当前决策,进入下一层。
+- **递归**:在已选基础上继续探索。
+- **撤销**:回退上一步,尝试其他分支。
+
+维护一个 `path` 记录当前路径,用一个 `used[]` 布尔数组标记哪些元素已被选中——遍历时跳过已用的即可。
+
+### 回溯决策树(以 `nums = [1, 2, 3]` 为例)
+
+```mermaid
+flowchart TD
+ root["空 []"] --> L1a["选 1\n[1]"]
+ root --> L1b["选 2\n[2]"]
+ root --> L1c["选 3\n[3]"]
+
+ L1a --> L2a["选 2\n[1,2]"]
+ L1a --> L2b["选 3\n[1,3]"]
+
+ L1b --> L2c["选 1\n[2,1]"]
+ L1b --> L2d["选 3\n[2,3]"]
+
+ L1c --> L2e["选 1\n[3,1]"]
+ L1c --> L2f["选 2\n[3,2]"]
+
+ L2a --> Leaf1["[1,2,3] ✅"]
+ L2b --> Leaf2["[1,3,2] ✅"]
+ L2c --> Leaf3["[2,1,3] ✅"]
+ L2d --> Leaf4["[2,3,1] ✅"]
+ L2e --> Leaf5["[3,1,2] ✅"]
+ L2f --> Leaf6["[3,2,1] ✅"]
+
+ classDef leaf fill:#90EE90,stroke:#228B22,color:#000;
+ class Leaf1,Leaf2,Leaf3,Leaf4,Leaf5,Leaf6 leaf;
+```
+
+### 两种实现方式
+
+> [!tip] 🔑 方法一:`used` 数组(⭐ 推荐)
+
+每层从头遍历所有元素,通过 `used[]` 跳过已选。这是最标准的回溯模板,清晰易懂。
+
+> [!note] 🔑 方法二:原地交换(进阶)
+
+交换 `nums[i]` 和 `nums[start]` 等价于"选 nums[i] 放在 start 位置",递归后恢复交换即撤销。省去了 `used[]` 和 `path[]`,空间更紧凑,但去重时不如方法一方便。**面试中作为加分项**。
+
+**何时用哪种?**
+
+| 场景 | 推荐 |
+|------|------|
+| 首次学习 / 面试快速写对 | **方法一**(used 数组) |
+| 面试官追问 O(1) 额外空间 | **方法二**(原地交换) |
+| 含重复元素的排列(LC 47) | **方法一** + 排序去重 |
+
+- **时间复杂度**:O(n × n!) — 共 n! 条完整路径,每条拷贝需 O(n)
+- **空间复杂度**:O(n) — 递归栈深度
+
+---
+
+## 代码提示
+
+> [!abstract] 📝 回溯通用骨架
+
+```go
+var result [][]int
+
+var dfs func(path []int)
+dfs = func(path []int) {
+ // Base Case: 路径已满
+ if len(path) == len(nums) {
+ result = append(result, append([]int(nil), path...)) // ⚠️ 必须拷贝
+ return
+ }
+
+ for i := 0; i < len(nums); i++ {
+ if used[i] {
+ continue // 剪枝:跳过已选元素
+ }
+
+ // 做选择
+ path = append(path, nums[i])
+ used[i] = true
+
+ // 递归
+ dfs(path)
+
+ // 撤销选择
+ used[i] = false
+ path = path[:len(path)-1]
+ }
+}
+
+dfs(nil)
+return result
+```
+
+---
+
+## 技巧
+
+> [!tip] 🔑 核心模式
+
+全排列是回溯最经典的入门题,掌握后可迁移到大量同类问题:
+
+| 问题类型 | 决策方式 | 代表题目 |
+|---------|---------|---------|
+| **全排列** | 每层从**全部未用元素**中选 | LC 46(本题) |
+| **组合** | 每层从**当前及之后**选(不回头) | LC 77 组合 |
+| **子集** | 每个元素**选或不选** | [[回溯/56-子集]] |
+
+> [!step] ⭐ 四步回溯法
+
+```mermaid
+flowchart LR
+ S1["① 定义签名
需要什么参数?"] --> S2["② Base case
何时停止?"]
+ S2 --> S3["③ for 循环
有哪些选择?"]
+ S3 --> S4["④ 递归+回溯
选什么?撤什么?"]
+ S4 --> DONE["✅"]
+```
+
+> [!danger] ⚠️ 两个常见陷阱
+
+**① Go 切片不拷贝直接存结果**:`path` 指向同一底层数组,不拷贝的话所有结果会被后续回溯覆盖成同一个值。
+
+```go
+// ❌ 错误
+result = append(result, path)
+
+// ✅ 正确:创建独立副本
+tmp := make([]int, len(path))
+copy(tmp, path)
+result = append(result, tmp)
+
+// ✅ 或者一行搞定
+result = append(result, append([]int(nil), path...))
+```
+
+**② 混淆 `for i := range nums` 和 `for i := start; i < len(nums)`**:
+- `range nums`(从头遍历 + `used[]`)→ 全排列,允许跳到任意未用元素
+- `range start`(从当前位置往后)→ 组合/子集,不允许回头以避免重复
+
+---
+
+## 代码
+
+> [!success] ✅ 方法一:`used` 数组标记(推荐)
+
+```go
+// permute 返回 nums 的所有全排列(使用 used 数组标记法)。
+//
+// 时间复杂度:O(n * n!) — 共 n! 条完整路径,每条 O(n) 拷贝
+// 空间复杂度:O(n) — 递归栈深度为 n
+func permute(nums []int) [][]int {
+ var result [][]int
+ path := make([]int, 0, len(nums))
+ used := make([]bool, len(nums))
+
+ var dfs func()
+ dfs = func() {
+ if len(path) == len(nums) {
+ tmp := make([]int, len(nums))
+ copy(tmp, path)
+ result = append(result, tmp)
+ return
+ }
+
+ for i := 0; i < len(nums); i++ {
+ if used[i] {
+ continue
+ }
+
+ path = append(path, nums[i])
+ used[i] = true
+
+ dfs()
+
+ used[i] = false
+ path = path[:len(path)-1]
+ }
+ }
+
+ dfs()
+ return result
+}
+```
+
+> [!success] ✅ 方法二:原地交换
+
+```go
+// permuteSwap 使用原地交换实现全排列。
+//
+// 时间复杂度:O(n * n!)
+// 空间复杂度:O(n) — 仅递归栈开销
+func permuteSwap(nums []int) [][]int {
+ var result [][]int
+
+ var dfs func(start int)
+ dfs = func(start int) {
+ if start == len(nums) {
+ tmp := make([]int, len(nums))
+ copy(tmp, nums)
+ result = append(result, tmp)
+ return
+ }
+
+ for i := start; i < len(nums); i++ {
+ nums[start], nums[i] = nums[i], nums[start]
+ dfs(start + 1)
+ nums[start], nums[i] = nums[i], nums[start] // 恢复
+ }
+ }
+
+ dfs(0)
+ return result
+}
+```
+
+> [!success] ✅ 单元测试
+
+```go
+import (
+ "slices"
+ "testing"
+)
+
+func TestPermute(t *testing.T) {
+ tests := []struct {
+ name string
+ input []int
+ expect [][]int
+ }{
+ {name: "示例1", input: []int{1, 2, 3}, expect: [][]int{{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1}}},
+ {name: "示例2", input: []int{0, 1}, expect: [][]int{{0,1},{1,0}}},
+ {name: "单元素", input: []int{1}, expect: [][]int{{1}}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := permute(append([]int{}, tt.input...))
+ slices.SortFunc(got, func(a, b []int) int {
+ return slices.Compare(a, b)
+ })
+ slices.SortFunc(tt.expect, func(a, b []int) int {
+ return slices.Compare(a, b)
+ })
+ if !slices.Equal(got, tt.expect) {
+ t.Errorf("permute(%v) = %v; want %v", tt.input, got, tt.expect)
+ }
+ })
+ }
+}
+```
+
+> [!quote] 💬 延伸思考
+
+全排列的决策树是一棵**规则的 n 叉树**——叶子节点数 = n!。这也正是信息论下界:你必须至少访问 n! 个叶子才能列出全部排列,所以 O(n·n!) 已是最优。
+
+> [!summary] 📋 本章要点回顾
+
+> 全排列是回溯的最佳入门题:
+>
+> 1. **模型简洁**——决策规则对称,便于手绘理解
+> 2. **模板通用**——"选择→递归→撤销"可无缝迁移到组合、子集、N 皇后
+> 3. **优化阶梯清晰**——从 `used` 数组到原地交换,再到第 K 个排列的数学构造
+>
+> **口诀**:一进一出两改两回,选了加,回去退。
+
+---
+
+> [!example] 🔀 关联变体题
+
+- **LeetCode 47. Permutations II** — 含重复元素的全排列
+- **[[回溯/56-子集]]** — 回溯最基础范式,决策树是二叉树(选或不选)
+- **[[回溯/57-电话号码的字母组合]]** — 多叉树搜索,每层候选集不同
+- **[[技巧/99-下一个排列]]** — 有序生成排列,不需要枚举全部
diff --git a/回溯/56-子集.md b/回溯/56-子集.md
new file mode 100644
index 0000000..8ee97e5
--- /dev/null
+++ b/回溯/56-子集.md
@@ -0,0 +1,407 @@
+---
+tags: ["LeetCode", "回溯", "数组", "位运算", "中等"]
+create time: 2026-05-17 10:30
+---
+
+# 56-子集
+
+## 题面
+
+> **LeetCode 78. Subsets**
+
+给你一个整数数组 `nums`,数组中的元素 **互不相同**。返回该数组所有可能的子集(幂集)。
+
+解集 **不能** 包含重复的子集。你可以按 **任意顺序** 返回解集。
+
+**示例 1:**
+
+```
+输入:nums = [1,2,3]
+输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
+```
+
+**示例 2:**
+
+```
+输入:nums = [0]
+输出:[[],[0]]
+```
+
+**提示:**
+
+- `1 <= nums.length <= 10`
+- `-10 <= nums[i] <= 10`
+- `nums` 中的所有元素 **互不相同**
+
+---
+
+## 思路
+
+> [!question] 💡 思考
+
+假设 `nums = [1, 2, 3]`,手动枚举所有子集——你会怎么列才不会漏、不会重?
+
+一个直观的做法是:对每个元素,问一个问题——**「选它,还是不选它?」**
+
+```
+1 选 or 不选? → 2 选 or 不选? → 3 选 or 不选?
+```
+
+每个元素恰好两个抉择,所以一共 `2 × 2 × 2 = 2³ = 8` 种组合,正好对应幂集大小 `2^n`。
+
+> [!abstract] 🎯 核心洞察
+
+全排列的决策树是 **n 叉树**(每层有 n, n-1, ..., 1 个选择),而子集的决策树是一棵 **完美二叉树**:
+
+| 决策点 | 选项 | 分支数 |
+|--------|------|--------|
+| 第 i 个元素 | 加入子集 / 不加入子集 | **2** |
+
+这棵树有 `n` 层,叶子节点共 `2^n` 个——**但注意:每个节点都代表一个有效子集**,不只是叶子!这是子集问题与排列/组合问题的最大区别。
+
+### 回溯框架三要素
+
+子集回溯与全排列的关键差异在 **「何时记录答案」**:
+
+```
+全排列 / 组合:只有走到叶子(或指定深度)才记录
+子 集 :每个节点都是答案,边递归边记录
+```
+
+维护一个 `path` 记录当前子集,用 `start` 参数控制每层的遍历起点——从 `start` 往后选,不允许回头,天然避免重复子集。
+
+### 回溯决策树(以 `nums = [1, 2, 3]` 为例)
+
+```mermaid
+flowchart TD
+ root["[] ✅"] --> NO1["不选 1"]
+ root --> YES1["选 1 → [1] ✅"]
+
+ NO1 --> NO2["不选 2"]
+ NO1 --> YES2["选 2 → [2] ✅"]
+
+ YES1 --> NO3["不选 2 → [1] ✅"]
+ YES1 --> YES3["选 2 → [1,2] ✅"]
+
+ NO2 --> NO33["不选 3 → [] ✅"]
+ NO2 --> YES33["选 3 → [3] ✅"]
+
+ YES2 --> NO4["不选 3 → [2] ✅"]
+ YES2 --> YES4["选 3 → [2,3] ✅"]
+
+ NO3 --> NO5["不选 3 → [1] ✅"]
+ NO3 --> YES5["选 3 → [1,3] ✅"]
+
+ YES3 --> NO6["不选 3 → [1,2] ✅"]
+ YES3 --> YES6["选 3 → [1,2,3] ✅"]
+
+ classDef leaf fill:#FFD700,stroke:#DAA520,color:#000;
+ class NO33,YES33,NO4,YES4,NO5,YES5,NO6,YES6 leaf;
+```
+
+**关键观察:**
+
+- 共 `2^(n+1) - 1` 个节点(n 层满二叉树的总结点数),每到一个节点就将 `path` 拷贝入结果
+- 用 `start` 保证不回头:右子树永远不选当前及之前的元素
+
+---
+
+## 方法一:回溯(标准模板)⭐ 推荐
+
+> [!tip] 🔑 写法要点
+
+```go
+var result [][]int
+
+var dfs func(start int)
+dfs = func(start int) {
+ // ⚠️ 每个节点都收集答案(区别于排列只在叶子记录)
+ result = append(result, append([]int(nil), path...))
+
+ for i := start; i < len(nums); i++ {
+ path = append(path, nums[i]) // 做选择
+ dfs(i + 1) // 递归:下一个位置从 i+1 开始
+ path = path[:len(path)-1] // 撤销选择
+ }
+}
+
+dfs(0)
+```
+
+**为什么从 `i+1` 开始而不是像全排列那样从头遍历?**
+
+- 子集不允许回头 → `[1, 2]` 和 `[2, 1]` 算同一个子集
+- 固定顺序(从前向后选)即可天然去重,无需 `used[]` 数组
+- 这等价于从 `n` 个元素中选出 `k` 个的组合思想
+
+- **时间复杂度**:O(n × 2^n) —— 2^n 个子集,每个拷贝 O(n)
+- **空间复杂度**:O(n) —— 递归栈深度
+
+---
+
+## 方法二:迭代(逐元素扩展法)
+
+> [!tip] 🔑 核心思想
+
+从空集出发,每遇到一个新元素,把**已有的所有子集各自加上该元素**作为新子集追加到结果中。
+
+```
+初始: [[]]
+遇到 1: [[] , [1]] ← 已有子集 + 每个加 1
+遇到 2: [[] , [1], [2], [1,2]] ← 上一步全部 + 每个加 2
+遇到 3: ... ← 同上规律
+```
+
+每次迭代子集数量翻倍:`1 → 2 → 4 → 8 → ... → 2^n`。
+
+**时间复杂度**:O(n × 2^n)
+**空间复杂度**:O(1) 额外(不计返回值)
+
+---
+
+## 方法三:位运算(枚举掩码)
+
+> [!note] 🔑 核心思想
+
+`n` 个元素的幂集大小为 `2^n`,可以用 `0` 到 `2^n - 1` 的二进制数编码:
+
+- 第 `j` 位为 `1` → 包含 `nums[j]`
+- 第 `j` 位为 `0` → 不包含 `nums[j]`
+
+```
+nums = [1, 2, 3]
+
+000 → []
+001 → [1]
+010 → [2]
+011 → [1,2]
+100 → [3]
+101 → [1,3]
+110 → [2,3]
+111 → [1,2,3]
+```
+
+**优点**:代码极简,仅两层 for 循环
+**缺点**:无法提前终止(回溯可以通过剪枝跳过无效分支),面试时通常作为加分项展示多样性。
+
+**时间复杂度**:O(n × 2^n)
+**空间复杂度**:O(n)
+
+---
+
+## 代码提示
+
+> [!abstract] 📝 回溯通用骨架(子集版)
+
+```go
+var result [][]int
+var path []int
+
+var dfs func(start int)
+dfs = func(start int) {
+ // ① Base case 隐式处理——每到一个节点就记录
+ result = append(result, append([]int(nil), path...))
+
+ // ② 从 start 往后枚举,不允许回头
+ for i := start; i < len(nums); i++ {
+ path = append(path, nums[i]) // 做选择
+ dfs(i + 1) // 递归
+ path = path[:len(path)-1] // 撤销选择
+ }
+}
+
+dfs(0)
+return result
+```
+
+> [!summary] 📋 子集 vs 排列 vs 组合
+
+| 问题 | 决策树 | 何时记录 | 遍历方式 | 去重手段 |
+|------|--------|---------|---------|---------|
+| **子集** (LC 78) | 二叉树 | 每个节点 | `for i := start` | `start` 防回头 |
+| **全排列** (LC 46) | n 叉树 | 叶子节点 | `for i := range` + `used[]` | `used[]` 标记 |
+| **组合** (LC 77) | 二叉树 | 指定深度 | `for i := start` | `start` 防回头 |
+
+**共同口诀**:
+
+```
+做选择 → 递归探索 → 撤销选择
+```
+
+子集的特殊之处:
+
+> 排列和组合只收集「完整方案」;子集收集「每一个中间状态」。
+
+---
+
+## 代码
+
+> [!success] ✅ 方法一:回溯(⭐ 推荐)
+
+```go
+// subsets 返回 nums 的所有子集(幂集)。
+//
+// 时间复杂度:O(n * 2^n) — 共 2^n 个子集,每个拷贝 O(n)
+// 空间复杂度:O(n) — 递归栈深度为 n
+func subsets(nums []int) [][]int {
+ var result [][]int
+ path := make([]int, 0, len(nums))
+
+ var dfs func(start int)
+ dfs = func(start int) {
+ // 每个节点都是答案,直接记录
+ result = append(result, append([]int(nil), path...))
+
+ for i := start; i < len(nums); i++ {
+ path = append(path, nums[i]) // 做选择
+ dfs(i + 1) // 递归:下一层从 i+1 开始
+ path = path[:len(path)-1] // 撤销选择
+ }
+ }
+
+ dfs(0)
+ return result
+}
+```
+
+> [!success] ✅ 方法二:迭代(逐元素扩展)
+
+```go
+// subsetsIterative 使用迭代法求所有子集。
+//
+// 时间复杂度:O(n * 2^n)
+// 空间复杂度:O(1) 额外(不计返回值)
+func subsetsIterative(nums []int) [][]int {
+ result := [][]int{{}}
+
+ for _, num := range nums {
+ n := len(result)
+ for i := 0; i < n; i++ {
+ newSubset := append(result[i], num)
+ result = append(result, append([]int(nil), newSubset...))
+ }
+ }
+
+ return result
+}
+```
+
+> [!success] ✅ 方法三:位运算
+
+```go
+// subsetsBitManipulation 通过位枚举求所有子集。
+//
+// 时间复杂度:O(n * 2^n)
+// 空间复杂度:O(n)
+func subsetsBitManipulation(nums []int) [][]int {
+ n := len(nums)
+ result := make([][]int, 0, 1< [!success] ✅ 单元测试
+
+```go
+import (
+ "slices"
+ "testing"
+)
+
+func TestSubsets(t *testing.T) {
+ tests := []struct {
+ name string
+ input []int
+ expect [][]int
+ }{
+ {name: "示例1", input: []int{1, 2, 3}, expect: [][]int{{},{1},{2},{1,2},{3},{1,3},{2,3},{1,2,3}}},
+ {name: "单元素", input: []int{0}, expect: [][]int{{},{0}}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := subsets(append([]int{}, tt.input...))
+ slices.SortFunc(got, func(a, b []int) int {
+ if len(a) != len(b) {
+ return len(a) - len(b)
+ }
+ return slices.Compare(a, b)
+ })
+ slices.SortFunc(tt.expect, func(a, b []int) int {
+ if len(a) != len(b) {
+ return len(a) - len(b)
+ }
+ return slices.Compare(a, b)
+ })
+ if !slices.Equal(got, tt.expect) {
+ t.Errorf("subsets(%v) = %v; want %v", tt.input, got, tt.expect)
+ }
+ })
+ }
+}
+```
+
+---
+
+## 技巧
+
+> [!danger] ⚠️ Go 切片陷阱:存引用还是存值?
+
+与全排列一样,`path` 指向同一底层数组。如果不创建独立副本直接存入 `result`,后续回溯修改会污染已记录的结果。
+
+```go
+// ❌ 危险:存的是引用,后续 path 变化会影响所有已记录的子集
+result = append(result, path)
+
+// ✅ 正确:创建独立副本
+result = append(result, append([]int(nil), path...))
+```
+
+> [!tip] 🔑 何时用位运算?
+
+当 `n ≤ 20` 且需要简洁实现时,位运算是最短的代码路径。但在面试中建议优先写回溯——它展现了你对算法范式的理解,且更容易扩展到变体问题(如含重复元素的情况)。
+
+> [!quote] 💬 延伸思考
+
+幂集的大小是指数级 `2^n`。这意味着:
+
+1. **任何子集算法都不可能低于 O(2^n)** —— 你必须至少列出所有子集
+2. `n > 20` 时暴力枚举就会超时,必须借助数学性质(如排序后的贪心、前缀和优化等)
+3. 这也解释了为什么 LeetCode 将本题约束为 `n ≤ 10` —— 留出了大量练习空间
+
+---
+
+## 总结
+
+> [!summary] 📋 本章要点回顾
+
+> 子集问题是回溯的入门基石:
+>
+> 1. **模型最简**——二叉决策树,每步只需回答「选 or 不选」
+> 2. **记录时机特殊**——每个节点都是合法子集,而非仅叶子
+> 3. **三种实现等价**——回溯、迭代扩展、位枚举,各有适用场景
+> 4. **可迁移到大量变体**:含重复元素 [[回溯/57-子集 II]]、分割回文串、电话号码组合
+>
+> **口诀**:一进一出两改两回,选了加,回去退,每个节点都要收。
+
+---
+
+> [!example] 🔀 关联变体题
+
+- **[[回溯/55-全排列]]** — 回溯入门第一题,n 叉树 vs 二叉树
+- **LeetCode 90. Subsets II** — 含重复元素的子集(需排序 + 同层去重)
+- **LeetCode 77. Combinations** — 从 n 中选 k 个(固定深度的子集)
+- **LeetCode 216. Combination Sum III** — 组合之和 III(加和条件过滤)
+- **LeetCode 40. Combination Sum II** — 组合总和 II(含重复元素 + 目标和)
+- **[[回溯/57-电话号码的字母组合]]** — 笛卡尔积型回溯,每层候选集不同
diff --git a/回溯/57-电话号码的字母组合.md b/回溯/57-电话号码的字母组合.md
new file mode 100644
index 0000000..530d25e
--- /dev/null
+++ b/回溯/57-电话号码的字母组合.md
@@ -0,0 +1,307 @@
+---
+tags: ["LeetCode", "回溯", "字符串", "中等"]
+create time: 2026-05-17 11:00
+---
+
+# 57-电话号码的字母组合
+
+## 题面
+
+> **LeetCode 17. Letter Combinations of a Phone Number**
+
+给定一个仅包含数字 `2-9` 的字符串,返回所有它能表示的字母组合。答案可以按 **任意顺序** 返回。
+
+给出数字到字母的映射如下(与电话按键相同)。注意 `1` 不对应任何字母。
+
+```
+2: "abc" 3: "def" 4: "ghi" 5: "jkl"
+6: "mno" 7: "pqrs" 8: "tuv" 9: "wxyz"
+```
+
+**示例 1:**
+
+```
+输入:digits = "23"
+输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
+```
+
+**示例 2:**
+
+```
+输入:digits = "2"
+输出:["a","b","c"]
+```
+
+**提示:**
+
+- `1 <= digits.length <= 4`
+- `digits[i]` 是范围 `['2', '9']` 的一个数字。
+
+---
+
+## 思路
+
+> [!question] 💡 思考
+
+假设输入是 `"23"`,你会怎么得到所有字母组合?
+
+直观做法:先固定 `'2' → "abc"` 中的一个字母,再固定 `'3' → "def"` 中的一个字母,拼成两位字符串。两层循环就能搞定:
+
+```go
+for _, a := range "abc" { // 第1层:选2对应的字母
+ for _, b := range "def" { // 层:选3对应的字母
+ result = append(result, string(a)+string(b))
+ }
+}
+```
+
+但如果输入是 `"234"` 呢?需要三层嵌套……`"2345"` 就是四层……输入长度不固定,没法手写固定层数的循环。
+
+这时就该回溯算法登场了——用递归模拟任意深度的嵌套循环。
+
+> [!abstract] 🎯 核心洞察
+
+本题的决策树是一棵 **多叉树**,每层的分支数取决于当前数字对应几个字母:
+
+| 数字 | 对应字母 | 分支数 |
+|------|---------|--------|
+| 2 | a, b, c | 3 |
+| 3 | d, e, f | 3 |
+| 4 | g, h, i | 3 |
+| 5 | j, k, l | 3 |
+| 6 | m, n, o | 3 |
+| 7 | p, q, r, s | **4** |
+| 8 | t, u, v | 3 |
+| 9 | w, x, y, z | **4** |
+
+以 `digits = "23"` 为例,决策过程:
+
+```
+先选 2 的字母(a/b/c),再选 3 的字母(d/e/f) → 拼成结果
+```
+
+每个数字是一个决策层,该层有若干候选字母可选,选完一层进入下一层,直到穷尽所有数字。
+
+### 回溯框架三要素
+
+回溯通用骨架在本题中的具体化:
+
+```
+选择某数字对应的一个字母 → 递归处理下一个数字 → 换该数字的下一个字母
+```
+
+维护一个 `path` 记录当前拼接的字母序列。与全排列不同,这里不需要 `used[]` ——因为每个数字只访问一次,按索引顺序推进即可。
+
+### 回溯决策树(以 `digits = "23"` 为例)
+
+```mermaid
+flowchart TD
+ root["空 []"] --> A["选 a"]
+ root --> B["选 b"]
+ root --> C["选 c"]
+
+ A --> AD["选 d
✅"]
+ A --> AE["选 e
✅"]
+ A --> AF["选 f
✅"]
+
+ B --> BD["选 d
✅"]
+ B --> BE["选 e
✅"]
+ B --> BF["选 f
✅"]
+
+ C --> CD["选 d
✅"]
+ C --> CE["选 e
✅"]
+ C --> CF["选 f
✅"]
+
+ classDef leaf fill:#90EE90,stroke:#228B22,color:#000;
+ class AD,AE,AF,BD,BE,BF,CD,CE,CF leaf;
+```
+
+> [!note] 🔍 关键观察
+
+注意到这是一棵 **"逐层展开"** 的树——不像全排列那样每层都从全部元素中选,而是第 `i` 层只能选 `digits[i]` 对应的那些字母。一旦选完最后一层(深度等于 `digits` 长度),路径就是一条完整的答案。
+
+---
+
+## 代码提示
+
+> [!abstract] 📝 回溯通用骨架
+
+```go
+var digitMap = map[byte]string{
+ '2': "abc", '3': "def", '4': "ghi", '5': "jkl",
+ '6': "mno", '7': "pqrs", '8': "tuv", '9': "wxyz",
+}
+
+var result []string
+
+var dfs func(path []byte)
+dfs = func(path []byte) {
+ // Base case: 已选满 digits 的长度
+ if len(path) == len(digits) {
+ result = append(result, string(path))
+ return
+ }
+
+ // 找出当前层(第 len(path) 个数字)的候选字母
+ ch := digits[len(path)]
+ for _, letter := range digitMap[ch] {
+ path = append(path, byte(letter)) // 做选择(range string → rune,需转 byte)
+ dfs(path) // 递归
+ path = path[:len(path)-1] // 撤销选择
+ }
+}
+
+// 边界:空输入直接返回空数组
+if digits == "" {
+ return result
+}
+
+dfs(nil)
+return result
+```
+
+> [!tip] 🔑 设计细节
+
+为什么 Base Case 是 `len(path) == len(digits)` 而不是传一个 `index` 参数?
+
+两种方式等价,但利用 `len(path)` 作为隐式索引更简洁——不需要额外传参,也不需要回退时改变 index。代价是每次用 `digits[len(path)]` 做一次切片访问,这在 Go 中是 O(1) 操作,毫无影响。
+
+---
+
+## 技巧
+
+> [!tip] 🔑 核心模式
+
+本题属于 **"笛卡尔积"** 类型的回溯——每层独立选择,最终结果是各层候选集的乘积:
+
+| 问题类型 | 决策方式 | 代表题目 |
+|---------|---------|---------|
+| **字母组合** | 每层从**当前数字对应的字母集**中选 | LC 17(本题) |
+| **全排列** | 每层从**全部未用元素**中选 | LC 46 |
+| **子集** | 每层**选或不选** | LC 78 |
+
+> [!summary] 📋 笛卡尔积 vs 其他回溯
+
+```mermaid
+flowchart LR
+ CART["笛卡尔积
每层候选集不同
如本题"]
+ PERM["全排列
每层从剩余中选
需 used[]"]
+ SUB["子集/组合
每层从 start 往后选
start 防回头"]
+
+ CART -. "同属回溯范式" .-> PERM
+ PERM -. "同属回溯范式" .-> SUB
+```
+
+> [!danger] ⚠️ 一个易错边界条件
+
+**输入为空字符串 `""` 时应该返回 `[]` 而非 `[""]`!**
+
+这是面试中最容易翻车的地方。很多初学者看到 `len(path) == 0 == len("")` 就直接把空字符串加入了结果——但实际上空输入不应该产生任何有效组合。
+
+```go
+if digits == "" {
+ return []string{}
+}
+```
+
+---
+
+## 代码
+
+> [!success] ✅ 解法:回溯
+
+```go
+// letterCombinations 返回 digits 能表示的所有字母组合。
+//
+// 时间复杂度:O(4^k * k) — k 为 digits 长度,最多 4 个字母/位,拼接结果 O(k)
+// 空间复杂度:O(k) — 递归栈深度为 k,k ≤ 4
+func letterCombinations(digits string) []string {
+ if digits == "" {
+ return []string{}
+ }
+
+ digitMap := [10]string{
+ "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz",
+ }
+
+ var result []string
+ path := make([]byte, 0, len(digits))
+
+ var dfs func()
+ dfs = func() {
+ if len(path) == len(digits) {
+ result = append(result, string(path))
+ return
+ }
+
+ // 取出当前层对应数字的字母集合
+ letters := digitMap[digits[len(path)]-'0']
+ for i := 0; i < len(letters); i++ {
+ path = append(path, letters[i]) // 做选择
+ dfs() // 递归进入下一层
+ path = path[:len(path)-1] // 撤销选择
+ }
+ }
+
+ dfs()
+ return result
+}
+```
+
+> [!success] ✅ 单元测试
+
+```go
+import (
+ "slices"
+ "testing"
+)
+
+func TestLetterCombinations(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expect []string
+ }{
+ {name: "示例1", input: "23", expect: []string{"ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"}},
+ {name: "示例2", input: "2", expect: []string{"a", "b", "c"}},
+ {name: "单字符7", input: "7", expect: []string{"p", "q", "r", "s"}},
+ {name: "含空串", input: "", expect: []string{}},
+ {name: "双7", input: "77", expect: []string{"pp", "pq", "pr", "ps", "qp", "qq", "qr", "qs", "rp", "rq", "rr", "rs", "sp", "sq", "sr", "ss"}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := letterCombinations(tt.input)
+ slices.Sort(got)
+ slices.Sort(tt.expect)
+ if !slices.Equal(got, tt.expect) {
+ t.Errorf("letterCombinations(%q) = %v; want %v", tt.input, got, tt.expect)
+ }
+ })
+ }
+}
+```
+
+---
+
+## 总结
+
+> [!summary] 📋 本章要点回顾
+
+> 电话号码的字母组合是回溯中 **"笛卡尔积型"** 的代表题:
+>
+> 1. **决策特征鲜明**——每层的选择集由当前数字决定,不同于排列的全量选择和子集的选/不选
+> 2. **实现最简回溯**——无需 `used[]`、无需 `start`,一个 `path` 走天下
+> 3. **边界处理关键**——空输入必须特判,否则会得到错误的 `[""]`
+> 4. **复杂度清晰**——4 叉为上限,`k ≤ 4` 的约束下枚举规模极小
+>
+> **口诀**:不进不出只改不回,选了加,回去退,每层候选看数字。
+
+---
+
+> [!example] 🔀 关联变体题
+
+- **[[回溯/55-全排列]]** — 每层从全部未用元素中选,需 `used[]`
+- **[[回溯/56-子集]]** — 二叉决策树,选或不选
+- **LeetCode 980. Unique Paths III** — 带障碍和步数约束的回溯探索
+- **LeetCode 22. Generate Parentheses** — 另一种笛卡尔积变形(状态受限的逐位构造)