二叉树高频

共 21 题
📑 题目列表 21 题
#
★★★

1. 二叉树的前中后序遍历中递归与迭代(显式栈)实现,以及 Morris 遍历的 O(1) 空间原理?

二叉树的前中后序遍历,说明递归与迭代(显式栈)实现,以及 Morris 遍历的 O(1) 空间原理?

  • 递归三序遍历
  • 迭代显式栈实现
  • Morris 遍历 O(1) 空间原理

递归:前序(根左右)、中序(左根右)、后序(左右根),代码简洁。迭代:前序用栈(根入栈,出栈访问,右左入栈);中序用栈(沿左链入栈,出栈访问后转向右子树);后序较复杂(用"prev 记录上次访问"或双栈/入栈标记)。Morris 遍历:O(1) 空间、O(n) 时间,利用"中序前驱节点"的右孩子原为 null 的特性做线索化——找当前节点的中序前驱(左子树最右),若其右孩子为 null 则设为当前节点(建立线索),访问后恢复;若右孩子已指向当前节点则说明左子树已遍历完,恢复右孩子为 null 并转向右子树。Morris 通过临时改建树(线索)避免栈,遍历完恢复原结构,故 O(1) 空间。

递归最直观,迭代用显式栈模拟递归,Morris 用"线索"实现 O(1) 空间。Morris 的核心是"利用空右指针记录回溯路径",访问后恢复。这是"空间最优遍历"的经典技巧,理解其线索化与恢复是关键。

// Morris 中序遍历
List<Integer> morrisInorder(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    TreeNode cur = root, pre = null;
    while (cur != null) {
        if (cur.left == null) { res.add(cur.val); cur = cur.right; }
        else {
            pre = cur.left;
            while (pre.right != null && pre.right != cur) pre = pre.right; // 找前驱
            if (pre.right == null) { pre.right = cur; cur = cur.left; } // 建线索
            else { pre.right = null; res.add(cur.val); cur = cur.right; } // 恢复
        }
    }
    return res;
}
#
★★★

2. 二叉树的最近公共祖先(LCA)中普通二叉树递归解法与 BST 利用有序性的差异?

求二叉树的最近公共祖先,说明普通二叉树递归解法与 BST 利用有序性的差异?

  • 普通二叉树递归后序查找
  • BST 用值比较定向
  • 复杂度差异

普通二叉树 LCA:递归后序遍历,返回条件:若当前节点为空或等于 p/q 则返回;否则递归左右子树,若左右都非空则当前节点是 LCA;若只有一侧非空则返回该侧结果。复杂度 O(n) 时间、O(n) 栈。BST LCA:利用有序性,从根开始,若 p、q 都小于当前值则 LCA 在左子树,都大于则在右子树,否则(一个左一个右或等于当前)当前节点即 LCA。复杂度 O(height) 时间、O(1) 空间(迭代)。差异:普通二叉树需全树后序搜索,BST 用值比较做二分导航,无需全树遍历。

普通二叉树 LCA 是"后序归并"(左右子树结果),BST LCA 是"值大小定向"(二分导航)。BST 利用有序性把 O(n) 降到 O(height)。理解"普通树需搜索"与"BST 可导航"的差异是核心。

// 普通二叉树 LCA
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    if (left != null && right != null) return root; // 左右都有,当前是 LCA
    return left != null ? left : right;
}
// BST LCA:迭代按值定向
TreeNode lcaBST(TreeNode root, TreeNode p, TreeNode q) {
    while (root != null) {
        if (p.val < root.val && q.val < root.val) root = root.left;
        else if (p.val > root.val && q.val > root.val) root = root.right;
        else return root;
    }
    return null;
}
#
★★★

3. 二叉树的层序遍历及其变体(之字形、右视图、每层最大值)的统一 BFS 模板?

二叉树的层序遍历及其变体(之字形、右视图、每层最大值),给出统一 BFS 模板?

  • 层序 BFS 按层处理
  • 用队列 + 每层 size 区分层
  • 变体在层内操作不同

统一 BFS 模板:用队列,把根入队;外层 while 队列非空,内层用当前队列 size 记录本层节点数,循环 size 次处理本层全部节点(出队、访问、把左右孩子入队)。这样每轮外层处理一层。变体:① 层序遍历:每层结果收集成一个列表;② 之字形:按层号奇偶决定正序/逆序收集(或用双端队列);③ 右视图:每层结束时取最后一个节点加入结果;④ 每层最大值:每层遍历时维护 max。核心是"用 size 固定每层边界",所有变体都在"层内收集"上微调。

