深度剖析树的遍历与计算树的高度技术文章大纲
引言
- 树结构在计算机科学中的重要性
- 遍历与高度计算的基础应用场景(如算法优化、数据处理)
树的遍历方法
-
深度优先遍历(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)的高度
root
不为空(是节点 1),不进入if
语句。- 执行
int hl = getHeight(root.left)
→ 计算左子树(节点 2)的高度。 - 执行
int hr = getHeight(root.right)
→ 计算右子树(节点 3)的高度。 - 比较
hl
和hr
,取最大值后 +1,得到整棵树的高度。
第 2 层:计算左子树(节点 2)的高度(对应步骤 2 的细节)
root
是节点 2(非空),不进入if
语句。- 执行
int hl = getHeight(root.left)
→ 计算左子树(节点 4)的高度。 - 执行
int hr = getHeight(root.right)
→ 计算右子树(节点 5)的高度。 - 比较后返回
max(hl, hr) + 1
。
第 3 层:计算节点 4 的高度(节点 2 的左子树)
root
是节点 4(非空),不进入if
语句。- 执行
int hl = getHeight(root.left)
→ 节点 4 的左子节点为null
,调用返回0
。 - 执行
int hr = getHeight(root.right)
→ 节点 4 的右子节点为null
,调用返回0
。 max(0, 0) = 0
,返回0 + 1 = 1
→ 节点 4 的高度为 1。
第 3 层:计算节点 5 的高度(节点 2 的右子树)
root
是节点 5(非空),不进入if
语句。- 执行
int hl = getHeight(root.left)
→ 计算左子树(节点 6)的高度。 - 执行
int hr = getHeight(root.right)
→ 节点 5 的右子节点为null
,返回0
。 - 比较后返回
max(hl, 0) + 1
。
第 4 层:计算节点 6 的高度(节点 5 的左子树)
root
是节点 6(非空),不进入if
语句。- 执行
int hl = getHeight(root.left)
→ 节点 6 的左子节点为null
,返回0
。 - 执行
int hr = getHeight(root.right)
→ 节点 6 的右子节点为null
,返回0
。 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 的细节)
root
是节点 3(非空),不进入if
语句。- 执行
int hl = getHeight(root.left)
→ 节点 3 的左子节点为null
,返回0
。 - 执行
int hr = getHeight(root.right)
→ 节点 3 的右子节点为null
,返回0
。 max(0, 0) = 0
,返回0 + 1 = 1
→ 节点 3 的高度为 1。
最终回溯到根节点(1)的计算
- 根节点的左子树高度
hl = 3
(节点 2 的高度),右子树高度hr = 1
(节点 3 的高度)。 max(3, 1) = 3
,返回3 + 1 = 4
→ 整棵树的高度为 4。
递归过程的核心思想
- 分解问题:将 "求整棵树的高度" 分解为 "求左子树高度" 和 "求右子树高度" 两个子问题。
- 终止条件:当遇到空节点时,高度为 0(递归不再深入)。
- 合并结果:子问题的结果(左右子树高度)取最大值后加 1,得到当前节点为根的树的高度。
这个过程就像从叶子节点开始 "自底向上" 计算:先算出最底层叶子的高度,再逐步向上推导出父节点、根节点的高度,最终得到整棵树的高度。
应用场景与优化
- 遍历与高度计算在平衡二叉树(AVL树)中的作用
- 时间复杂度分析:递归与迭代的对比(O(n) vs O(n))
- 空间复杂度优化技巧(如Morris遍历)
总结
- 不同遍历方法的适用场景
- 高度计算在算法设计中的扩展应用(如动态规划)
参考文献
- 《算法导论》中树的相关章节
- LeetCode典型例题(如104. 二叉树的最大深度)