深度剖析树的遍历,计算树的高度

深度剖析树的遍历与计算树的高度技术文章大纲

引言
  • 树结构在计算机科学中的重要性
  • 遍历与高度计算的基础应用场景(如算法优化、数据处理)
树的遍历方法
  • 深度优先遍历(DFS)

    • 代码中使用的TreeNode(树节点)是二叉树的基本组成单元,每个节点包含:

    • val:节点存储的值(如整数)

    • left:指向左子节点的引用(如果没有左子节点则为null

    • right:指向右子节点的引用(如果没有右子节点则为null

    一、前序遍历(preOrder)

    前序遍历的顺序是:根节点 → 左子树 → 右子树

    复制代码
      void preOrder(TreeNode root) {
          // 步骤1:判断当前节点是否为空
          if (root == null) {
              return;  // 空树/空节点无需遍历,直接返回
          }
          
          // 步骤2:访问当前根节点(输出节点值)
          System.out.print(root.val + " ");
          
          // 步骤3:递归遍历左子树(处理左子节点)
          preOrder(root.left);  // 用左子节点作为新的"根节点"重复整个流程
          
          // 步骤4:递归遍历右子树(处理右子节点)
          preOrder(root.right);  // 用右子节点作为新的"根节点"重复整个流程
      }

    执行逻辑示例(以如下二叉树为例):

    plaintext

    复制代码
          1
         / \
        2   3
       / \
      4   5
    • 调用preOrder(1)root不为空,输出1
    • 执行preOrder(2)(左子节点):
      • root不为空,输出2
      • 执行preOrder(4)(左子节点):
        • root不为空,输出4
        • 执行preOrder(null)(4 的左子节点):空节点,返回
        • 执行preOrder(null)(4 的右子节点):空节点,返回
      • 执行preOrder(5)(2 的右子节点):
        • root不为空,输出5
        • 左右子节点均为空,返回
    • 执行preOrder(3)(1 的右子节点):
      • root不为空,输出3
      • 左右子节点均为空,返回
    • 最终输出:1 2 4 5 3

    二、中序遍历(inOrder)

    中序遍历的顺序是:左子树 → 根节点 → 右子树

    复制代码
      void inOrder(TreeNode root) {
          // 步骤1:判断当前节点是否为空
          if (root == null) {
              return;  // 空树/空节点无需遍历,直接返回
          }
          
          // 步骤2:递归遍历左子树(先处理左子节点)
          inOrder(root.left);  // 用左子节点作为新的"根节点"重复整个流程
          
          // 步骤3:访问当前根节点(输出节点值)
          System.out.print(root.val + " ");
          
          // 步骤4:递归遍历右子树(再处理右子节点)
          inOrder(root.right);  // 用右子节点作为新的"根节点"重复整个流程
      }

    执行逻辑示例(同上二叉树):

    • 调用inOrder(1)root不为空
    • 执行inOrder(2)(左子节点):
      • root不为空,执行inOrder(4)(左子节点):
        • root不为空,执行inOrder(null)(4 的左子节点):返回
        • 输出4
        • 执行inOrder(null)(4 的右子节点):返回
      • 输出2
      • 执行inOrder(5)(2 的右子节点):
        • root不为空,执行inOrder(null)(5 的左子节点):返回
        • 输出5
        • 执行inOrder(null)(5 的右子节点):返回
    • 输出1
    • 执行inOrder(3)(1 的右子节点):
      • root不为空,执行inOrder(null)(3 的左子节点):返回
      • 输出3
      • 执行inOrder(null)(3 的右子节点):返回
    • 最终输出:4 2 5 1 3

    三、后序遍历(postOrder)

    后序遍历的顺序是:左子树 → 右子树 → 根节点

    复制代码
      void postOrder(TreeNode root) {
          // 步骤1:判断当前节点是否为空(注意:原代码此处有重复判断,修正后如下)
          if (root == null) {
              return;  // 空树/空节点无需遍历,直接返回
          }
          
          // 步骤2:递归遍历左子树(先处理左子节点)
          postOrder(root.left);  // 用左子节点作为新的"根节点"重复整个流程
          
          // 步骤3:递归遍历右子树(再处理右子节点)
          postOrder(root.right);  // 用右子节点作为新的"根节点"重复整个流程
          
          // 步骤4:访问当前根节点(输出节点值)
          System.out.print(root.val + " ");
      }

    注意 :原代码中postOrder方法有重复的if (root == null)判断,属于笔误,上面已修正。

    执行逻辑示例(同上二叉树):

    • 调用postOrder(1)root不为空
    • 执行postOrder(2)(左子节点):
      • root不为空,执行postOrder(4)(左子节点):
        • root不为空,执行postOrder(null)(4 的左子节点):返回
        • 执行postOrder(null)(4 的右子节点):返回
        • 输出4
      • 执行postOrder(5)(2 的右子节点):
        • root不为空,执行postOrder(null)(5 的左子节点):返回
        • 执行postOrder(null)(5 的右子节点):返回
        • 输出5
      • 输出2
    • 执行postOrder(3)(1 的右子节点):
      • root不为空,执行postOrder(null)(3 的左子节点):返回
      • 执行postOrder(null)(3 的右子节点):返回
      • 输出3
    • 输出1
    • 最终输出:4 5 2 3 1

    总结:三种遍历的核心区别

    • 前序遍历:先处理根节点,再处理左右子树(根 → 左 → 右)
    • 中序遍历:先处理左子树,再处理根节点,最后处理右子树(左 → 根 → 右)
    • 后序遍历:先处理左右子树,最后处理根节点(左 → 右 → 根)
    • 三者的递归逻辑完全相同,唯一区别是 "访问根节点(输出值)的时机"。通过递归不断深入左子树,再回溯处理右子树,最终完成整个二叉树的遍历。
树的高度计算
java 复制代码
  //获取树的高度
    //整棵树的高度 = 左子树的高度和右子树的高度的最大值 + 1
    public int getHeight(TreeNode root){
        if(root == null){
            return 0;
        }
        int hl = getHeight(root.left);  //获取左子树的高度
        int hr = getHeight(root.right);  //获取右子树的高度
        int max = hl>hr?hl:hr;
        return max+1;
    }

这段代码用于计算二叉树的高度(也称为深度),采用了递归的思想。二叉树的高度定义为:从根节点到最远叶子节点的路径上的节点总数(或边数,这里实现的是节点数计数方式)。下面我会逐行详细解释每一步的执行逻辑。

前提:二叉树高度的定义

  • 空树的高度为 0
  • 非空树的高度 = 左子树高度和右子树高度中的最大值 + 1(加 1 是因为要包含当前根节点)

代码逐行解析

复制代码
public int getHeight(TreeNode root) {
    // 步骤1:判断当前节点是否为空
    if (root == null) {
        return 0;  // 空树/空节点的高度为0
    }
    
    // 步骤2:递归计算左子树的高度
    int hl = getHeight(root.left);  // 用左子节点作为新的"根节点"计算高度
    
    // 步骤3:递归计算右子树的高度
    int hr = getHeight(root.right);  // 用右子节点作为新的"根节点"计算高度
    
    // 步骤4:取左右子树高度的最大值
    int max = hl > hr ? hl : hr;  // 三元运算符,等价于 Math.max(hl, hr)
    
    // 步骤5:返回当前树的高度(最大值 + 1,包含当前根节点)
    return max + 1;
}

执行逻辑示例(以具体二叉树为例)

假设我们有如下二叉树(数字表示节点值,不是高度):

plaintext

复制代码
    1
   / \
  2   3
 / \
4   5
   /
  6

我们调用 getHeight(root) 其中 root 是值为 1 的根节点,看看代码如何计算高度。

第 1 层:计算根节点(1)的高度
  1. root 不为空(是节点 1),不进入 if 语句。
  2. 执行 int hl = getHeight(root.left) → 计算左子树(节点 2)的高度。
  3. 执行 int hr = getHeight(root.right) → 计算右子树(节点 3)的高度。
  4. 比较 hlhr,取最大值后 +1,得到整棵树的高度。
第 2 层:计算左子树(节点 2)的高度(对应步骤 2 的细节)
  1. root 是节点 2(非空),不进入 if 语句。
  2. 执行 int hl = getHeight(root.left) → 计算左子树(节点 4)的高度。
  3. 执行 int hr = getHeight(root.right) → 计算右子树(节点 5)的高度。
  4. 比较后返回 max(hl, hr) + 1
第 3 层:计算节点 4 的高度(节点 2 的左子树)
  1. root 是节点 4(非空),不进入 if 语句。
  2. 执行 int hl = getHeight(root.left) → 节点 4 的左子节点为 null,调用返回 0
  3. 执行 int hr = getHeight(root.right) → 节点 4 的右子节点为 null,调用返回 0
  4. max(0, 0) = 0,返回 0 + 1 = 1 → 节点 4 的高度为 1。
第 3 层:计算节点 5 的高度(节点 2 的右子树)
  1. root 是节点 5(非空),不进入 if 语句。
  2. 执行 int hl = getHeight(root.left) → 计算左子树(节点 6)的高度。
  3. 执行 int hr = getHeight(root.right) → 节点 5 的右子节点为 null,返回 0
  4. 比较后返回 max(hl, 0) + 1
第 4 层:计算节点 6 的高度(节点 5 的左子树)
  1. root 是节点 6(非空),不进入 if 语句。
  2. 执行 int hl = getHeight(root.left) → 节点 6 的左子节点为 null,返回 0
  3. 执行 int hr = getHeight(root.right) → 节点 6 的右子节点为 null,返回 0
  4. max(0, 0) = 0,返回 0 + 1 = 1 → 节点 6 的高度为 1。
回溯到节点 5 的计算
  • 节点 5 的左子树高度 hl = 1(节点 6 的高度),右子树高度 hr = 0
  • max(1, 0) = 1,返回 1 + 1 = 2 → 节点 5 的高度为 2。
回溯到节点 2 的计算
  • 节点 2 的左子树高度 hl = 1(节点 4 的高度),右子树高度 hr = 2(节点 5 的高度)。
  • max(1, 2) = 2,返回 2 + 1 = 3 → 节点 2 的高度为 3。
第 2 层:计算右子树(节点 3)的高度(对应步骤 3 的细节)
  1. root 是节点 3(非空),不进入 if 语句。
  2. 执行 int hl = getHeight(root.left) → 节点 3 的左子节点为 null,返回 0
  3. 执行 int hr = getHeight(root.right) → 节点 3 的右子节点为 null,返回 0
  4. max(0, 0) = 0,返回 0 + 1 = 1 → 节点 3 的高度为 1。
最终回溯到根节点(1)的计算
  • 根节点的左子树高度 hl = 3(节点 2 的高度),右子树高度 hr = 1(节点 3 的高度)。
  • max(3, 1) = 3,返回 3 + 1 = 4 → 整棵树的高度为 4。

递归过程的核心思想

  1. 分解问题:将 "求整棵树的高度" 分解为 "求左子树高度" 和 "求右子树高度" 两个子问题。
  2. 终止条件:当遇到空节点时,高度为 0(递归不再深入)。
  3. 合并结果:子问题的结果(左右子树高度)取最大值后加 1,得到当前节点为根的树的高度。

这个过程就像从叶子节点开始 "自底向上" 计算:先算出最底层叶子的高度,再逐步向上推导出父节点、根节点的高度,最终得到整棵树的高度。

应用场景与优化

  • 遍历与高度计算在平衡二叉树(AVL树)中的作用
  • 时间复杂度分析:递归与迭代的对比(O(n) vs O(n))
  • 空间复杂度优化技巧(如Morris遍历)
总结
  • 不同遍历方法的适用场景
  • 高度计算在算法设计中的扩展应用(如动态规划)
参考文献
  • 《算法导论》中树的相关章节
  • LeetCode典型例题(如104. 二叉树的最大深度)