层序 BFS 的关键是"用当前队列 size 分隔层",否则无法区分层边界。所有变体复用同一 BFS 框架,只在"层内怎么处理"上变化。这是"BFS 层序遍历"的统一模板。

List<Integer> rightSideView(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) return res;
    Deque<TreeNode> q = new ArrayDeque<>();
    q.offer(root);
    while (!q.isEmpty()) {
        int size = q.size();
        for (int i = 0; i < size; i++) {
            TreeNode node = q.poll();
            if (i == size - 1) res.add(node.val); // 右视图:每层最后
            if (node.left != null) q.offer(node.left);
            if (node.right != null) q.offer(node.right);
        }
    }
    return res;
}
#
★★★

4. 二叉树的最大路径和(LeetCode 124)中节点贡献值要取 max(0, 子节点贡献),全局答案如何在后序遍历中更新?

二叉树的最大路径和,说明为何节点贡献值取 max(0, 子节点贡献),以及全局答案如何在后序遍历中更新?

  • 节点贡献值定义(单侧路径)
  • 取 max(0, 子贡献) 的原因
  • 全局答案在后序更新

定义"贡献值":从节点出发,沿某条路径向下一侧能获得的最大路径和(单侧)。节点贡献 = node.val + max(0, 左贡献) + max(0, 右贡献)?不——贡献值用于"向上传递",是节点向父节点提供"单侧"最大和:node.val + max(0, 左子贡献, 右子贡献)。为什么取 max(0,...):路径可以只经过当前节点而不延展到某个子节点,若子节点贡献为负,延展反而减少总和,故取 0(不选)。全局答案:后序遍历时,对每个节点计算"以该节点为最高点的路径和" = node.val + max(0,左贡献) + max(0,右贡献)(可同时连接左右两侧),用全局变量更新最大值。后序保证子节点贡献先算好。

关键区分"贡献值"(单侧,向上传)与"路径和"(双侧,更新全局)。max(0,...) 允许路径"不经过负贡献的子节点"。这是"树形 DP + 全局带累"的经典题,后序保证子先算。

int ans = Integer.MIN_VALUE;
int maxPathSum(TreeNode root) {
    dfs(root); return ans;
}
int dfs(TreeNode node) {
    if (node == null) return 0;
    int L = Math.max(0, dfs(node.left));  // 负贡献取 0
    int R = Math.max(0, dfs(node.right));
    ans = Math.max(ans, node.val + L + R); // 以 node 为顶点的路径
    return node.val + Math.max(L, R);      // 单侧贡献向上传
}
#
★★★

5. 验证二叉搜索树中为什么"只比较父子节点"是经典错误?上下界递归与中序遍历两种正解?

验证二叉搜索树,说明为何"只比较父子节点"是经典错误,以及上下界递归与中序遍历两种正解?

  • 只比较父子是错误(需全局上下界)
  • 上下界递归传递 [lo,hi]
  • 中序遍历递增验证

只比较父子节点(node.left.val < node.val < node.right.val)是经典错误:因为 BST 要求左子树所有节点 < 根、右子树所有节点 > 根,只比较父子的局部条件无法保证"跨层"的全局约束(如某节点大于其父但小于其更上层的祖先,仍合法;反之可能被误判)。正解 ① 上下界递归:递归时传递 (lo, hi) 上下界,节点值必须在 (lo,hi) 内,左子树更新 hi、右子树更新 lo,用 null 表示无穷。正解 ② 中序遍历:BST 中序遍历严格递增,中序遍历收集序列并检查是否严格升序。两者都正确,上下界递归是标准解,中序更直观。

"只比较父子"漏掉了"右子树最左节点必须大于根"这类跨层约束。上下界递归通过逐层收窄 (lo,hi) 保证全局约束;中序遍历利用"BST 中序递增"的唯一性。两者是 BST 验证的两大正解。

boolean isValidBST(TreeNode root) {
    return validate(root, null, null);
}
boolean validate(TreeNode node, Integer lo, Integer hi) {
    if (node == null) return true;
    if (lo != null && node.val <= lo) return false;
    if (hi != null && node.val >= hi) return false;
    return validate(node.left, lo, node.val) && validate(node.right, node.val, hi);
}
#
★★★

