当前位置: 首页 > news >正文

数据结构:构建 (create) 一个二叉树

目录

问题的本质——什么信息才能唯一确定一棵树?

推导“最佳拍档”——哪两种遍历序列能行?

递归思想——如何构建一棵树?

第1步:确定整棵树的根节点

第2步:划分左右子树的成员

第3步:递归构建左右子树

将逻辑翻译为代码

第1步:搭建函数框架和递归出口

第2步:实现核心逻辑 - 创建根节点

第3步:划分中序序列并进行递归

第4步:创建主函数(包装函数)

完整代码和验证


问题的本质——什么信息才能唯一确定一棵树?

我们先思考一下,要构建一棵树,我们需要什么“原材料”?

假设我给你一些节点,比如 A, B, C。我告诉你 A 是根节点。那么 BC 在哪里呢?

  • 可能是 B 是左孩子,C 是右孩子。

  • 也可能是 B 是左孩子,CB 的左孩子。

  • 还可能是 BC 都是 A 的右孩子(形成一个链表)。

显然,只告诉我节点数据是不够的,因为它缺乏结构信息

那么,我们上一节学到的“遍历序列”是不是一种结构信息呢?我们来试试。

数据结构:二叉树的遍历 (Binary Tree Traversals)-CSDN博客

假设我告诉你,一棵树的前序遍历序列是 A B C。这棵树长什么样?

  • A 肯定是根节点,因为前序遍历第一个就是根。

  • BA 的左孩子吗?如果是,那么 C 可能是 A 的右孩子,也可能是 B 的左/右孩子。

  • BA 的右孩子吗?...

    A/ \B   C
    A/B/
C
    A/B\C

你会发现,只靠一种遍历序列,我们无法唯一地确定一棵树的结构。 信息还是不够!

核心矛盾: 遍历序列是一个一维的、线性的数据。而树是一个二维的、非线性的结构。

从一维信息恢复二维结构,必然会丢失信息,导致不确定性。

解决方案: 那么,我们需要补充什么样的信息才能消除这种不确定性呢?

我们需要两种不同规则的遍历序列,用它们互相配合,锁定每一个节点的位置。


推导“最佳拍档”——哪两种遍历序列能行?

我们来分析一下上一节学到的三种遍历序列的“特长”:

  1. 前序遍历 (DLR: 根-左-右): 序列的第一个元素永远是当前这棵(子)树的根节点。

  2. 后序遍历 (LRD: 左-右-根):序列的最后一个元素永远是当前这棵(子)树的根节点。

  3. 中序遍历 (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,找到 BB 左边是 D,右边是 E

  • 结论:DB 的左孩子,EB 的右孩子。

  • 到这里,DE 都已经是单个节点(叶子节点),它们的左右子树都是 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;
}

关于后序+中序的思考:

这个逻辑是完全一样的。区别在于:

  1. 后序遍历的根在序列的末尾,所以你要从后往前处理后序序列。

  2. 因为根是最后处理的,所以你的递归调用顺序应该是先构建右子树,再构建左子树,最后才把它们连接到根上。

这个推导过程清晰地展示了如何从一个根本性的问题(什么信息能唯一确定一棵树)出发,通过逻辑分析和推理,一步步设计出算法,并最终转化为简洁、高效的递归代码。

http://www.dtcms.com/a/334431.html

相关文章:

  • 【图论】分层图 / 拆点
  • 算法训练营day53 图论④ 110.字符串接龙、105.有向图的完全可达性、106.岛屿的周长
  • 树、哈夫曼树以及二叉树的各种操作
  • 【CF】Day128——杂题 (图论 + 贪心 | 集合 + 贪心 + 图论 | 二分答案 + 贪心)
  • 【完整源码+数据集+部署教程】植物病害检测系统源码和数据集:改进yolo11-RFAConv
  • ceph pools have too many placement groups
  • 0815 UDP通信协议TCP并发服务器
  • 【Java web】HTTP 协议详解
  • H20芯片与中国的科技自立:一场隐形的博弈
  • RK3568 NPU RKNN(四):RKNN-ToolKit2性能和内存评估
  • web-apache优化
  • Java Web部署
  • Python:如何在Pycharm中显示geemap地图?
  • Flutter InheritedWidget 详解:从生命周期到数据流动的完整解析
  • 开源数据发现平台:Amundsen Frontend Service React 配置 Flask 配置 Superset 预览集成
  • 教育行业破局:课程答疑智能体如何用“按次付费+算力限制”实现精准变现,可独立部署(井云智能体封装系统)
  • NLP:Transformer模型构建
  • 数字分类:机器学习经典案例解析
  • 通过rss订阅小红书,程序员将小红书同步到自己的github主页
  • MCU软件架构---RAM分区设计原则(四)
  • PyTorch生成式人工智能——使用MusicGen生成音乐
  • 二叉树的三种遍历方法
  • List容器:特性与操作使用指南
  • VS Code配置MinGW64编译GLPK(GNU Linear Programming Kit)开源库
  • 实现Android图片手势缩放功能的完整自定义View方案,结合了多种手势交互功能
  • 纸板制造制胶工艺学习记录4
  • Redis集群设计实战:从90%缓存命中率看高并发系统优化
  • Windows常见文件夹cache的作用还有其他缓存类型文件夹的作用
  • backward怎么计算的是torch.tensor(2.0, requires_grad=True)变量的梯度
  • 【论文阅读】Multimodal Graph Contrastive Learning for Multimedia-based Recommendation