数据结构:构建 (create) 一个二叉树
目录
问题的本质——什么信息才能唯一确定一棵树?
推导“最佳拍档”——哪两种遍历序列能行?
递归思想——如何构建一棵树?
第1步:确定整棵树的根节点
第2步:划分左右子树的成员
第3步:递归构建左右子树
将逻辑翻译为代码
第1步:搭建函数框架和递归出口
第2步:实现核心逻辑 - 创建根节点
第3步:划分中序序列并进行递归
第4步:创建主函数(包装函数)
完整代码和验证
问题的本质——什么信息才能唯一确定一棵树?
我们先思考一下,要构建一棵树,我们需要什么“原材料”?
假设我给你一些节点,比如 A, B, C
。我告诉你 A
是根节点。那么 B
和 C
在哪里呢?
-
可能是
B
是左孩子,C
是右孩子。 -
也可能是
B
是左孩子,C
是B
的左孩子。 -
还可能是
B
和C
都是A
的右孩子(形成一个链表)。
显然,只告诉我节点数据是不够的,因为它缺乏结构信息。
那么,我们上一节学到的“遍历序列”是不是一种结构信息呢?我们来试试。
数据结构:二叉树的遍历 (Binary Tree Traversals)-CSDN博客
假设我告诉你,一棵树的前序遍历序列是 A B C
。这棵树长什么样?
-
A
肯定是根节点,因为前序遍历第一个就是根。 -
B
是A
的左孩子吗?如果是,那么C
可能是A
的右孩子,也可能是B
的左/右孩子。 -
B
是A
的右孩子吗?...
A/ \B C
A/B/
C
A/B\C
你会发现,只靠一种遍历序列,我们无法唯一地确定一棵树的结构。 信息还是不够!
核心矛盾: 遍历序列是一个一维的、线性的数据。而树是一个二维的、非线性的结构。
从一维信息恢复二维结构,必然会丢失信息,导致不确定性。
解决方案: 那么,我们需要补充什么样的信息才能消除这种不确定性呢?
我们需要两种不同规则的遍历序列,用它们互相配合,锁定每一个节点的位置。
推导“最佳拍档”——哪两种遍历序列能行?
我们来分析一下上一节学到的三种遍历序列的“特长”:
-
前序遍历 (DLR: 根-左-右): 序列的第一个元素永远是当前这棵(子)树的根节点。
-
后序遍历 (LRD: 左-右-根):序列的最后一个元素永远是当前这棵(子)树的根节点。
-
中序遍历 (LDR: 左-根-右): 根节点在序列的中间,它像一根“柱子”,把序列划分成了两部分:左边的所有元素都属于左子树,右边的所有元素都属于右子树。
看到关键点了吗?
-
前序和后序遍历能帮我们轻松地找到根。
-
中序遍历能帮我们以根为界,划分出左右子树的范围。
这就是“最佳拍档”!我们可以用一个(前序或后序)来确定根,再用中序来确定左右子树的成员。
我们来推导 前序遍历 + 中序遍历 这个组合。
原材料:
-
前序序列 (Pre-order):
A B D E C F
-
中序序列 (In-order):
D B E A C F
A/ \B C/ \ \D E F
递归思想——如何构建一棵树?
第1步:确定整棵树的根节点
-
看前序序列
A B D E C F
,第一个元素是A
。 -
结论:
A
就是整棵树的根节点。
第2步:划分左右子树的成员
-
我们已经知道根是
A
了。现在看中序序列D B E A C F
。 -
找到
A
在中序序列中的位置。 -
A
左边的D B E
就是A
的左子树的所有成员。 -
A
右边的C F
就是A
的右子树的所有成员。
第3步:递归构建左右子树
现在问题被分解成了两个一模一样的子问题:
子问题1 (构建A的左子树):
-
我们知道它的成员是
{D, B, E}
。那么它的前序和中序序列是什么? -
很简单,回到原始序列中,只看这三个字母,保持它们原来的相对顺序。
-
原始前序:
A [B D E] C F
-> 左子树的前序:B D E
-
原始中序:
[D B E] A C F
-> 左子树的中序:D B E
-
现在,我们对这两个新的、更短的序列,重复第1步。
B/ \D E
子问题2 (构建A的右子树):
-
成员是
{C, F}
。 -
原始前序:
A B D E [C F]
-> 右子树的前序:C F
-
原始中序:
D B E A [C F]
-> 右子树的中序:C F
-
同样,对这两个新序列,重复第1步。
C\F
我们来深入子问题1 (左子树 B D E
):
-
第1步 (子问题): 看它的前序
B D E
,第一个是B
。所以B
是这个子树的根。 -
第2步 (子问题): 看它的中序
D B E
,找到B
。B
左边是D
,右边是E
。 -
结论:
D
是B
的左孩子,E
是B
的右孩子。 -
到这里,
D
和E
都已经是单个节点(叶子节点),它们的左右子树都是NULL
,递归的“出口”到了。
这个过程会一直持续下去,直到处理的序列为空。这就是从第一性原理推导出的,利用两种遍历序列构建树的完整逻辑。
将逻辑翻译为代码
现在我们把这个递归逻辑变成代码。我们需要一个函数,它能根据给定的前序和中序序列,返回构建好的树的根节点指针。
我们先定义函数原型。这个函数需要什么参数?
-
char* preorder
: 前序序列数组。 -
char* inorder
: 中序序列数组。 -
为了处理子问题,我们还需要告诉函数当前要处理的序列是哪一段。所以我们需要数组的边界。
-
int in_start
,int in_end
: 表示当前处理的中序序列在原数组中的起始和结束索引。
为什么只需要中序的边界,而不需要前序的边界?
因为前序序列的根总是在第一个位置,我们处理一个就用掉一个。我们可以用一个全局的或者传入指针的索引来依次访问前序序列,这样更简单。
第1步:搭建函数框架和递归出口
// 节点定义和创建函数(和上一节一样)
typedef struct Node {char data;struct Node* left;struct Node* right;
} Node;Node* createNode(char data) {Node* newNode = (Node*)malloc(sizeof(Node));newNode->data = data;newNode->left = NULL;newNode->right = NULL;return newNode;
}// 这是一个递归的辅助函数
// pre_idx_ptr 是一个指向前序序列当前索引的指针,这样在递归中修改才能生效
Node* buildTreeRecursive(char* preorder, int* pre_idx_ptr, char* inorder, int in_start, int in_end) {// 递归的出口 (Base Case)// 如果中序序列的起始点大于结束点,说明这是一个空子树,没有节点需要创建if (in_start > in_end) {return NULL;}// 后续的递归步骤将在这里填充// ...
}
第2步:实现核心逻辑 - 创建根节点
根据我们的推导,函数要做的第一件事就是从前序序列中取出当前的根。
Node* buildTreeRecursive(char* preorder, int* pre_idx_ptr, char* inorder, int in_start, int in_end) {if (in_start > in_end) {return NULL;}// 1. 从前序序列中获取根节点的值// 当前要处理的根,就是前序序列中 pre_idx_ptr 指向的那个元素char root_val = preorder[*pre_idx_ptr];// 2. 创建根节点Node* root = createNode(root_val);// 3. 用掉了一个前序元素,将索引向后移动一位,为下一个递归调用做准备(*pre_idx_ptr)++;// ... 接下来是划分和递归return root; // 暂时先返回根节点
}
第3步:划分中序序列并进行递归
创建了根节点后,我们需要在中序序列里找到它,然后划分出左右子树的范围,再进行递归调用。
Node* buildTreeRecursive(char* preorder, int* pre_idx_ptr, char* inorder, int in_start, int in_end) {if (in_start > in_end) {return NULL;}char root_val = preorder[*pre_idx_ptr];Node* root = createNode(root_val);(*pre_idx_ptr)++;// 4. 在中序序列中找到根节点的位置,以划分左右子树int in_root_idx = -1; // 初始化一个找不到的索引for (int i = in_start; i <= in_end; i++) {if (inorder[i] == root_val) {in_root_idx = i;break; // 找到了就退出循环}}// 如果在中序序列中找不到根(输入有误),可以加个错误处理// if (in_root_idx == -1) { /* error handling */ }// 5. 递归构建左右子树// 注意递归的顺序!因为我们是按前序(根->左->右)的顺序消耗元素的,// 所以必须先递归构建左子树,再递归构建右子树。// 构建左子树// 左子树的范围是中序序列的 in_start 到 根的前一个位置(in_root_idx - 1)root->left = buildTreeRecursive(preorder, pre_idx_ptr, inorder, in_start, in_root_idx - 1);// 构建右子树// 右子树的范围是中序序列的 根的后一个位置(in_root_idx + 1) 到 in_endroot->right = buildTreeRecursive(preorder, pre_idx_ptr, inorder, in_root_idx + 1, in_end);return root;
}
第4步:创建主函数(包装函数)
为了让用户调用起来更方便,我们创建一个主函数,它负责初始化前序索引并调用递归函数。
// 主函数,用户调用这个
Node* buildTree(char* preorder, char* inorder, int size) {int pre_idx = 0; // 初始化前序序列的起始索引// 调用递归辅助函数,初始范围是整个中序数组return buildTreeRecursive(preorder, &pre_idx, inorder, 0, size - 1);
}
至此,整个从逻辑推导到代码实现的过程就完成了。
完整代码和验证
让我们把所有部分组合起来,并写一个 main
函数来验证我们的成果。
验证方法很简单:用我们创建树的函数 buildTree
来构建一棵树,然后用上一节学过的任意一种遍历(比如后序遍历)来打印这棵树,看看结果是否符合预期。
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // For strlen// --- 节点定义和创建函数 ---
typedef struct Node {char data;struct Node* left;struct Node* right;
} Node;Node* createNode(char data) {Node* newNode = (Node*)malloc(sizeof(Node));newNode->data = data;newNode->left = NULL;newNode->right = NULL;return newNode;
}// --- 从前序和中序序列构建树的实现 ---// 递归辅助函数
Node* buildTreeRecursive(char* preorder, int* pre_idx_ptr, char* inorder, int in_start, int in_end) {// 递归出口if (in_start > in_end) {return NULL;}// 1. 创建根节点char root_val = preorder[*pre_idx_ptr];Node* root = createNode(root_val);(*pre_idx_ptr)++;// 2. 在中序序列中找到根,以划分左右子树int in_root_idx = -1;for (int i = in_start; i <= in_end; i++) {if (inorder[i] == root_val) {in_root_idx = i;break;}}// 3. 递归构建左子树和右子树root->left = buildTreeRecursive(preorder, pre_idx_ptr, inorder, in_start, in_root_idx - 1);root->right = buildTreeRecursive(preorder, pre_idx_ptr, inorder, in_root_idx + 1, in_end);return root;
}// 主构建函数
Node* buildTree(char* preorder, char* inorder, int size) {int pre_idx = 0;return buildTreeRecursive(preorder, &pre_idx, inorder, 0, size - 1);
}// --- 验证用的遍历函数 (从上一节课拿来) ---
void postOrder(Node* root) {if (root == NULL) return;postOrder(root->left);postOrder(root->right);printf("%c ", root->data);
}// --- Main 函数 ---
int main() {char preorder[] = "ABDECF";char inorder[] = "DBEACF";int size = strlen(preorder);printf("Input Pre-order: %s\n", preorder);printf("Input In-order: %s\n", inorder);// 使用我们推导出的函数来构建树Node* root = buildTree(preorder, inorder, size);printf("\nVerification with Post-order traversal:\n");printf("Expected: D E B F C A\n");printf("Actual: ");postOrder(root);printf("\n");// 如果 Actual 和 Expected 一致,说明我们的树构建完全正确!// 在此添加释放树内存的代码...return 0;
}
关于后序+中序的思考:
这个逻辑是完全一样的。区别在于:
-
后序遍历的根在序列的末尾,所以你要从后往前处理后序序列。
-
因为根是最后处理的,所以你的递归调用顺序应该是先构建右子树,再构建左子树,最后才把它们连接到根上。
这个推导过程清晰地展示了如何从一个根本性的问题(什么信息能唯一确定一棵树)出发,通过逻辑分析和推理,一步步设计出算法,并最终转化为简洁、高效的递归代码。