6. 二叉树的遍历变体中前序+中序重建、Morris 遍历 O(1) 空间的原理?

说明前序+中序重建二叉树,以及 Morris 遍历 O(1) 空间的原理?

  • 前序+中序重建的递归
  • 用中序定位根、前序定根
  • Morris O(1) 空间原理

前序+中序重建:前序第一个元素是根,在中序中找到根的位置,把中序分成左右两段(左子树、右子树的节点集合),用索引在 前序中划分左右子树,递归重建。关键是"中序定位根确定左右子树大小,前序顺序给出根"。复杂度 O(n)(中序用哈希表定位索引加速)。Morris 遍历 O(1) 空间原理:利用节点左子树的最右节点(中序前驱)的空右指针做线索,临时指向当前节点,实现"无栈回溯",遍历完恢复。前序+中序重建是"由遍历序复原树"的经典题,Morris 是"线索化遍历"的空间最优解。

前序+中序重建必须知道"中序定位根 + 前序定根顺序",两者缺一不可(只有前序+后序有时无法唯一确定)。Morris 复用在遍历题中,其 O(1) 空间靠"临时线索"而非栈。理解重建的"根定位"与遍历的"线索化"是两个核心。

Map<Integer, Integer> idx;
TreeNode build(int[] pre, int preL, int preR, int inL, int inR) {
    if (preL > preR) return null;
    int rootVal = pre[preL];
    TreeNode root = new TreeNode(rootVal);
    int k = idx.get(rootVal);
    int leftSize = k - inL;
    root.left = build(pre, preL + 1, preL + leftSize, inL, k - 1);
    root.right = build(pre, preL + leftSize + 1, preR, k + 1, inR);
    return root;
}
#
★★★

7. 二叉搜索树转排序双向链表(LeetCode 426)中如何用中序遍历维护前驱指针,原地转换的空间复杂度?

二叉搜索树转排序双向链表,说明如何用中序遍历维护前驱指针,以及原地转换的空间复杂度?

  • 中序遍历天然有序
  • 维护前驱指针连接左右
  • 原地转换 O(log n) 栈(递归)

BST 中序遍历得到升序序列,故可用中序遍历把节点连成双向链表。维护一个 prev 指针(上一个访问的节点):遍历到当前节点时,若 prev 非空则 prev.right=cur、cur.left=prev,然后 prev=cur。最后把头指针(最左节点)与尾指针(最右节点)相连成循环。此时,原数组中每个节点的 left/right 被改造成 prev/next 指针,无需额外空间。空间复杂度:递归中序遍历用系统栈 O(log n)(平衡树)或 O(n)(退化树);若用 Morris 实现可到 O(1)。原地转换即"复用节点的 left/right 指针"。

中序遍历序列即有序序列,用 prev 指针在遍历中把相邻节点连成双向链表。原地转换不申请新节点,复用 left/right。空间主要来自递归栈,Morris 可 O(1)。这是"利用中序有序性 + 指针复用"的经典题。

TreeNode prev = null, head = null;
void inorder(TreeNode node) {
    if (node == null) return;
    inorder(node.left);
    if (prev == null) head = node; else { prev.right = node; node.left = prev; }
    prev = node;
    inorder(node.right);
}
TreeNode treeToDoublyList(TreeNode root) {
    if (root == null) return null;
    inorder(root);
    head.left = prev; prev.right = head; // 连成循环
    return head;
}
#
★★★

8. 二叉树的最大深度(LeetCode 104)中递归(1+max(左右))、迭代 BFS 与迭代 DFS 三种写法?

二叉树的最大深度,用递归、迭代 BFS、迭代 DFS 三种写法?

  • 递归 1+max(左右)
  • BFS 层数
  • 迭代 DFS 栈记录深度

① 递归:maxDepth(root)= root==null?0 : 1+max(maxDepth(left), maxDepth(right))。② 迭代 BFS:层序遍历,每处理一层深度+1,层数即最大深度。③ 迭代 DFS:用栈存 (节点, 深度),模拟前序,访问时更新最大深度。三者都 O(n) 时间。递归最简洁但可能栈溢出(深树),BFS 用队列无递归栈,DFS 迭代用显式栈。空间:递归 O(height)、BFS O(宽度)、DFS 栈 O(height)。

