🌳 二叉树构建算法全解析 | 三种方法详解+动图演示
📌 前言
二叉树是数据结构中的重要内容,今天我们将通过图解+代码的方式,详细讲解三种不同的二叉树构建方法。每种方法都配有完整示例和动图演示!
方法一:递归构建法 (Preorder)
🎯 核心思想
"先根后子树":按照根节点→左子树→右子树的顺序递归构建
📝 输入格式
"ABC##D##"
(#
表示空节点)
🖼️ 图解构建过程
graph TD
A[步骤1: 创建A] --> B[步骤2: 创建B]
B --> C[步骤3: 创建C]
C --> N1[步骤4: C左子树#]
C --> N2[步骤5: C右子树#]
B --> D[步骤6: 创建D]
D --> N3[步骤7: D左子树#]
D --> N4[步骤8: D右子树#]
🏗️ 构建结果
css
A
/
B
/ \
C D
💻 代码关键点
cpp
BTNode* buildTree1(const string& str, int& index) {
if(index >= str.length() || str[index] == '#') {
index++;
return nullptr; // 遇到#返回空
}
BTNode* node = new BTNode(str[index++]); // 创建当前节点
node->lchild = buildTree1(str, index); // 递归构建左子树
node->rchild = buildTree1(str, index); // 递归构建右子树
return node;
}
方法二:括号表示法构建二叉树 - 代码关键点深度解析
完整代码
cpp
BTNode* buildTree2(string str) {
stack<BTNode*> st; // 父节点栈
BTNode* p = nullptr; // 当前节点指针
bool flag = true; // 方向标记(true=左,false=右)
int i = 0; // 字符串索引
while(i < str.length()) {
switch(str[i]) {
case '(':
st.push(p);
flag = true;
break;
case ')':
st.pop();
break;
case ',':
flag = false;
break;
default:
p = new BTNode(str[i]);
if(r == nullptr) {
r = p;
}
else {
if(flag && !st.empty()) {
st.top()->lchild = p;
}
else if(!flag && !st.empty()) {
st.top()->rchild = p;
}
}
break;
}
i++;
}
return r;
}
关键变量说明
变量 | 类型 | 作用 |
---|---|---|
st |
stack<BTNode*> |
保存待处理子树的父节点(遇到"("时压栈,遇到")"时弹栈) |
p |
BTNode* |
当前正在处理的节点指针 |
flag |
bool |
方向标记:true 表示下一个节点是左孩子,false 表示是右孩子 |
r |
BTNode* |
类的成员变量,存储最终树的根节点 |
核心逻辑分解
1. 遇到字母(节点值)
cpp
default:
p = new BTNode(str[i]); // 创建新节点
if(r == nullptr) { // 第一个节点是根节点
r = p;
}
else { // 非根节点处理
if(flag && !st.empty()) { // 作为左孩子连接
st.top()->lchild = p;
}
else if(!flag && !st.empty()) { // 作为右孩子连接
st.top()->rchild = p;
}
}
break;
内存变化示例(处理"A(B(D,E),C(,F))"中的'A'):
yaml
执行前:
st: [] p: nullptr r: nullptr
执行后:
st: [] p: A r: A
内存:
A
/ \
null null
2. 遇到'('
cpp
case '(':
st.push(p); // 当前节点压栈(成为父节点)
flag = true; // 接下来处理左子树
break;
处理逻辑:
- 将当前节点保存到栈中(后续子树的父节点)
- 重置方向为左子树
示例(处理完'A'后遇到'('):
ini
执行前:
st: [] p: A r: A
执行后:
st: [A] p: A flag=true
3. 遇到','
cpp
case ',':
flag = false; // 切换为右子树处理
break;
处理逻辑:
- 表示左子树处理完毕
- 准备处理右子树
示例(处理"B(D,E)"中的','):
ini
执行前:
st: [A,B] p: D flag=true
执行后:
st: [A,B] p: D flag=false
4. 遇到')'
cpp
case ')':
st.pop(); // 子树处理完成,回到父节点
break;
处理逻辑:
- 弹出栈顶元素(当前子树的父节点)
- 准备处理父节点的兄弟节点
示例(处理"B(D,E)"后的')'):
yaml
执行前:
st: [A,B] p: E
执行后:
st: [A] p: E
完整执行示例(处理"A(B(D,E),C(,F))")
步骤 | 字符 | 操作 | 栈状态 | 当前节点 | 方向 | 树结构 |
---|---|---|---|---|---|---|
1 | A | 创建根节点A | [] | A | true | A |
2 | ( | A入栈 | [A] | A | true | |
3 | B | 创建B作为A的左孩子 | [A] | B | true | A(B) |
4 | ( | B入栈 | [A,B] | B | true | |
5 | D | 创建D作为B的左孩子 | [A,B] | D | true | A(B(D)) |
6 | , | 切换右子树 | [A,B] | D | false | |
7 | E | 创建E作为B的右孩子 | [A,B] | E | false | A(B(D,E)) |
8 | ) | 弹出B | [A] | E | - | |
9 | , | 切换右子树 | [A] | E | false | |
10 | C | 创建C作为A的右孩子 | [A] | C | false | A(B(D,E),C) |
11 | ( | C入栈 | [A,C] | C | true | |
12 | , | 切换右子树 | [A,C] | C | false | |
13 | F | 创建F作为C的右孩子 | [A,C] | F | false | A(B(D,E),C(,F)) |
14 | ) | 弹出C | [A] | F | - | |
15 | ) | 弹出A | [] | F | - | 构建完成 |
内存管理注意事项
- 节点创建 :每次遇到新字母时
new BTNode()
,需要确保最终有对应的删除操作 - 栈的作用:确保在嵌套子树结构中能正确回溯到父节点
- 方向标记:正确处理左右子树的切换时机
边界情况处理
- 空输入 :
while
循环会自动跳过 - 多余括号:缺少对应处理(实际使用时需要增强鲁棒性)
- 根节点覆盖 :通过
r==nullptr
判断确保根节点只设置一次
这种构建方式特别适合处理带括号表示法的树结构字符串,通过维护栈和方向标记,可以高效地构建出正确的二叉树结构。
方法三:关系表构建法
🎯 核心思想
"两步走策略":
- 创建所有节点
- 建立连接关系
📝 输入格式
r
6
A B C
B D E
C # F
D # #
E # #
F # #
🖼️ 构建流程图
graph TB
subgraph 第一阶段
A[读取所有节点] --> B[存入map]
end
subgraph 第二阶段
C[重新读取] --> D[建立父子关系]
end
D --> E[确定根节点]
💡 关键技巧
map<char, BTNode*>
快速查找节点seekg(0)
重置输入流begin()->second
获取第一个节点作为根
💻 完整代码
cpp
BTNode* buildTree3(istream& in){
int n;
in >> n;
map<char,BTNode*> nodes;
for(int i=0;i<n;i++){
char parent,left,right;
in >> parent >> left >> right ;
if(nodes.find(parent)==nodes.end()){
nodes[parent]=new BTNode(parent);
}
if(left!='#'&&nodes.find(left)==nodes.end()){
nodes[left]=new BTNode(left);
}
if(right!='#'&&nodes.find(right)==nodes.end()){
nodes[right]=new BTNode(right);
}
}
in.seekg(0,ios::beg);
in >> n;
for(int i=0;i<n;i++){
char parent,left,right;
in >> parent >> left >> right;
if(left!='#'){
nodes[parent]->lchild=nodes[left];
}
if(right!='#'){
nodes[parent]->rchild=nodes[right];
}
}
r=nodes.begin()->second;
return r;
}
📊 方法对比总结
方法 | 适用场景 | 时间复杂度 | 空间复杂度 |
---|---|---|---|
递归构建 | 先序遍历字符串 | O(n) | O(h) |
括号表示法 | 带括号的表达式 | O(n) | O(n) |
关系表构建 | 明确的父子关系表 | O(n) | O(n) |