当前位置: 首页 > 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.lryc.cn/news/622720.html

相关文章:

  • OpenJDK 17的C1和C2编译器实现中,方法返回前插入安全点(Safepoint Poll)的机制
  • 【lubancat】鲁班猫4实现开机后自动播放视频
  • 攻击者如何毒害人工智能工具和防御系统
  • 罗技MX Anywhere 2S鼠标修复记录
  • 【攻防实战】红队攻防之Goby反杀
  • 云原生俱乐部-RH124知识点总结(1)
  • PHP反序列化的CTF题目环境和做题复现第2集_POP链构造
  • 布隆过滤器的原理及使用
  • 基于STM32的智能书房系统设计与实现
  • 从阿里一面真题看:索引树搜索次数背后的逻辑
  • Sklearn 机器学习 邮件文本分类 加载邮件数据
  • 防御保护16
  • Redis集群设计实战:从90%缓存命中率看高并发系统优化
  • Rust 语法基础教程
  • AI应用安全 - Prompt注入攻击
  • [1Prompt1Story] 滑动窗口机制 | 图像生成管线 | VAE变分自编码器 | UNet去噪神经网络
  • 【LeetCode题解】LeetCode 35. 搜索插入位置
  • Dify实战应用指南(上传需求稿生成测试用例)
  • Jenkins常见问题及解决方法
  • STM32 延时函数详解
  • 343整数拆分
  • 后量子密码算法ML-DSA介绍及开源代码实现
  • 【Qt开发】常用控件(四)
  • 算法提升之树上问题-(tarjan求LCA)
  • 基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
  • MySQL 配置性能优化赛技术文章
  • Win10、Win11电脑之间无法Ping通解决办法
  • 设计模式之【快速通道模式】,享受VIP的待遇
  • Python - 100天从新手到大师:第十一天常用数据结构之字符串
  • OpenCV Python——图像拼接(一)(图像拼接原理、基础知识、单应性矩阵 + 图像变换 + 拼接)