两种迭代是为了避免递归栈溢出。BFS 按层数计数,DFS 显式栈存深度。掌握三种写法体现"同一问题的不同实现",面试常考。

// 递归
int maxDepth(TreeNode root) { return root == null ? 0 : 1 + Math.max(maxDepth(root.left), maxDepth(root.right)); }
// 迭代 BFS
int maxDepthBFS(TreeNode root) {
    if (root == null) return 0;
    int depth = 0; Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root);
    while (!q.isEmpty()) { int size = q.size(); depth++; for (int i = 0; i < size; i++) { TreeNode n = q.poll(); if (n.left != null) q.offer(n.left); if (n.right != null) q.offer(n.right); } }
    return depth;
}
#
★★

9. 二叉树层序遍历如何区分"每层输出"与"整体输出"两种模式?

说明二叉树层序遍历如何区分"每层输出"与"整体输出"两种模式?

  • 每层输出需用 size 分隔层
  • 整体输出无需分隔
  • 两种模式的实现差异

整体输出:直接用队列 BFS,出队时访问并加入一个扁平列表,无需区分层边界,队列中所有节点顺序即层序(按层从左到右)。每层输出:需要在访问时区分层,用"当前队列 size"作为本层节点数,内层循环 size 次处理本层,把每层结果收集成独立列表。两种模式核心差异:是否用 size 分隔层。整体输出天然无分隔(一个列表),每层输出需在每层开始时记录 size 以界定边界。很多层序变体(右视图、之字形、每层最大值)都依赖"每层输出"模式。

"每层输出"是"整体输出"的扩展,关键一步是"进入每层时记录 size = 当前队列大小",内层循环恰好处理完本层。掌握 size 分隔技巧即可完成所有层序变体。

// 每层输出:外层 while,内层用 size 分隔
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
    int size = q.size(); // 本层节点数
    List<Integer> level = new ArrayList<>();
    for (int i = 0; i < size; i++) { TreeNode n = q.poll(); level.add(n.val); if (n.left != null) q.offer(n.left); if (n.right != null) q.offer(n.right); }
    res.add(level);
}
#
★★

10. 二叉树的序列化与反序列化(前序+null 标记 vs 层序)的设计取舍?

二叉树的序列化与反序列化,说明前序+null 标记与层序两种设计的取舍?

  • 前序+null 标记编码
  • 层序编码
  • 两者取舍

前序+null 标记:按前序遍历序列化,把 null 节点也用占位符标记(如 "null"),序列化结果唯一、可唯一重建。反序列化用队列/索引按前序顺序重建。需要 null 标记是因为普通前序无法唯一确定树的形状(有歧义),null 标记强制确定。层序:按 BFS 层序序列化,把 null 也写入,反序列化用队列按层序填充。两者都需要 null 标记保证唯一性。取舍:前序实现简洁、递归天然;层序更直观(与层序遍历一致)但需处理层边界。两者都 O(n)。前序 null 标记是最常用(如 LeetCode 的字符串表示)。

序列化必须"编码足够信息唯一重建",null 标记是关键(否则形状不确定)。前序 + null 用递归自然,层序 + null 用队列。设计取舍看实现习惯与可读性。

// 前序序列化
String serialize(TreeNode root) {
    if (root == null) return "null";
    return root.val + "," + serialize(root.left) + "," + serialize(root.right);
}
// 反序列化:用队列按前序消费
TreeNode deserialize(Deque<String> q) {
    String s = q.poll();
    if (s.equals("null")) return null;
    TreeNode node = new TreeNode(Integer.parseInt(s));
    node.left = deserialize(q); node.right = deserialize(q);
    return node;
}
#
★★

11. 平衡二叉树判断与二叉树直径中如何在一次后序遍历中同时求高度并判断/累积答案?

判断平衡二叉树与求二叉树直径,说明如何在后序遍历中同时求高度并判断/累积答案?

  • 后序遍历返回子树高度
  • 平衡判断:左右高度差 ≤1
  • 直径:左右高度和更新全局

平衡二叉树:后序遍历返回每个子树的高度,同时判断左右子树高度差是否 ≤1,若某节点不平衡则全局置 false。用"返回 -1 表示不平衡"或"返回高度 + 全局标志",一次后序同时求高度与判断。直径:直径是"经过某节点的最长路径" = 左子树高度 + 右子树高度。后序遍历返回子树高度,同时用全局变量更新 max(左高度+右高度)。两者都"一次后序 + 全局变量",复用"子树高度"这一信息。复杂度 O(n)。

