树 | 数组 | 哈希表 | 分治 | 二叉树
当然,让我们深入解析这个问题和提供的代码,帮助你更好地理解二叉树的构建过程。
问题描述
题目:给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历序列,inorder
是同一棵树的中序遍历序列。请根据这两个遍历序列构造二叉树并返回其根节点。
示例
考虑以下二叉树:
3
/ \
9 20
/ \
15 7
- 前序遍历(Preorder):
[3, 9, 20, 15, 7]
- 中序遍历(Inorder):
[9, 3, 15, 20, 7]
根据这两个遍历序列,可以唯一确定这棵二叉树。
基本概念
在深入算法之前,让我们先回顾一下二叉树的两种遍历方式:
-
前序遍历(Preorder Traversal):
- 访问顺序:根节点 -> 左子树 -> 右子树
- 例如,上述示例的前序遍历顺序是
3 -> 9 -> 20 -> 15 -> 7
-
中序遍历(Inorder Traversal):
- 访问顺序:左子树 -> 根节点 -> 右子树
- 例如,上述示例的中序遍历顺序是
9 -> 3 -> 15 -> 20 -> 7
利用这两种遍历方式,可以唯一确定一棵二叉树。
算法思想
构建二叉树的关键在于:
-
确定根节点:
- 前序遍历的第一个元素一定是整个树的根节点。
-
划分左右子树:
- 根据根节点在中序遍历中的位置,可以将中序遍历序列划分为左子树和右子树的中序遍历序列。
- 左子树的中序遍历序列位于根节点的左边,右子树的中序遍历序列位于根节点的右边。
-
递归构建:
- 根据左子树和右子树的中序遍历序列长度,可以在前序遍历序列中划分出对应的左子树和右子树的前序遍历序列。
- 递归地对左右子树进行上述操作,直到遍历序列为空,返回
null
。
详细步骤解析
让我们结合代码一步步解析这个过程。
代码实现
const buildTree = (preorder, inorder) => {
if (inorder.length === 0) return null;
// 1. 前序遍历的第一个元素是根节点
const rootValue = preorder[0];
const root = new TreeNode(rootValue);
// 2. 在中序遍历中找到根节点的位置
const mid = inorder.indexOf(rootValue);
// 3. 划分左子树和右子树的遍历序列
const leftInorder = inorder.slice(0, mid);
const rightInorder = inorder.slice(mid + 1);
const leftPreorder = preorder.slice(1, mid + 1);
const rightPreorder = preorder.slice(mid + 1);
// 4. 递归构建左子树和右子树
root.left = buildTree(leftPreorder, leftInorder);
root.right = buildTree(rightPreorder, rightInorder);
return root;
};
逐步讲解
-
基例处理:
if (inorder.length === 0) return null;
- 如果中序遍历序列为空,说明当前子树为空,返回
null
。
- 如果中序遍历序列为空,说明当前子树为空,返回
-
确定根节点:
const rootValue = preorder[0]; const root = new TreeNode(rootValue);
- 前序遍历的第一个元素
preorder[0]
是当前树的根节点的值。 - 创建一个新的
TreeNode
节点作为根节点。
- 前序遍历的第一个元素
-
在中序遍历中找到根节点的位置:
const mid = inorder.indexOf(rootValue);
- 在中序遍历序列
inorder
中找到根节点的索引mid
。 - 这个位置将中序遍历序列划分为左子树和右子树。
- 在中序遍历序列
-
划分左右子树的遍历序列:
const leftInorder = inorder.slice(0, mid); const rightInorder = inorder.slice(mid + 1); const leftPreorder = preorder.slice(1, mid + 1); const rightPreorder = preorder.slice(mid + 1);
-
中序遍历:
- 左子树的中序遍历序列是
inorder.slice(0, mid)
。 - 右子树的中序遍历序列是
inorder.slice(mid + 1)
。
- 左子树的中序遍历序列是
-
前序遍历:
- 左子树的前序遍历序列是
preorder.slice(1, mid + 1)
。 - 右子树的前序遍历序列是
preorder.slice(mid + 1)
。
- 左子树的前序遍历序列是
-
这里需要注意,
preorder.slice(1, mid + 1)
代表从前序遍历中排除根节点后,下一段长度为mid
的子数组对应于左子树。
-
-
递归构建左右子树:
root.left = buildTree(leftPreorder, leftInorder); root.right = buildTree(rightPreorder, rightInorder);
- 递归调用
buildTree
函数,分别构建左子树和右子树,并将其赋值给根节点的left
和right
属性。
- 递归调用
-
返回根节点:
return root;
- 当左右子树递归构建完成后,返回当前根节点。
示例演示
让我们通过一个具体的例子来演示这个过程。
示例:
- 前序遍历:
[3, 9, 20, 15, 7]
- 中序遍历:
[9, 3, 15, 20, 7]
步骤 1:构建根节点
- 根节点值:
3
(preorder[0]
) - 创建根节点
TreeNode(3)
步骤 2:在中序遍历中找到根节点的位置
- 在
inorder
中,3
的索引为1
步骤 3:划分左右子树的遍历序列
-
左子树:
- 中序遍历:
[9]
(inorder.slice(0, 1)
) - 前序遍历:
[9]
(preorder.slice(1, 2)
)
- 中序遍历:
-
右子树:
- 中序遍历:
[15, 20, 7]
(inorder.slice(2)
) - 前序遍历:
[20, 15, 7]
(preorder.slice(2)
)
- 中序遍历:
步骤 4:递归构建左子树和右子树
-
构建左子树:
- 前序遍历:
[9]
- 中序遍历:
[9]
- 根节点:
9
- 左子树:
[]
(返回null
) - 右子树:
[]
(返回null
)
- 前序遍历:
-
构建右子树:
- 前序遍历:
[20, 15, 7]
- 中序遍历:
[15, 20, 7]
- 根节点:
20
- 在中序中找到
20
的位置:索引1
- 左子树:
- 前序遍历:
[15]
- 中序遍历:
[15]
- 根节点:
15
- 左右子树:
[]
(返回null
)
- 前序遍历:
- 右子树:
- 前序遍历:
[7]
- 中序遍历:
[7]
- 根节点:
7
- 左右子树:
[]
(返回null
)
- 前序遍历:
- 前序遍历:
最终构建的二叉树:
3
/ \
9 20
/ \
15 7
复杂度分析
时间复杂度
- 最坏情况:
O(n^2)
- 原因:
- 每次调用
indexOf
在inorder
数组中查找根节点的位置,需要O(n)
时间。 - 在树不平衡的情况下,递归调用的深度可能达到
n
,导致总时间复杂度为O(n^2)
。
- 每次调用
空间复杂度
- 最坏情况:
O(n^2)
- 原因:
- 每次递归调用都会创建新的数组
preorder.slice()
和inorder.slice()
,每次创建的数组大小可能接近n
。 - 递归深度达到
n
,因此总体空间复杂度是O(n^2)
。
- 每次递归调用都会创建新的数组
优化建议
为了将时间复杂度优化到 O(n)
,可以使用哈希表(Map)来存储中序遍历中每个值到其索引的映射,避免每次递归中使用 indexOf
查找根节点的位置。
优化后的代码
const buildTree = (preorder, inorder) => {
// 创建中序遍历值到索引的映射
const inorderMap = new Map();
inorder.forEach((value, index) => {
inorderMap.set(value, index);
});
// 定义一个指针,指向当前前序遍历的根节点
let preorderIndex = 0;
const arrayToTree = (left, right) => {
if (left > right) return null;
// 选择当前 preOrder 的根节点值
const rootValue = preorder[preorderIndex++];
const root = new TreeNode(rootValue);
// 根节点在中序遍历中的索引
const mid = inorderMap.get(rootValue);
// 递归构建左子树和右子树
root.left = arrayToTree(left, mid - 1);
root.right = arrayToTree(mid + 1, right);
return root;
};
return arrayToTree(0, inorder.length - 1);
};
优化后的复杂度
-
时间复杂度:
O(n)
- 每个节点只被访问一次。
- 哈希表查找
O(1)
,避免了indexOf
的O(n)
时间。
-
空间复杂度:
O(n)
- 哈希表需要
O(n)
的额外空间。 - 递归调用的空间复杂度为
O(n)
。
- 哈希表需要
总结
通过前序遍历和中序遍历序列,可以唯一构建出一棵二叉树。关键步骤包括:
- 确定根节点:前序遍历的第一个元素。
- 划分子树:在中序遍历中找到根节点的位置,划分出左子树和右子树的遍历序列。
- 递归构建:对左子树和右子树重复上述步骤,直到遍历序列为空。
初始的实现方法简单直观,但在大规模数据下可能存在性能问题。通过使用哈希表优化,可以显著提升算法的效率,使其适用于更大规模的输入。
实战建议
- 理解遍历方式:深入理解前序和中序遍历的访问顺序,有助于正确划分子树的遍历序列。
- 掌握递归思想:递归是处理树结构问题的常用方法,掌握其思想和实现方式非常重要。
- 优化技巧:在需要频繁查找元素位置时,考虑使用哈希表等数据结构来优化时间复杂度。
- 画图辅助理解:通过画出树的结构和遍历序列,有助于更直观地理解算法过程。
你的代码已经相当不错了,使用了递归构建二叉树,并通过哈希表优化了查找根节点在中序遍历中的位置。我将对你的代码进行进一步的优化和说明,以确保它的清晰性和效率。
优化思路
- 使用两个指针:避免使用字符串切割,使用指针来表示当前处理的子树的范围。
- 使用哈希表:将中序遍历的元素及其索引存储在哈希表中,以便在常数时间内查找根节点的位置,从而避免在每次递归中进行线性查找。
优化后的代码
以下是经过优化后的代码:
// 定义二叉树节点
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// 构建二叉树
const buildTree = (preorder, inorder) => {
// 创建一个哈希表,用于存储中序遍历值及其索引
const indexMap = new Map();
for (let i = 0; i < inorder.length; i++) {
indexMap.set(inorder[i], i);
}
// 辅助函数,使用指针进行递归
const helper = (p_start, p_end, i_start, i_end) => {
if (p_start > p_end) return null; // 基线条件:无节点
// 根节点的值
const rootVal = preorder[p_start];
const root = new TreeNode(rootVal); // 创建根节点
// 在哈希表中获取根节点在中序遍历中的位置
const mid = indexMap.get(rootVal);
const leftNum = mid - i_start; // 左子树的节点数
// 递归构建左子树和右子树
root.left = helper(p_start + 1, p_start + leftNum, i_start, mid - 1);
root.right = helper(p_start + leftNum + 1, p_end, mid + 1, i_end);
return root; // 返回构建好的根节点
};
// 调用辅助函数开始构建树
return helper(0, preorder.length - 1, 0, inorder.length - 1);
};
// 测试代码
const preorder = [3, 9, 20, 15, 7];
const inorder = [9, 3, 15, 20, 7];
const tree = buildTree(preorder, inorder);
// 辅助函数:打印二叉树(前序遍历)
const printTree = (node) => {
if (!node) return;
console.log(node.value);
printTree(node.left);
printTree(node.right);
};
// 打印构建的树
printTree(tree);
代码解析
1. 哈希表的构建
const indexMap = new Map();
for (let i = 0; i < inorder.length; i++) {
indexMap.set(inorder[i], i);
}
- 这段代码将中序遍历的每个元素及其对应的索引存入
indexMap
中。这样,我们可以在常数时间内找到任何元素在中序遍历中的位置。
2. 辅助函数 helper
const helper = (p_start, p_end, i_start, i_end) => {
if (p_start > p_end) return null; // 基线条件:无节点
const rootVal = preorder[p_start];
const root = new TreeNode(rootVal); // 创建根节点
const mid = indexMap.get(rootVal); // 查找根节点在中序遍历中的位置
const leftNum = mid - i_start; // 左子树的节点数
// 递归构建左子树和右子树
root.left = helper(p_start + 1, p_start + leftNum, i_start, mid - 1);
root.right = helper(p_start + leftNum + 1, p_end, mid + 1, i_end);
return root; // 返回构建好的根节点
};
- 基线条件:如果
p_start
大于p_end
,则说明当前子树为空,返回null
。 - 根节点的创建:使用前序遍历的第一个元素创建根节点。
- 查找中序位置:利用哈希表快速找到根节点在中序遍历中的索引。
- 递归调用:计算左子树和右子树的范围,递归构建子树。
3. 主调用
return helper(0, preorder.length - 1, 0, inorder.length - 1);
- 开始递归构建整个树,初始的指针范围是前序和中序遍历的整个范围。
性能分析
时间复杂度
- 构建哈希表:
O(n)
,遍历一次中序数组。 - 递归构建树:每个节点只访问一次,整体为
O(n)
。 - 总时间复杂度:
O(n)
。
空间复杂度
- 哈希表的空间:
O(n)
,用于存储中序遍历的索引。 - 递归栈空间:最坏情况下,树高度为
n
(如链状树),空间复杂度为O(n)
;平均情况下为O(log n)
。 - 总空间复杂度:
O(n)
。
好的,让我们通过一个具体的例子详细推导如何根据前序遍历和中序遍历构建二叉树的过程。我们将使用以下示例:
示例数据
- 前序遍历(Preorder):
[3, 9, 20, 15, 7]
- 中序遍历(Inorder):
[9, 3, 15, 20, 7]
目标
根据这两个遍历序列构建出如下的二叉树:
3
/ \
9 20
/ \
15 7
步骤 1: 构建哈希表
首先,我们需要构建一个哈希表来存储中序遍历中每个节点值及其对应的索引,以便快速查找。
const inorder = [9, 3, 15, 20, 7];
const indexMap = new Map();
for (let i = 0; i < inorder.length; i++) {
indexMap.set(inorder[i], i);
}
结果:
indexMap = {
9: 0,
3: 1,
15: 2,
20: 3,
7: 4
}
步骤 2: 定义递归函数
我们将使用一个递归函数 helper
,它接受当前前序和中序遍历的范围指针:
const helper = (p_start, p_end, i_start, i_end) => {
if (p_start > p_end) return null; // 基线条件
// ...
};
步骤 3: 开始构建树
我们从前序遍历的第一个元素开始(根节点),并逐步构建子树。
第一次调用 helper
- 参数:
helper(0, 4, 0, 4)
- 前序遍历范围:
preorder[0]
到preorder[4]
(即[3, 9, 20, 15, 7]
) - 中序遍历范围:
inorder[0]
到inorder[4]
(即[9, 3, 15, 20, 7]
)
处理过程
-
根节点:
rootVal = preorder[0] = 3
- 创建根节点
TreeNode(3)
-
查找中序位置:
mid = indexMap.get(3) = 1
-
计算左子树的节点数:
leftNum = mid - i_start = 1 - 0 = 1
-
递归构建左子树:
root.left = helper(1, 1, 0, 0)
-
递归构建右子树:
root.right = helper(2, 4, 2, 4)
步骤 4: 构建左子树
第二次调用 helper
- 参数:
helper(1, 1, 0, 0)
- 前序范围:
preorder[1]
到preorder[1]
(即[9]
) - 中序范围:
inorder[0]
到inorder[0]
(即[9]
)
处理过程
-
根节点:
rootVal = preorder[1] = 9
- 创建节点
TreeNode(9)
-
查找中序位置:
mid = indexMap.get(9) = 0
-
计算左子树的节点数:
leftNum = mid - i_start = 0 - 0 = 0
-
递归构建左子树:
root.left = helper(2, 1, 0, -1)
→ 结束,返回null
-
递归构建右子树:
root.right = helper(2, 1, 1, 0)
→ 结束,返回null
返回到第一次调用的 helper
,左子树构建完成。
步骤 5: 构建右子树
返回到第一次调用的 helper
,现在构建右子树:
第三次调用 helper
- 参数:
helper(2, 4, 2, 4)
- 前序范围:
preorder[2]
到preorder[4]
(即[20, 15, 7]
) - 中序范围:
inorder[2]
到inorder[4]
(即[15, 20, 7]
)
处理过程
-
根节点:
rootVal = preorder[2] = 20
- 创建节点
TreeNode(20)
-
查找中序位置:
mid = indexMap.get(20) = 3
-
计算左子树的节点数:
leftNum = mid - i_start = 3 - 2 = 1
-
递归构建左子树:
root.left = helper(3, 3, 2, 2)
-
递归构建右子树:
root.right = helper(4, 4, 4, 4)
步骤 6: 构建左子树的右子树
第四次调用 helper
- 参数:
helper(3, 3, 2, 2)
- 前序范围:
preorder[3]
到preorder[3]
(即[15]
) - 中序范围:
inorder[2]
到inorder[2]
(即[15]
)
处理过程
-
根节点:
rootVal = preorder[3] = 15
- 创建节点
TreeNode(15)
-
查找中序位置:
mid = indexMap.get(15) = 2
-
计算左子树的节点数:
leftNum = mid - i_start = 2 - 2 = 0
-
递归构建左子树:
root.left = helper(4, 3, 2, 1)
→ 结束,返回null
-
递归构建右子树:
root.right = helper(4, 3, 3, 2)
→ 结束,返回null
返回到第三次调用的 helper
,左子树构建完成。
步骤 7: 构建右子树的右子树
返回到第三次调用的 helper
,现在构建右子树:
第五次调用 helper
- 参数:
helper(4, 4, 4, 4)
- 前序范围:
preorder[4]
到preorder[4]
(即[7]
) - 中序范围:
inorder[4]
到inorder[4]
(即[7]
)
处理过程
-
根节点:
rootVal = preorder[4] = 7
- 创建节点
TreeNode(7)
-
查找中序位置:
mid = indexMap.get(7) = 4
-
计算左子树的节点数:
leftNum = mid - i_start = 4 - 4 = 0
-
递归构建左子树:
root.left = helper(5, 4, 4, 3)
→ 结束,返回null
-
递归构建右子树:
root.right = helper(5, 4, 5, 4)
→ 结束,返回null
构建完成
经过一系列的递归调用后,我们完成了整个树的构建。最终返回的树结构如下:
3
/ \
9 20
/ \
15 7
结果验证
通过前序遍历和中序遍历的结果,我们可以验证构建的树是否正确:
- 前序遍历:
3 -> 9 -> 20 -> 15 -> 7
- 中序遍历:
9 -> 3 -> 15 -> 20 -> 7
这与原始的前序和中序遍历一致,证明了树的构建是正确的。
二叉树遍历简介
二叉树遍历是指按照特定顺序访问二叉树中的所有节点。常见的遍历方式有三种:
- 前序遍历(Preorder Traversal)
- 中序遍历(Inorder Traversal)
- 后序遍历(Postorder Traversal)
每种遍历方式都有其独特的访问顺序,适用于不同的应用场景。
一、前序遍历(Preorder Traversal)
定义
前序遍历按照以下顺序访问节点:
- 根节点(Root)
- 左子树(Left Subtree)
- 右子树(Right Subtree)
简而言之,先访问根节点,然后遍历左子树,最后遍历右子树。
递归实现
下面是前序遍历的递归实现代码:
// 定义二叉树节点
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// 前序遍历:根 -> 左 -> 右
const preorderTraversal = (root) => {
const result = [];
const traverse = (node) => {
if (node === null) return;
result.push(node.value); // 访问根节点
traverse(node.left); // 遍历左子树
traverse(node.right); // 遍历右子树
};
traverse(root);
return result;
};
示例讲解
让我们使用一个简单的二叉树来演示前序遍历:
A
/ \
B C
/ \
D E
- 前序遍历顺序:
A -> B -> D -> E -> C
步骤解析
- 访问根节点
A
,将其加入结果。 - 遍历左子树
B
:- 访问
B
,加入结果。 - 遍历
B
的左子树D
:- 访问
D
,加入结果。 D
没有子节点,返回。
- 访问
- 遍历
B
的右子树E
:- 访问
E
,加入结果。 E
没有子节点,返回。
- 访问
- 访问
- 遍历右子树
C
:- 访问
C
,加入结果。 C
没有子节点,遍历结束。
- 访问
最终结果:['A', 'B', 'D', 'E', 'C']
二、中序遍历(Inorder Traversal)
定义
中序遍历按照以下顺序访问节点:
- 左子树(Left Subtree)
- 根节点(Root)
- 右子树(Right Subtree)
简而言之,先遍历左子树,然后访问根节点,最后遍历右子树。
递归实现
下面是中序遍历的递归实现代码:
// 中序遍历:左 -> 根 -> 右
const inorderTraversal = (root) => {
const result = [];
const traverse = (node) => {
if (node === null) return;
traverse(node.left); // 遍历左子树
result.push(node.value); // 访问根节点
traverse(node.right); // 遍历右子树
};
traverse(root);
return result;
};
示例讲解
使用同样的二叉树:
A
/ \
B C
/ \
D E
- 中序遍历顺序:
D -> B -> E -> A -> C
步骤解析
- 遍历左子树
B
:- 遍历
B
的左子树D
:- 访问
D
,加入结果。 D
没有子节点,返回。
- 访问
- 访问
B
,加入结果。 - 遍历
B
的右子树E
:- 访问
E
,加入结果。 E
没有子节点,返回。
- 访问
- 遍历
- 访问根节点
A
,加入结果。 - 遍历右子树
C
:- 访问
C
,加入结果。 C
没有子节点,遍历结束。
- 访问
最终结果:['D', 'B', 'E', 'A', 'C']
三、后序遍历(Postorder Traversal)
定义
后序遍历按照以下顺序访问节点:
- 左子树(Left Subtree)
- 右子树(Right Subtree)
- 根节点(Root)
简而言之,先遍历左子树和右子树,最后访问根节点。
递归实现
下面是后序遍历的递归实现代码:
// 后序遍历:左 -> 右 -> 根
const postorderTraversal = (root) => {
const result = [];
const traverse = (node) => {
if (node === null) return;
traverse(node.left); // 遍历左子树
traverse(node.right); // 遍历右子树
result.push(node.value); // 访问根节点
};
traverse(root);
return result;
};
示例讲解
使用同样的二叉树:
A
/ \
B C
/ \
D E
- 后序遍历顺序:
D -> E -> B -> C -> A
步骤解析
- 遍历左子树
B
:- 遍历
B
的左子树D
:- 访问
D
,加入结果。 D
没有子节点,返回。
- 访问
- 遍历
B
的右子树E
:- 访问
E
,加入结果。 E
没有子节点,返回。
- 访问
- 访问
B
,加入结果。
- 遍历
- 遍历右子树
C
:- 访问
C
,加入结果。 C
没有子节点,返回。
- 访问
- 访问根节点
A
,加入结果。
最终结果:['D', 'E', 'B', 'C', 'A']
四、总结与对比
遍历方式 | 顺序说明 | 示例顺序 |
---|---|---|
前序遍历 | 根 -> 左 -> 右 | A, B, D, E, C |
中序遍历 | 左 -> 根 -> 右 | D, B, E, A, C |
后序遍历 | 左 -> 右 -> 根 | D, E, B, C, A |
应用场景
-
前序遍历:
- 用于创建树的一个副本。
- 用于在树中查找某个节点的路径。
-
中序遍历:
- 对于二叉搜索树(BST),中序遍历会按升序获取节点值。
-
后序遍历:
- 用于删除树或释放树的节点,因为需要先处理子节点再处理根节点。
可视化对比
让我们将三种遍历方式在上述二叉树中的访问顺序进行对比:
A
/ \
B C
/ \
D E
- 前序:A → B → D → E → C
- 中序:D → B → E → A → C
- 后序:D → E → B → C → A
五、完整示例代码
为了更好地理解,我们将结合前序、中序和后序遍历的实现,以及示例树的构建和遍历结果展示。
// 定义二叉树节点
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// 构建示例树
/*
A
/ \
B C
/ \
D E
*/
const buildSampleTree = () => {
const root = new TreeNode('A');
root.left = new TreeNode('B');
root.right = new TreeNode('C');
root.left.left = new TreeNode('D');
root.left.right = new TreeNode('E');
return root;
};
// 前序遍历:根 -> 左 -> 右
const preorderTraversal = (root) => {
const result = [];
const traverse = (node) => {
if (node === null) return;
result.push(node.value); // 访问根节点
traverse(node.left); // 遍历左子树
traverse(node.right); // 遍历右子树
};
traverse(root);
return result;
};
// 中序遍历:左 -> 根 -> 右
const inorderTraversal = (root) => {
const result = [];
const traverse = (node) => {
if (node === null) return;
traverse(node.left); // 遍历左子树
result.push(node.value); // 访问根节点
traverse(node.right); // 遍历右子树
};
traverse(root);
return result;
};
// 后序遍历:左 -> 右 -> 根
const postorderTraversal = (root) => {
const result = [];
const traverse = (node) => {
if (node === null) return;
traverse(node.left); // 遍历左子树
traverse(node.right); // 遍历右子树
result.push(node.value); // 访问根节点
};
traverse(root);
return result;
};
// 主函数
const main = () => {
const root = buildSampleTree();
const preorder = preorderTraversal(root);
console.log('前序遍历(Preorder):', preorder.join(' -> '));
const inorder = inorderTraversal(root);
console.log('中序遍历(Inorder):', inorder.join(' -> '));
const postorder = postorderTraversal(root);
console.log('后序遍历(Postorder):', postorder.join(' -> '));
};
main();
输出结果
前序遍历(Preorder): A -> B -> D -> E -> C
中序遍历(Inorder): D -> B -> E -> A -> C
后序遍历(Postorder): D -> E -> B -> C -> A
六、深入理解递归
递归是一种解决问题的方法,其中函数直接或间接调用自身。二叉树的遍历自然适合用递归实现,因为树的结构本身是递归定义的(每个子节点也是一棵二叉树)。
递归遍历的原理
以前序遍历为例:
- 基线情况(Base Case):如果当前节点为空(
null
),则返回。 - 处理当前节点:访问根节点,执行相应操作(如将值加入结果数组)。
- 递归遍历左子树:调用遍历函数处理左子节点。
- 递归遍历右子树:调用遍历函数处理右子节点。
这种方式确保了所有节点按照预定的顺序被访问。
栈的作用
递归在内部使用调用栈来跟踪函数调用。每一次递归调用都会被压入栈中,确保在完成子树遍历后能正确返回到父节点继续执行。
七、实践建议
-
动手实践:自己实现不同的遍历方式,加深理解。
-
画图辅助:在纸上画出树的结构,标出访问顺序,帮助可视化。
-
理解递归基线:确保每个递归函数都有明确的基线情况,防止无限递归。
-
分析时间与空间复杂度:
- 时间复杂度:遍历所有节点,时间复杂度为
O(n)
,其中n
是节点数。 - 空间复杂度:递归调用栈的深度与树的高度有关,最坏情况下(如完全不平衡的树)为
O(n)
,平均情况下为O(log n)
。
- 时间复杂度:遍历所有节点,时间复杂度为
-
探索迭代方法:了解如何使用栈来实现迭代的遍历方式,进一步扩展知识。