数据结构:构建 (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.pswp.cn/diannao/96131.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/96131.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【STM32】HAL库中的实现(五):ADC (模数转换)

什么是 ADC&#xff08;模数转换器&#xff09; ADC&#xff08;Analog to Digital Converter&#xff09;是将 模拟信号&#xff08;电压&#xff09;转换成数字信号&#xff08;数值&#xff09; 的器件。 在 STM32 中&#xff0c;ADC 通常具有以下特性&#xff1a;特性描述分…

智慧校园中IPTV融合对讲:构建高效沟通新生态

在智慧校园的建设浪潮里&#xff0c;IPTV融合对讲系统宛如一颗璀璨的新星&#xff0c;以其独特的功能和强大的优势&#xff0c;为校园的沟通与管理带来了全新的变革&#xff0c;构建起一个高效、便捷、智能的沟通新生态。从日常沟通层面来看&#xff0c;IPTV融合对讲系统打破了…

智能合约里的 “拒绝服务“ 攻击:让你的合约变成 “死机的手机“

你有没有遇到过手机突然卡死&#xff0c;点什么都没反应的情况&#xff1f;在区块链世界里&#xff0c;智能合约也可能遭遇类似的 "罢工"—— 这就是 "拒绝服务攻击"&#xff08;Denial of Service&#xff0c;简称 DoS&#xff09;。今天用大白话讲讲合约…

安全设计-防止非法移机

前言我们的设备在实际使用过程中&#xff0c;在我们的巡查机制粒度下&#xff0c;发现依然有设备被非法移动到其他非计划点位。因此&#xff0c;我们需要设计一套及时预警&#xff0c;但是对客户无感&#xff0c;不影响业务办理的防范机制。1.方案设计交互图2.方案说明 2.1方案…

OpenHarmony之三方库适配深度实践:从移植到合规的全链路指南

1. 为什么要做三方库适配?——更深层的价值分析 维度 现状痛点 预期收益 深度价值 生态 成熟开源库无法直接运行 复用 10+ 年开源沉淀,提升功能覆盖率 避免生态碎片化:通过标准化适配流程,确保不同厂商对同一库的实现一致 性能 JS 层重实现耗 CPU 原生 C/C++ 加速 3~10 倍 …

2025年09月计算机二级MySQL选择题每日一练——第一期

计算机二级中选择题是非常重要的&#xff0c;所以开始写一个每日一题的专栏。 答案及解析将在末尾公布&#xff01; 今日主题&#xff1a;MySQL 基础概念 1、以下关于数据库的特点中&#xff0c;描述正确的是&#xff08; &#xff09; A. 数据无冗余 B. 数据不可共享&#xff…

JAVA字符串操作——在蓝桥杯的基本应用

我们来系统地梳理一下 Java 中的字符串操作。Java 的字符串操作非常丰富&#xff0c;主要涉及到 String、StringBuilder 和 StringBuffer 这三个核心类。 目录 一、核心类简介 二、String 类的常用操作 1. 创建字符串 2. 获取基本信息 3. 比较字符串 4. 查找与判断 5. 转…

【深度学习基础】PyTorch Tensor生成方式及复制方法详解

目录PyTorch Tensor生成方式及复制方法详解一、Tensor的生成方式&#xff08;一&#xff09;从Python列表/元组创建&#xff08;二&#xff09;从NumPy数组创建&#xff08;三&#xff09;特殊初始化方法&#xff08;四&#xff09;从现有Tensor创建&#xff08;五&#xff09;…

动态规划:入门思考篇

1. 简单类比 假如我们要求全国人数&#xff0c;那么我们只要知道各个省的人数&#xff0c;然后将各个省的人数相加即可&#xff0c;要想知道各个省的人数&#xff0c;只要将这个省下面所有的市人数相加即可&#xff0c;同样&#xff0c;如果想要知道各个市的人数&#xff0c;只…

小杨的 X 字矩阵(举一反三)-洛谷B3865 [GESP202309 二级]

题目描述 小杨想要构造一个 X 字矩阵&#xff08; 为奇数&#xff09;&#xff0c;这个矩阵的两条对角线都是半角加号 &#xff0c;其余都是半角减号 - 。例如&#xff0c;一个 55 的 X 字矩阵如下&#xff1a; --- --- ---- --- --- 请你帮小杨根据给定的 打印出对应的“X …

数据组合与合并:Pandas 数据整合全指南 +缺失值处理

数据组合与合并&#xff1a;Pandas 数据整合全指南在进行数据分析之前&#xff0c;数据清洗与整合是关键步骤。 遵循“整洁数据”&#xff08;Tidy Data&#xff09;原则&#xff1a; 每个观测值占一行每个变量占一列每种观测单元构成一张独立的表格 整理好数据后&#xff0c;常…

c#联合halcon的基础教程(案例:亮度计算、角度计算和缺陷检测)(含halcon代码)

目录 1.环境配置 2.案例一&#xff1a;亮度计算 halcon代码&#xff1a; 主界面代码&#xff1a; 3.案例二&#xff1a; 角度计算 halcon代码&#xff1a; 主界面代码&#xff1a; 4.案例三&#xff1a;缺陷检测 halcon代码&#xff1a; 主界面代码&#xff1a; 通过…

大数据云原生是什么

"云原生"&#xff08;Cloud Native&#xff09;指的是‌利用云计算原生优势&#xff08;弹性、按需服务、自动化、分布式等&#xff09;来设计、构建、部署和运行大数据应用和工作负载的方法论与技术体系‌。它不是简单地“把大数据平台搬到云上”&#xff0c;而是从…

Pytest项目_day16(yaml和parametrize结合)

查询手机号归属地 我们首先可以在YAML文件中定义测试数据 方式一&#xff0c;使用- 注意&#xff1a;当我们需要一次传入两个参数时&#xff0c;需要定义两层迭代&#xff0c;即两层列表不够直观&#xff0c;容易写错 输出的结果为&#xff1a; 然后我们可以将测试数据传入test…

【Nginx指南】从核心原理到生产实践

目录Nginx指南&#xff1a;从核心原理到生产实践引言&#xff1a;Nginx在现代架构中的核心地位一、Nginx核心能力与应用场景1.1 多场景适配的全能型中间件1.2 技术优势&#xff1a;Nginx成为行业标准的关键二、Nginx安装部署&#xff1a;源码编译与包管理方案2.1 源码编译&…

物体检测

目录 1 目标定位 2 地标检测 3 目标检测 4 在卷积网络上实现滑动窗口 5 边界框预测 6 交并比 7 非极大值抑制 8 锚框 9 YOLO算法 10 用u-net进行语义分割 11 转置卷积 12 u-net 结构灵感 1 目标定位 你已经对图片分类有所了解。例如通过这张图片可以识…

es7.x es的高亮与solr高亮查询的对比对比说明

一 solr&es高亮1.1 solr与es高亮功能解释说明&#xff1a;1)高亮配置&#xff1a;fragmentSize(1000) 设置片段长度numOfFragments(1) 指定返回的片段数量preTags() 和 postTags() 设置高亮标记2)字段处理差异&#xff1a;在 ES 中&#xff0c;使用 matchQuery 而非 termQ…

DSP音频算法工程师技能2

一、核心知识准备1. 算法原理3A算法&#xff08;AGC自动增益控制/AEC回声消除/ANS降噪&#xff09;&#xff1a;掌握AEC的NLMS/双讲检测原理&#xff0c;ANS的谱减法/维纳滤波&#xff0c;AGC的压缩曲线设计。熟悉Speex/WebRTC等开源实现。EQ音效&#xff1a;IIR/FIR滤波器设计…

第4章-04-用WebDriver页面元素操作

🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌,CSDN博客专家,阿里云社区专家博主,2023年CSDN全站百大博主。 🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。 🏆本文已收录于专栏:Web爬虫入门与实战精讲,后续完整更新内容如下。 文章…

【计算机视觉与深度学习实战】04基于K-Means聚类的图像分割系统设计与实现

摘要 图像分割作为计算机视觉领域的基础任务,在目标检测、医学影像分析、自动驾驶等众多应用中发挥着关键作用。本文基于K-Means聚类算法设计并实现了一个完整的图像分割系统,该系统集成了多种颜色空间转换、自定义初始化策略、空间特征融合等先进技术。通过Python和Tkinter…