两者共用"后序返回高度"的框架:平衡判断用高度差,直径用高度和。后序保证子节点先算,故一次遍历即可。区别是平衡判断看"差 ≤1",直径看"和最大"。这是"树形信息后序聚合"的通用技巧。

// 平衡判断:返回高度,-1 表示不平衡
int check(TreeNode node) {
    if (node == null) return 0;
    int L = check(node.left); if (L == -1) return -1;
    int R = check(node.right); if (R == -1) return -1;
    if (Math.abs(L - R) > 1) return -1;
    return Math.max(L, R) + 1;
}
// 直径:后序更新全局 maxDepth
int dfs(TreeNode node) {
    if (node == null) return 0;
    int L = dfs(node.left), R = dfs(node.right);
    diameter = Math.max(diameter, L + R); // 经过 node 的路径
    return Math.max(L, R) + 1;
}
#
★★

12. 完全二叉树的节点个数(LeetCode 222)中如何用左右子树高度判断满树,使复杂度降到 O(log^2 n)?

完全二叉树的节点个数,说明如何用左右子树高度判断满树,把复杂度降到 O(log² n)?

  • 完全二叉树性质
  • 用左右子树高度判断满树
  • 递归只走一条路径的 O(log² n)

完全二叉树:除最后一层外都满,最后一层左对齐。利用此性质:根节点左右子树高度相等则左子树是满树,节点数 = 2^h - 1(h 为左子树高度),只需递归右子树;否则右子树是满树(高度为左子树高度-1),递归左子树。递归时计算高度 O(log n),每层只走一侧,共 O(log n) 层,总 O(log² n)。具体:countNodes(root) = (左子树满) ? 2^h + countNodes(right) : countNodes(left) + 2^(h-1)。用 (1<<h) 计算 2^h。

关键利用"完全二叉树"的结构:左右子树高度相等说明左子树满(可公式算),否则右子树满。每层只递归一侧,配合高度计算 O(log n),得 O(log² n)。这是"利用结构性质 + 单侧递归"的优化。

int countNodes(TreeNode root) {
    if (root == null) return 0;
    int lh = height(root.left), rh = height(root.right);
    if (lh == rh) return (1 << lh) + countNodes(root.right); // 左子树满
    return (1 << rh) + countNodes(root.left);                 // 右子树满
}
int height(TreeNode node) { int h = 0; while (node != null) { h++; node = node.left; } return h; }
#
★★

13. 对称二叉树与翻转二叉树的递归与迭代实现?

对称二叉树与翻转二叉树,说明递归与迭代实现?

  • 对称判断:递归比较左右镜像
  • 翻转:递归交换左右
  • 迭代用栈/队列

对称二叉树(101):递归判断——两棵树对称当且仅当左.left 与右.right 对称且左.right 与右.left 对称且值相等。isSymmetric(root) = isMirror(root.left, root.right)。迭代:用队列/栈把镜像节点对入队,逐对比较。翻转二叉树(226):递归——交换左右子树,再递归翻转左右。invert(root) = swap then invert(left), invert(right)。迭代:用栈/队列逐节点交换左右。对称是"镜像相等",翻转是"左右互换",两者都用递归(或显式栈)实现。

对称判断核心是"镜像递归"(两棵子树的交叉比较),翻转核心是"交换 + 递归"。两者递归结构清晰,迭代用显式栈模拟。对称是"比较",翻转是"变换"。

// 对称:镜像比较
boolean isMirror(TreeNode a, TreeNode b) {
    if (a == null && b == null) return true;
    if (a == null || b == null) return false;
    return a.val == b.val && isMirror(a.left, b.right) && isMirror(a.right, b.left);
}
// 翻转
TreeNode invert(TreeNode root) {
    if (root == null) return null;
    TreeNode t = root.left; root.left = root.right; root.right = t;
    invert(root.left); invert(root.right);
    return root;
}
#
★★

14. 二叉树的最小深度(LeetCode 111)中为什么必须遇到叶子节点才结算深度,与最大深度的对称写法陷阱?

