二叉树的每个节点最多有两个孩子:
struct Node {
int value;
Node* left;
Node* right;
};value:节点值。left:左孩子。right:右孩子。
当前实现使用裸指针管理节点,由 BinaryTree 析构时统一释放整棵树。
层序构建使用 std::optional<int> 表示空节点。
例如:
BinaryTree tree({
1,
2,
3,
4,
5,
std::nullopt,
6,
});表示:
1
/ \
2 3
/ \ \
4 5 6
构建过程:
- 第一个元素作为根节点。
- 使用队列保存等待挂孩子的节点。
- 每次弹出一个节点。
- 从数组中读取它的左孩子和右孩子。
- 非空孩子创建节点并入队。
这种方式和层序遍历天然对应。
前序遍历顺序:
根 -> 左 -> 右
中序遍历顺序:
左 -> 根 -> 右
所以前序数组的第一个元素一定是当前子树的根。
例如:
preorder = 1 2 4 5 3 6
inorder = 4 2 5 1 3 6
前序第一个元素 1 是根。
在中序中找到 1:
4 2 5 | 1 | 3 6
左边 4 2 5 是左子树,右边 3 6 是右子树。
左子树大小是 3,因此前序也可以切分:
1 | 2 4 5 | 3 6
递归处理左右子树即可。
顺序:
根 -> 左 -> 右
代码逻辑:
out.push_back(node->value);
preorder(node->left, out);
preorder(node->right, out);前序适合复制树、序列化树结构、表达父节点先于子节点的场景。
顺序:
左 -> 根 -> 右
代码逻辑:
inorder(node->left, out);
out.push_back(node->value);
inorder(node->right, out);对于二叉搜索树,中序遍历结果是有序数组。
顺序:
左 -> 右 -> 根
代码逻辑:
postorder(node->left, out);
postorder(node->right, out);
out.push_back(node->value);后序适合释放树、计算子树信息等“先处理孩子,再处理父节点”的场景。
当前实现释放节点 destroy 也使用后序思想:
destroy(node->left);
destroy(node->right);
delete node;层序遍历使用队列:
- 根节点入队。
- 队列不为空时,弹出队首节点。
- 访问当前节点。
- 左孩子非空则入队。
- 右孩子非空则入队。
层序遍历顺序是从上到下、从左到右。
实现和测试重点覆盖:
- 空树。
- 层序数组为空。
- 层序数组第一个元素为空。
- 只有一个节点。
- 有空孩子的非完全二叉树。
- 前序和中序长度不一致。
- 前序和中序内容不匹配。
- 构建后执行四种遍历。
- 调用
clear()后再次遍历。
设节点数为 n。
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 层序建树 | O(n) |
O(n) |
| 前序 + 中序建树 | O(n^2) |
O(h) |
| 前序遍历 | O(n) |
O(h) |
| 中序遍历 | O(n) |
O(h) |
| 后序遍历 | O(n) |
O(h) |
| 层序遍历 | O(n) |
O(n) |
其中 h 是树高。
当前前序 + 中序建树为了代码直观,每次在中序区间里线性查找根节点,因此最坏是 O(n^2)。如果要优化,可以提前建立 value -> index 哈希表,把建树优化到 O(n)。
推荐这样讲:
- 先说明节点结构:值、左孩子、右孩子。
- 讲四种遍历顺序:前中后层。
- 讲递归遍历的终止条件:节点为空直接返回。
- 讲层序遍历为什么用队列。
- 讲前序 + 中序建树:前序定根,中序切左右子树。
- 最后补充内存释放使用后序思想。