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

二叉树的介绍

二叉树

  • 本文讲述了二叉树的类型,及其两种表示方法(链式、数组式)和三种递归式遍历方法(前序、中序、后序);之后,介绍了二叉搜索树的常见操作(查找、插入、删除)及其应用(中序遍历二叉搜索树可以将节点按照升序进行排序,平均时间复杂度为log(n) )。

「二叉树 binary tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的 分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用。

  • 二叉树节点结构体

    /* 二叉树节点结构体 */
    struct TreeNode {int val; 			// 节点值TreeNode *left; 	// 左子节点指针TreeNode *right; 	// 右子节点指针TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}	//构建函数
    };
    
  • 常见术语

    • 根节点:位于二叉树顶层的节点,没有父节点。
    • 叶节点:没有子节点的节点,其两个指针均指向 None
    • 边 :连接两个节点的线段,即节点引用(指针)。
    • 节点所在的层:从顶至底递增,根节点所在层为 1 。
    • 节点的度:节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
    • 二叉树的高度:从根节点到最远叶节点所经过的边的数量。
    • 节点的深度:从根节点到该节点所经过的边的数量。
    • 节点的高度:从最远叶节点到该节点所经过的边的数量。
    • image-20240824163418660

1. 二叉树类型

1.1 完美(满)二叉树 perfect binary tree

除了最底层外,其余所有层的节点都被完全填满。在完美二叉树中,除叶节点外,其余所有节点的度都为 2 ;若树高度为 ℎ ,则节点总数为 2 ℎ+1 − 1 ,呈现标准的指数级关系, 反映了自然界中常见的细胞分裂现象。

image-20240824163728400

1.2 完全二叉树 complete binary tree

只有最底层的节点未被填满,且最底层节点尽量靠左。

image-20240824163848236

1.3 完满二叉树 full binary tree

除了叶节点之外,其余所有节点都有两个子节点。

image-20240824164019093

1.4 平衡二叉树 balanced binary tree

任意节点的左子树和右子树的高度之差的绝对值不超过 1

image-20240824164301697

2. 二叉树的表示

2.1 链表表示法
/* 1.初始化二叉树 */
// 初始化节点
TreeNode* n1 = new TreeNode(1);
TreeNode* n2 = new TreeNode(2);
TreeNode* n3 = new TreeNode(3);
TreeNode* n4 = new TreeNode(4);
TreeNode* n5 = new TreeNode(5);//构建引用指向(即指针)
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;/* 2.插入与删除节点 */
TreeNode* P = new TreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1->left = P;
P->left = n2;
// 删除节点 P
n1->left = n2;/*3.前序遍历 */
void preOrder(TreeNode *root) {if (root == nullptr)return;// 访问优先级:根节点 -> 左子树 -> 右子树vec.push_back(root->val);preOrder(root->left);preOrder(root->right);
}/* 4.中序遍历 */
void inOrder(TreeNode *root) {if (root == nullptr)return;// 访问优先级:左子树 -> 根节点 -> 右子树inOrder(root->left);vec.push_back(root->val);inOrder(root->right);
}/* 5.后序遍历 */
void postOrder(TreeNode *root) {if (root == nullptr)return;// 访问优先级:左子树 -> 右子树 -> 根节点postOrder(root->left);postOrder(root->right);vec.push_back(root->val);
}
2.2 数组表示法
  • 我们将所有节点按照层序遍历的顺序存储在一个数组中,则每个节点都对应唯一的数组索引。可以推导出父节点索引与子节点索引之间的“映射公式”:若节点的索引为 𝑖 ,则该节点的左子节点索引为 2𝑖+1,右子节点索引为2𝑖+2,父节点索引为(𝑖-1)/2。但在二叉树中通常存在许多 None ,需要在层序遍历序列中显式地写出所有 None (例如,使用 int 最大值 INT_MAX 标记空位),这样处理后,层 序遍历序列就可以唯一表示二叉树了。

image-20240824175019653

/* 二叉树的数组表示 */
// 使用 int 最大值 INT_MAX 标记空位
vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
  • 值得说明的是,完全二叉树非常适合使用数组来表示。回顾完全二叉树的定义,None 只出现在最底层且靠右的位置,因此所有 None 一定出现在层序遍历序列的末尾。 这意味着使用数组表示完全二叉树时,可以省略存储所有 None ,非常方便。