二叉树的最小深度,说明为何必须遇到叶子节点才结算深度,以及与最大深度的对称写法陷阱?

  • 最小深度定义到叶子
  • 单侧 null 的处理
  • 与最大深度的对称陷阱

最小深度是"从根到最近叶子节点的路径长度"。易错:不能简单写 min(minDepth(left), minDepth(right))+1,因为若某节点只有一个孩子(另一侧为 null),min 会取到 0(null 侧),导致错误深度。正解:若节点没有左孩子,则最小深度 = minDepth(right)+1(必须走右);若没有右孩子,则 = minDepth(left)+1;若都有则 min(左,右)+1。即"必须遇到叶子(左右都 null)才结算为 1"。与最大深度的陷阱:最大深度可写 max(左,右)+1(null 侧为 0 不影响),因为 max 取大值;最小深度用 min 会遇到 null 侧为 0 的错误,需特判单侧 null。这是"对称写法陷阱"。

最小深度与最大深度的对称陷阱:max 对 null 侧取 0 无碍,min 对 null 侧取 0 会错误地"提前到底"。因此最小深度必须处理"单侧为 null"的情况,确保走到叶子才结算。这是细节题。

int minDepth(TreeNode root) {
    if (root == null) return 0;
    if (root.left == null) return minDepth(root.right) + 1; // 单侧
    if (root.right == null) return minDepth(root.left) + 1;
    return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
}
#
★★

15. 路径总和(LeetCode 112/113)中递归减去当前节点值并在叶子处判断,记录路径的回溯写法如何撤销选择?

路径总和,说明递归减去当前节点值并在叶子处判断,以及记录路径的回溯如何撤销选择?

  • 递归减去当前节点值
  • 叶子处判断 targetSum
  • 回溯撤销选择

112(判断是否存在):递归时把 targetSum 减去当前节点值,若到达叶子(左右都为 null)且剩余值为 0 则存在。递归向下传递剩余值。113(记录所有路径):递归时同样减去节点值,同时把节点加入当前路径列表;到达叶子且和为 0 时,把当前路径的拷贝加入结果;返回上一层时从路径列表移除该节点(撤销选择),即回溯。撤销选择用"递归前 add、递归后 remove 最后一个"。路径列表是共享的,需在回溯时维护,确保不同分支互不污染。

112 是"减到叶子判断",113 是"回溯 + 路径记录"。核心区别是 113 需要记录路径,共享列表需在递归返回时撤销(remove 最后加入的节点)。"递归前 add、递归后 remove"是回溯标准写法。

void dfs(TreeNode node, int remaining, List<Integer> path, List<List<Integer>> res) {
    if (node == null) return;
    path.add(node.val);
    remaining -= node.val;
    if (node.left == null && node.right == null && remaining == 0)
        res.add(new ArrayList<>(path)); // 拷贝
    dfs(node.left, remaining, path, res);
    dfs(node.right, remaining, path, res);
    path.remove(path.size() - 1); // 撤销选择
}
#
★★

16. 二叉树的所有路径(LeetCode 257)中先序遍历携带路径字符串,回溯时如何正确拼接与撤销?

二叉树的所有路径,说明先序遍历携带路径字符串,以及回溯时如何拼接与撤销?

  • 先序遍历携带路径
  • 用字符串拼接(不可变)或列表回溯
  • 拼接与撤销

所有路径:先序遍历,携带当前路径。两种实现:① 用字符串参数(不可变):每次递归传入 path + "->" + node.val,天然无共享副作用(字符串不可变,各分支独立),无需撤销。② 用列表/可变路径:递归前加入节点,递归后移除(撤销),递归到叶子时把路径拼接成字符串。拼接:叶子时把路径列表用 "->" 连接加入结果。撤销:用可变列表时,递归返回后 remove 最后节点。字符串不可变方案更简洁(无撤销),列表方案省字符串拼接但需撤销。

字符串不可变方案利用"值传递"天然隔离分支,无需撤销;列表可变方案需回溯撤销。这是"不可变参数 vs 可变回溯"的对比。先序遍历保证路径顺序正确。

// 字符串不可变方案,无需撤销
void dfs(TreeNode node, String path, List<String> res) {
    if (node == null) return;
    path += path.isEmpty() ? "" + node.val : "->" + node.val;
    if (node.left == null && node.right == null) res.add(path);
    dfs(node.left, path, res);
    dfs(node.right, path, res);
}
#
★★

