树算法:从遍历到最近公共祖先

从前序、中序、后序、层序遍历进入高度、直径、路径、LCA 和树形 DP 的状态设计。

已发布文章计算机基础入门8 分钟阅读发布于 2026年9月1日更新于 2026年9月15日
文章目录
  1. 树算法路线
  2. 三种 DFS 遍历
  3. 层序遍历
  4. 树的高度和直径
  5. 最近公共祖先 LCA
  6. 树形 DP
  7. 常见错误
  8. 工程场景
  9. 面试题
  10. 小结
知识目录数据结构与算法:从基础到工程实践27 / 77
LOCAL NOTE高亮并记笔记
0 / 1000 · 只保存在这台设备
READER NOTES树算法:从遍历到最近公共祖先》的本机笔记

重新整理“树算法:从遍历到最近公共祖先”时,我先翻了自己以前的错误记录,其中最典型的一条就是:树题里我经常能写出遍历,却不知道信息应该从父节点往下传,还是从子树往上汇总。沿着这个问题再看原理和代码,比直接背结论清楚得多。

树算法连接了数据结构和算法两部分。前面学树结构时,我们关注节点如何组织;到了算法篇,我们更关注如何遍历、统计、寻找路径和合并子树状态。

树的问题通常有一个特点:父子关系天然递归,所以很多树算法都可以从“当前节点要向上返回什么”来思考。

树算法路线

树算法路线

图:树算法从遍历开始,逐步进入路径和子树状态

常见树算法:

  • 前序、中序、后序遍历。
  • 层序遍历。
  • 求深度、高度、节点数。
  • 路径和、直径。
  • 二叉搜索树查询和范围遍历。
  • 最近公共祖先 LCA。
  • 树形动态规划。

三种 DFS 遍历

void preorder(TreeNode root) {
    if (root == null) return;
    visit(root);
    preorder(root.left);
    preorder(root.right);
}

void inorder(TreeNode root) {
    if (root == null) return;
    inorder(root.left);
    visit(root);
    inorder(root.right);
}

void postorder(TreeNode root) {
    if (root == null) return;
    postorder(root.left);
    postorder(root.right);
    visit(root);
}

前序适合自顶向下传状态;后序适合先拿到子树结果,再计算当前节点;中序在 BST 中可以得到有序序列。

层序遍历

层序遍历使用队列:

List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> ans = new ArrayList<>();
    if (root == null) return ans;
    Queue<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 cur = q.poll();
            level.add(cur.val);
            if (cur.left != null) q.offer(cur.left);
            if (cur.right != null) q.offer(cur.right);
        }
        ans.add(level);
    }
    return ans;
}

层序适合按层输出、最短层数、宽度统计。

树的高度和直径

求高度:

int height(TreeNode root) {
    if (root == null) return 0;
    return Math.max(height(root.left), height(root.right)) + 1;
}

树的直径是任意两点之间最长路径。每个节点都可能作为最高拐点:

int ans = 0;

int depth(TreeNode root) {
    if (root == null) return 0;
    int left = depth(root.left);
    int right = depth(root.right);
    ans = Math.max(ans, left + right);
    return Math.max(left, right) + 1;
}

这是典型后序:先知道左右子树深度,再更新当前答案。

最近公共祖先 LCA

普通二叉树 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;
    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;
}

树形 DP

树形 DP 的核心是:每个节点从子节点拿状态,再计算自己的状态。

比如打家劫舍 III,每个节点有两个状态:

  • 偷当前节点。
  • 不偷当前节点。
int[] dfs(TreeNode root) {
    if (root == null) return new int[]{0, 0};
    int[] left = dfs(root.left);
    int[] right = dfs(root.right);
    int rob = root.val + left[1] + right[1];
    int notRob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
    return new int[]{rob, notRob};
}

树形 DP 的关键是返回值设计。如果当前节点需要的信息没有从子树返回,后面就会很别扭。

常见错误

  • 混淆深度和高度。
  • 后序问题写成前序,导致拿不到子树结果。
  • 层序遍历没有按层记录 size
  • LCA 没考虑节点不存在的情况。
  • BST 题没有利用有序性。
  • 递归深度过大导致栈溢出。

工程场景

  • 组织架构、菜单、目录树。
  • 评论楼中楼。
  • 权限继承。
  • 文件系统扫描。
  • AST 抽象语法树分析。
  • 前端路由树。

树结构工程中经常要处理软删除、排序、懒加载和权限过滤。算法题里的树是干净的,业务里的树往往会有残缺节点和循环脏数据,所以服务端要做防御。

面试题

  1. 前序、中序、后序分别适合什么问题?
  2. 为什么树的直径适合后序遍历?
  3. 普通二叉树 LCA 和 BST LCA 有什么区别?
  4. 树形 DP 的状态应该怎么设计?
  5. 递归处理业务树时如何防止脏数据导致死循环?

小结

树算法的核心是递归状态。想清楚当前节点要做什么、从子树拿什么、向父节点返回什么,就能解决大部分树题。前序偏传递状态,后序偏汇总状态,层序偏按层扩散。

JARVIS · 当前文章

有哪里没看懂?可以只问这篇。

Jarvis 会限定在《树算法:从遍历到最近公共祖先》及其公开关联内容中检索,并把引用定位回原文章节。

JARVIS / ARTICLE针对《树算法:从遍历到最近公共祖先》提问
当前范围树算法:从遍历到最近公共祖先不会悄悄扩大到全站
0 / 1000

准备好了。当前只会围绕树算法:从遍历到最近公共祖先回答。

READER SIGNAL

这篇内容对你有帮助吗?

不需要登录。你的反馈会直接进入作者待处理列表。