image-20240824175148420

  • 以下代码实现了一个基于数组表示的二叉树

    /* 数组表示下的二叉树类 */
    class ArrayBinaryTree {
    public:/* 构造方法 */ArrayBinaryTree(vector<int> arr) { tree = arr; }/* 节点数量 */int size() {return tree.size();}/* 获取索引为 i 节点的值 */int val(int i) {// 若索引越界,则返回 INT_MAX ,代表空位if (i < 0 || i >= size())return INT_MAX;return tree[i];}/* 获取索引为 i 节点的左子节点的索引 */int left(int i) {return 2 * i + 1;}/* 获取索引为 i 节点的右子节点的索引 */int right(int i) {return 2 * i + 2;}/* 获取索引为 i 节点的父节点的索引 */int parent(int i) {return (i - 1) / 2;}/* 层序遍历 */vector<int> levelOrder() {vector<int> res;// 直接遍历数组for (int i = 0; i < size(); i++) {if (val(i) != INT_MAX)res.push_back(val(i));}return res;}/* 前序遍历 */vector<int> preOrder() {vector<int> res;dfs(0, "pre", res);return res;}/* 中序遍历 */vector<int> inOrder() {vector<int> res;dfs(0, "in", res);return res;}/* 后序遍历 */vector<int> postOrder() {vector<int> res;dfs(0, "post", res);return res;}private:vector<int> tree;/* 深度优先遍历 deep first search */void dfs(int i, string order, vector<int> &res) {// 若为空位,则返回if (val(i) == INT_MAX)return;// 前序遍历if (order == "pre")res.push_back(val(i));dfs(left(i), order, res);// 中序遍历if (order == "in")res.push_back(val(i));dfs(right(i), order, res);// 后序遍历if (order == "post")res.push_back(val(i));}
    };
    
  • 二叉树的数组表示主要有以下优点:

    • 数组存储在连续的内存空间中,对缓存友好,访问与遍历速度较快。
    • 不需要存储指针,比较节省空间。
    • 允许随机访问节点。
  • 然而,数组表示也存在一些局限性:

    • 数组存储需要连续内存空间,因此不适合存储数据量过大的树。
    • 增删节点需要通过数组插入与删除操作实现,效率较低。
    • 当二叉树中存在大量 None 时,数组中包含的节点数据比重较低,空间利用率较低。

3. 二叉搜索树 binary search tree

二叉搜索树满足以下条件:

  1. 对于根节点,左子树中所有节点的值 < 根节点的值 < 右子树中所有节点的值。
  2. 任意节点的左、右子树也是二叉搜索树,即同样满足条件1。

image-20240824175933375

  • 我们将二叉搜索树封装为一个类 ArrayBinaryTree ,并声明一个成员变量 root ,指向树的根节点。
3.1 二叉搜索树的操作
1. 查找节点
  • 给定要查找的目标节点值 num ,可以根据二叉搜索树的性质来查找。先声明一个节点 cur ,从二叉树的根节点 root 出发,循环比较节点值 cur.val 和 num 之间的大小关系:

    • 若 cur.val < num ,说明目标节点在 cur 的右子树中,因此执行 cur = cur.right 。
    • 若 cur.val > num ,说明目标节点在 cur 的左子树中,因此执行 cur = cur.left 。
    • 若 cur.val = num ,说明找到目标节点,跳出循环并返回该节点。
  • 二叉搜索树的查找操作与二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的 高度,当二叉树平衡时,使用 𝑂(log 𝑛) 时间。

    /* 查找节点 */
    TreeNode *search(int num) {TreeNode *cur = root;// 循环查找,越过叶节点后跳出while (cur != nullptr) {// 目标节点在 cur 的右子树中if (cur->val < num)cur = cur->right;// 目标节点在 cur 的左子树中else if (cur->val > num)cur = cur->left;// 找到目标节点,跳出循环elsebreak;}// 返回目标节点return cur;
    }
    
2. 插入节点
  • 给定一个待插入元素 num ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作流程如图所示:

    • 查找插入位置:与查找操作相似,从根节点出发,根据当前节点值和 num 的大小关系循环向下搜索,直 到越过叶节点(遍历至 None )时跳出循环。
    • 在该位置插入节点:初始化节点 num ,将该节点置于 None 的位置。

    image-20240824180843901

  • 在代码实现中,需要注意以下两点:

    • 二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插 入,直接返回。
    • 为了实现插入节点,我们需要借助节点 pre 保存上一轮循环的节点。这样在遍历至 None 时,我们可以 获取到其父节点,从而完成节点插入操作。
    /* 插入节点 */
    void insert(int num) {// 若树为空,则初始化根节点if (root == nullptr) {root = new TreeNode(num);return;}TreeNode *cur = root, *pre = nullptr;// 循环查找,越过叶节点后跳出while (cur != nullptr) {if (cur->val == num)	// 找到重复节点,直接返回return;pre = cur;if (cur->val < num)		// 插入位置在 cur 的右子树中cur = cur->right;else					// 插入位置在 cur 的左子树中cur = cur->left;}// 插入节点TreeNode *node = new TreeNode(num);if (pre->val < num)pre->right = node;elsepre->left = node;
    }
    
    • 与查找节点相同,插入节点使用 𝑂(log 𝑛) 时间。
3. 删除节点
  • 先在二叉树中查找到目标节点,再将其从二叉树中删除。 与插入节点类似,我们需要保证在删除操作完成后,二叉搜索树的性质仍然满足。 因此,我们需要根据目标节点的子节点数量,共分为 0、1 和 2 这三种情况,执行对应的删除节点操作。 如图所示:

    • 当待删除节点的度为 0 时,表示该节点是叶节点,可以直接删除。

      image-20240824223244599

    • 当待删除节点的度为 1 时,将待删除节点替换为其子节点即可。

      image-20240824223332615

    • 当待删除节点的度为 2 时,我们无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树 “左 < 根 < 右”的性质,因此这个节点可以是右子树的最小节点或左子树的最大节点

      • 假设我们选择右子树的最小节点(即中序遍历的下一个节点),则删除操作流程如下:

        • 找到待删除节点在“中序遍历序列”中的下一个节点,记为 tmp 。

        • 将 tmp 的值覆盖待删除节点的值,并在树中递归删除节点 tmp 。

          image-20240824223551671

        代码如下:

        /* 删除节点 */
        void remove(int num) {    if (root == nullptr)	// 若树为空,直接提前返回return;TreeNode *cur = root, *pre = nullptr;while (cur != nullptr) {	// 循环查找,越过叶节点后跳出        if (cur->val == num)	// 找到待删除节点,跳出循环break;pre = cur;if (cur->val < num)		// 待删除节点在 cur 的右子树中cur = cur->right;else					// 待删除节点在 cur 的左子树中cur = cur->left;}if (cur == nullptr)			// 若无待删除节点,则直接返回return;// 待删除节点的子节点数量 = 0 或 1if (cur->left == nullptr || cur->right == nullptr) {// 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点TreeNode *child = cur->left != nullptr ? cur->left : cur->right;// 删除节点 curif (cur != root) {if (pre->left == cur)pre->left = child;elsepre->right = child;} else {root = child;	// 若删除节点为根节点,则重新指定根节点}// 释放内存delete cur;}else {		// 子节点数量 = 2// 获取中序遍历中 cur 的下一个节点TreeNode *tmp = cur->right;while (tmp->left != nullptr) {tmp = tmp->left;}int tmpVal = tmp->val;// 递归删除节点 tmpremove(tmp->val);// 用 tmp 覆盖 curcur->val = tmpVal;}
        }
        
4. 节点遍历
  • 二叉搜索树满足“左子节点 < 根 节点 < 右子节点”的大小关系,二叉树的中序遍历遵循“左 → 根 → 右”的遍历顺序,这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:二叉 搜索树的中序遍历序列是升序的。利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 𝑂(𝑛) 时间,无须进行额外的排序操作, 非常高效。
5. 二叉搜索树的应用
  • 二叉搜索树的各项操作(查找、插入、删除)的时间复杂度都是对数阶,具有稳定且高效的性能表现。但如果我们在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为链表,这时各种操作的时间复杂度也会退化为 𝑂(𝑛) 。

    image-20240824224535855

  • 二叉搜索树常见应用:

    • 用作系统中的多级索引,实现高效的查找、插入、删除操作。
    • 作为某些搜索算法的底层数据结构。
      节点 < 右子节点”的大小关系,二叉树的中序遍历遵循“左 → 根 → 右”的遍历顺序,这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:二叉 搜索树的中序遍历序列是升序的。利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 𝑂(𝑛) 时间,无须进行额外的排序操作, 非常高效。
5. 二叉搜索树的应用
  • 二叉搜索树的各项操作(查找、插入、删除)的时间复杂度都是对数阶,具有稳定且高效的性能表现。但如果我们在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为链表,这时各种操作的时间复杂度也会退化为 𝑂(𝑛) 。

    [外链图片转存中…(img-kX4RgCAL-1724514112382)]

  • 二叉搜索树常见应用:

    • 用作系统中的多级索引,实现高效的查找、插入、删除操作。
    • 作为某些搜索算法的底层数据结构。
    • 用于存储数据流,以保持其有序状态。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 2024.8.26
  • 景联文科技:专业人像采集服务,助力人像采集在多领域应用
  • npm阿里云制品仓库
  • C++竞赛初阶L1-14-第六单元-数组(31~33课)542: T456472 数组逆序重存放
  • 使用 ECharts 进行数据可视化
  • Python单例模式:深入解析与应用
  • vue+uniapp
  • 如何使用ssm实现ssm框架的购物网站+vue
  • SpringBoot项目多线程实现定时任务-只需要三步
  • 通过Python绘制不同数据类型适合的可视化图表
  • CSS文本属性与字体
  • 秋招力扣Hot100刷题总结——堆
  • Java和C#哪个更适合大型项目?
  • 17 深入理解 C 语言 main 函数:返回值意义、命令行参数接收、跨环境差异及CMD乱码解决
  • anaconda的power shell和prompt有什么区别?
  • 0x05 Python数据分析,Anaconda八斩刀
  • canvas 高仿 Apple Watch 表盘
  • Go 语言编译器的 //go: 详解
  • Linux编程学习笔记 | Linux多线程学习[2] - 线程的同步
  • Shadow DOM 内部构造及如何构建独立组件
  • Transformer-XL: Unleashing the Potential of Attention Models
  • Webpack 4x 之路 ( 四 )
  • 阿里云ubuntu14.04 Nginx反向代理Nodejs
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 如何设计一个比特币钱包服务
  • 小程序开发之路(一)
  • 用jquery写贪吃蛇
  • 职业生涯 一个六年开发经验的女程序员的心声。
  • d²y/dx²; 偏导数问题 请问f1 f2是什么意思
  • ionic异常记录
  • Java数据解析之JSON
  • # 安徽锐锋科技IDMS系统简介
  • $.ajax,axios,fetch三种ajax请求的区别
  • (2)Java 简介
  • (31)对象的克隆
  • (4)事件处理——(7)简单事件(Simple events)
  • (done) ROC曲线 和 AUC值 分别是什么?
  • (正则)提取页面里的img标签
  • .Net6 Api Swagger配置
  • .NET导入Excel数据
  • .NET开发者必备的11款免费工具
  • .NET命令行(CLI)常用命令
  • /bin/bash^M: bad interpreter: No such file ordirectory
  • /var/lib/dpkg/lock 锁定问题
  • @ 代码随想录算法训练营第8周(C语言)|Day57(动态规划)
  • [100天算法】-每个元音包含偶数次的最长子字符串(day 53)
  • [Algorithm][动态规划][子序列问题][最长递增子序列][摆动序列]详细讲解
  • [Angularjs]ng-select和ng-options
  • [C#]winform制作圆形进度条好用的圆环圆形进度条控件和使用方法
  • [CAN] 创建解析CAN报文DBC文件教程
  • [CSDN首发]鱿鱼游戏的具体玩法详细介绍
  • [EFI]NUC11电脑 Hackintosh 黑苹果efi引导文件
  • [Git][分支设计规范]详细讲解
  • [I2C]I2C通信协议详解(二) --- I2C时序及规格指引
  • [LeetCode] 197. 上升的温度