17. 二叉树的最大宽度(LeetCode 662)中层序遍历时为每层节点按下标编号,如何用“当前下标-最左下标”计算宽度?

二叉树的最大宽度,说明层序遍历为每层节点按下标编号,用"当前下标-最左下标"计算宽度?

  • 按完全二叉树下标编号
  • 每层最左下标与当前下标差
  • 防溢出(大下标)

最大宽度:层序遍历时给每个节点按完全二叉树的下标编号(根为 0,左孩子 2i+1、右孩子 2i+2),每层的宽度 = 该层最右节点下标 - 该层最左节点下标 + 1。用队列存 (节点, 下标),每层处理时记录最左下标,用当前下标减最左下标 +1 更新最大宽度。注意:下标可能很大(深树),可把每层下标相对该层第一个节点偏移(用 curIndex - leftIndex 否则可能溢出),或用 long。宽度包括中间的空位(按完全二叉树编号),故用下标差而非计数。

关键是把二叉树"投影"到完全二叉树的下标上,宽度由下标差决定(含空位)。层序遍历 + 下标编号是标准做法。每层偏移防止溢出是细节。

int widthOfBinaryTree(TreeNode root) {
    Deque<long[]> q = new ArrayDeque<>(); // {node, index}
    q.offer(new long[]{root.val, 0});
    int maxW = 0;
    while (!q.isEmpty()) {
        int size = q.size();
        long left = q.peekFirst()[1], right = q.peekFirst()[1];
        for (int i = 0; i < size; i++) {
            long[] e = q.poll(); right = e[1];
            // 左/右孩子入队(下标 2*idx+1, 2*idx+2)
        }
        maxW = Math.max(maxW, (int)(right - left + 1));
    }
    return maxW;
}
#
★★

18. 二叉搜索树中的插入与删除(LeetCode 701/450)中删除分无子/单子/双子三种情况,双子为何用前驱或后继替换?

二叉搜索树的插入与删除,说明删除分无子/单子/双子三种情况,以及双子为何用前驱或后继替换?

  • 插入:按值比较递归
  • 删除:无子/单子/双子
  • 双子用前驱/后继替换

插入:从根比较,小于则插左、大于则插右,递归到空位插入,保持 BST。删除:分三种情况——① 无子:直接删除;② 单子:用孩子顶替;③ 双子:用左子树最大值(前驱)或右子树最小值(后继)的值替换当前节点,然后递归删除该前驱/后继节点。为什么双子用前驱/后继替换:双子节点有两个孩子,直接删无法同时保留两个子树;而前驱(左子树最大值)或后继(右子树最小值)只有一个孩子(最多单子),把它的值赋给当前节点再删除它,既保持 BST 有序性又变成"单子/无子删除"的简单情况。复杂度 O(height)。

删除抽象是"把双子问题降为单子/无子"。前驱/后继是 BST 中"最接近当前值"的节点,替换后有序性不变。理解"用前驱/后继值替换 + 递归删除其叶子"是删除关键。

TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;
    if (key < root.val) root.left = deleteNode(root.left, key);
    else if (key > root.val) root.right = deleteNode(root.right, key);
    else { // 找到
        if (root.left == null) return root.right;  // 无子/单子
        if (root.right == null) return root.left;
        TreeNode succ = root.right; while (succ.left != null) succ = succ.left; // 后继
        root.val = succ.val;
        root.right = deleteNode(root.right, succ.val);
    }
    return root;
}
#

19. 最近公共祖先(LCA)中递归法、父指针法、RMQ 法各自的复杂度与适用场景?

比较最近公共祖先(LCA)的递归法、父指针法、RMQ 法各自的复杂度与适用场景?

  • 递归法 O(n) 单次
  • 父指针法(记录父节点)
  • RMQ 法(欧拉序 + 稀疏表)O(1) 查询

① 递归法:后序归并,O(n) 时间单次,O(height) 栈,适合"单次查询"或"树不常变"。② 父指针法:记录每个节点的父节点与深度,先对齐深度再同步上移,O(height) 时间,适合"有父指针且 height 不大"。③ RMQ 法:把树转欧拉序(DFS 访问序列),LCA 转化为"欧拉序区间的最小深度",用稀疏表/Segment 预处理,O(n log n) 预处理、O(1) 查询,适合"大量查询"(如在线查询多次)。④ 倍增法:预处理每个节点的 2^k 祖先,O(n log n) 预处理、O(log n) 查询,是最常用折中。选型:少查询用递归/父指针,多查询用 RMQ/倍增。

四种 LCA 方法本质是"预处理 vs 单次"的权衡:递归/父指针无预处理,RMQ/倍增预处理后查询更快。适用场景看查询次数与树规模。RMQ 的 O(1) 查询适合高频查询,倍增 O(log n) 查询更通用。

// 倍增法 LCA:up[logN][n] 存 2^k 祖先
int lca(int u, int v) {
    if (depth[u] < depth[v]) { int t = u; u = v; v = t; }
    // 对齐深度
    for (int k = LOG - 1; k >= 0; k--) if (depth[u] - (1 << k) >= depth[v]) u = up[k][u];
    if (u == v) return u;
    for (int k = LOG - 1; k >= 0; k--) if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; }
    return up[0][u];
}
#

20. 判断一棵树是否为二叉搜索树,中序递增法 vs 区间约束递归法的边界差异?

判断一棵树是否为二叉搜索树,比较中序递增法 vs 区间约束递归法的边界差异?

  • 中序递增法
  • 区间约束递归法(上下界)
  • 边界差异(重复值、整型边界)

中序递增法:中序遍历,检查序列严格递增。若允许重复(严格递增要求不相等),则要处理相等视为非法(BST 通常要求左<根<右,严格)。边界:用 prev 记录上一个值,比较时必须严格大于 prev。区间约束递归法:递归传递 (lo, hi) 上下界,节点值必须在 (lo,hi) 内。边界差异:① 重复值处理——中序递增要求严格(相等判非法),区间法用开区间 (lo,hi) 也拒绝相等;② 整型边界——区间法初始上下界用 null 表示无穷(避免 Integer.MIN/MAX 无法表示真实无穷),或用 Long 类型;③ 处理方式——中序法用"前驱值"连续性检查,区间法用"逐层收窄区间"检查。两者都正确,边界差异主要在"重复值"与"整型边界"的处理。

两种正解的核心差异:中序法靠"全局递增连续性",区间法靠"逐层区间约束"。边界上都要处理"重复值(严格)"与"整型无穷(避免用 MAX/MIN 当正无穷)"。掌握两者边界处理即可应对这道题的变体。

// 中序递增法
Integer prev = null;
boolean isValidBST(TreeNode root) {
    if (root == null) return true;
    if (!isValidBST(root.left)) return false;
    if (prev != null && root.val <= prev) return false; // 严格递增
    prev = root.val;
    return isValidBST(root.right);
}
#

21. 修剪二叉搜索树(LeetCode 669)中递归返回裁剪后的子树,如何处理节点值越界的三种分支?

修剪二叉搜索树,说明递归返回裁剪后的子树,以及节点值越界的三种分支处理?

  • 递归返回裁剪后的子树
  • 节点值越界的三种分支
  • 保持 BST 结构

修剪:给定 [low, high],裁剪掉所有不在此区间的节点,保持 BST。递归:trim(node) 返回修剪后的子树根。三种分支:① node.val < low:整棵左子树与当前节点都小于 low,应全部裁剪,返回 trim(node.right)(右子树可能包含合法值);② node.val > high:整棵右子树与当前节点都大于 high,返回 trim(node.left);③ low ≤ node.val ≤ high:当前节点保留,递归修剪左右子树,node.left=trim(node.left)、node.right=trim(node.right),返回 node。因 BST 性质,节点值小于 low 时其左子树必然都小于 low(可整棵丢弃),大于 high 时右子树都大于 high(丢弃),故只需裁剪一侧。返回裁剪后的子树,接口保持 BST。

BST 的性质使"越界裁剪"只需剪一侧:小于 low 则左子树全弃,大于 high 则右子树全弃。递归返回裁剪后的子树,父节点用返回值重建连接。这是"利用 BST 单调性 + 递归重建"的题。

TreeNode trimBST(TreeNode root, int low, int high) {
    if (root == null) return null;
    if (root.val < low) return trimBST(root.right, low, high); // 左子树全弃
    if (root.val > high) return trimBST(root.left, low, high); // 右子树全弃
    root.left = trimBST(root.left, low, high);
    root.right = trimBST(root.right, low, high);
    return root;
}