数据结构-二叉搜索树(BST)

目录

什么是二叉搜索树

二叉搜索树的特性 

(1)顺序性

(2)局限性

二叉搜索树的应用 

二叉搜索树的操作

(1)查找节点

(2)插入节点

(3)删除节点

(4)中序遍历 


什么是二叉搜索树

        如图所示,二叉搜索树(binary search tree)满足以下条件。

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

二叉搜索树

        二分搜索树有着高效的插入、删除、查询操作。

        平均时间的时间复杂度为 O(log n),最差情况为 O(n)。二分搜索树与堆不同,不一定是完全二叉树,底层不容易直接用数组表示故采用链表来实现二分搜索树。只有在高频添加、低频查找删除数据的场景下,数组比二叉搜索树的效率更高。

查找元素插入元素删除元素
普通数组O(n)O(n)O(n)
顺序数组O(logn)O(n)O(n)
二分搜索树O(logn)O(logn)O(logn)

二叉搜索树的特性 

(1)顺序性

        二分搜索树可以当做查找表的一种实现。

        我们使用二分搜索树的目的是通过查找 key 马上得到 value。minimum、maximum、successor(后继)、predecessor(前驱)、floor(地板)、ceil(天花板、rank(排名第几的元素)、select(排名第n的元素是谁)这些都是二分搜索树顺序性的表现。

(2)局限性

        二分搜索树在时间性能上是具有局限性的。

        在理想情况下,二叉搜索树是“平衡”的,这样就可以在 log⁡𝑛 轮循环内查找任意节点。

        然而,如果我们在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为如图所示的链表,相应的,二叉搜索树的查找操作是和这棵树的高度相关的,而此时这颗树的高度就是这颗树的节点数 n,这时各种操作的时间复杂度也会退化为 𝑂(𝑛) 。

二叉搜索树退化


二叉搜索树的应用 

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

二叉搜索树的操作

(1)查找节点

        给定目标节点值 num ,可以根据二叉搜索树的性质来查找。如图 7-17 所示,我们声明一个节点 cur ,从二叉树的根节点 root 出发,循环比较节点值 cur.val 和 num 之间的大小关系。

  • 若 cur.val < num ,说明目标节点在 cur 的右子树中,因此执行 cur = cur.right 。
  • 若 cur.val > num ,说明目标节点在 cur 的左子树中,因此执行 cur = cur.left 。
  • 若 cur.val = num ,说明找到目标节点,跳出循环并返回该节点。

bst_search_step4

        二叉搜索树的查找操作与二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 𝑂(log⁡𝑛) 时间。示例代码如下:

/* 查找节点 */
TreeNode *search(BinarySearchTree *bst, int num) {TreeNode *cur = bst->root;// 循环查找,越过叶节点后跳出while (cur != NULL) {if (cur->val < num) {// 目标节点在 cur 的右子树中cur = cur->right;} else if (cur->val > num) {// 目标节点在 cur 的左子树中cur = cur->left;} else {// 找到目标节点,跳出循环break;}}// 返回目标节点return cur;
}

        通过查找节点的方法,我们可以完成98. 验证二叉搜索树 - 力扣(LeetCode)

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     struct TreeNode *left;*     struct TreeNode *right;* };*/
bool isValidBSTHelper(struct TreeNode* root, long min_val, long max_val) {// 如果根节点为空,直接返回 true,因为空树也是 BSTif (root == NULL) {return true;}// 检查当前节点值是否在范围内if (root->val <= min_val || root->val >= max_val) {return false;}// 递归检查左右子树,更新范围return isValidBSTHelper(root->left, min_val, root->val) && isValidBSTHelper(root->right, root->val, max_val);
}bool isValidBST(struct TreeNode* root) {// 使用 long 类型的最小值和最大值作为初始范围return isValidBSTHelper(root, LONG_MIN, LONG_MAX);
}

(2)插入节点

        给定一个待插入元素 num ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作流程如图所示。

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

在二叉搜索树中插入节点

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

  • 二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插入,直接返回。
  • 为了实现插入节点,我们需要借助节点 pre 保存上一轮循环的节点。这样在遍历至 None 时,我们可以获取到其父节点,从而完成节点插入操作。

        代码范例如下,与查找节点相同,插入节点使用 𝑂(log⁡𝑛) 时间。

/* 插入节点 */
void insert(BinarySearchTree *bst, int num) {// 若树为空,则初始化根节点if (bst->root == NULL) {bst->root = newTreeNode(num);return;}TreeNode *cur = bst->root, *pre = NULL;// 循环查找,越过叶节点后跳出while (cur != NULL) {// 找到重复节点,直接返回if (cur->val == num) {return;}pre = cur;if (cur->val < num) {// 插入位置在 cur 的右子树中cur = cur->right;} else {// 插入位置在 cur 的左子树中cur = cur->left;}}// 插入节点TreeNode *node = newTreeNode(num);if (pre->val < num) {pre->right = node;} else {pre->left = node;}
}

        通过插入节点的方法,我们可以完成701. 二叉搜索树中的插入操作 - 力扣(LeetCode)

struct TreeNode* createTreeNode(int val) {struct TreeNode* ret = malloc(sizeof(struct TreeNode));// 设置节点值ret->val = val;// 左右子节点为空ret->left = ret->right = NULL;// 返回新创建的节点return ret;
}struct TreeNode* insertIntoBST(struct TreeNode* root, int val) {// 如果根节点为空,直接将新节点作为根节点返回if (root == NULL) {root = createTreeNode(val);return root;}// 定义游标节点为根节点struct TreeNode* pos = root;// 循环查找插入位置while (pos != NULL) {// 如果 val 小于当前节点值if (val < pos->val) {// 如果当前节点的左子节点为空,将新节点插入左子节点位置if (pos->left == NULL) {pos->left = createTreeNode(val);break;} else {// 否则继续向左子树查找插入位置pos = pos->left;}} else {// 如果 val 大于等于当前节点值// 如果当前节点的右子节点为空,将新节点插入右子节点位置if (pos->right == NULL) {pos->right = createTreeNode(val);break;} else {// 否则继续向右子树查找插入位置pos = pos->right;}}}// 返回根节点return root;
}

(3)删除节点

        先在二叉树中查找到目标节点,再将其删除。与插入节点类似,我们需要保证在删除操作完成后,二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足。因此,我们根据目标节点的子节点数量,分 0、1 和 2 三种情况,执行对应的删除节点操作。

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

在二叉搜索树中删除节点(度为 0 )

        如下图所示,当待删除节点的度为 1 时,将待删除节点替换为其子节点即可。

在二叉搜索树中删除节点(度为 1 )

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

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

  1. 找到待删除节点在“中序遍历序列”中的下一个节点,记为 tmp 。
  2. 用 tmp 的值覆盖待删除节点的值,并在树中递归删除节点 tmp 。

bst_remove_case3_step4

        删除节点操作同样使用 𝑂(log⁡𝑛) 时间,其中查找待删除节点需要 𝑂(log⁡𝑛) 时间,获取中序遍历后继节点需要 𝑂(log⁡𝑛) 时间。示例代码如下:

/* 删除节点 */
// 由于引入了 stdio.h ,此处无法使用 remove 关键词
void removeItem(BinarySearchTree *bst, int num) {// 若树为空,直接提前返回if (bst->root == NULL)return;TreeNode *cur = bst->root, *pre = NULL;// 循环查找,越过叶节点后跳出while (cur != NULL) {// 找到待删除节点,跳出循环if (cur->val == num)break;pre = cur;if (cur->val < num) {// 待删除节点在 root 的右子树中cur = cur->right;} else {// 待删除节点在 root 的左子树中cur = cur->left;}}// 若无待删除节点,则直接返回if (cur == NULL)return;// 判断待删除节点是否存在子节点if (cur->left == NULL || cur->right == NULL) {/* 子节点数量 = 0 or 1 */// 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点TreeNode *child = cur->left != NULL ? cur->left : cur->right;// 删除节点 curif (pre->left == cur) {pre->left = child;} else {pre->right = child;}// 释放内存free(cur);} else {/* 子节点数量 = 2 */// 获取中序遍历中 cur 的下一个节点TreeNode *tmp = cur->right;while (tmp->left != NULL) {tmp = tmp->left;}int tmpVal = tmp->val;// 递归删除节点 tmpremoveItem(bst, tmp->val);// 用 tmp 覆盖 curcur->val = tmpVal;}
}

        除了迭代方法外,我们还可以使用递归方法来删除节点,下面的力扣题给出的方法就是递归方法。450. 删除二叉搜索树中的节点 - 力扣(LeetCode)

// 从二叉搜索树 root 中删除值为 key 的节点
struct TreeNode* deleteNode(struct TreeNode* root, int key) {// 如果根节点为空,直接返回 NULLif (root == NULL) {return NULL;}// 如果 key 小于当前节点值,递归删除左子树中的节点if (root->val > key) {root->left = deleteNode(root->left, key);return root;}// 如果 key 大于当前节点值,递归删除右子树中的节点if (root->val < key) {root->right = deleteNode(root->right, key);return root;}// 如果当前节点值等于 keyif (root->val == key) {// 如果当前节点没有左右子节点,直接返回 NULLif (!root->left && !root->right) {return NULL;}// 如果只有右子节点,返回右子节点if (!root->right) {return root->left;}// 如果只有左子节点,返回左子节点if (!root->left) {return root->right;}// 如果既有左子节点又有右子节点// 找到右子树中最小的节点作为当前节点的替代节点struct TreeNode *successor = root->right;while (successor->left) {successor = successor->left;}// 递归删除右子树中的最小节点root->right = deleteNode(root->right, successor->val);// 将替代节点的右子树连接到当前节点的右子树successor->right = root->right;// 将替代节点的左子树连接到当前节点的左子树successor->left = root->left;// 返回替代节点作为当前节点的父节点的子节点return successor;}// 返回根节点return root;
}

(4)中序遍历 

        如图所示,二叉树的中序遍历遵循“左 → 根 → 右”的遍历顺序,而二叉搜索树满足“左子节点 < 根节点 < 右子节点”的大小关系。

        这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:二叉搜索树的中序遍历序列是升序的。

        利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 𝑂(𝑛) 时间,无须进行额外的排序操作,非常高效。

二叉搜索树的中序遍历序列

        利用二叉搜索树中序遍历升序,我们可以完成 530. 二叉搜索树的最小绝对差

void traversal(struct TreeNode* cur, struct TreeNode** pre, int *result) {if (cur == NULL) return;//BST中序遍历是升序traversal(cur->left, pre, result);   // 左if (*pre != NULL){       // 中*result = fmin(*result, cur->val - (*pre)->val);}*pre = cur; // 记录前一个traversal(cur->right, pre, result);  // 右
}int getMinimumDifference(struct TreeNode* root) {int result = 114514;struct TreeNode* pre = NULL; // 初始值为NULLtraversal(root, &pre, &result); // 传递pre的指针的指针return result;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/4784.shtml

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

相关文章

【EI会议|稳定检索】2024年航空航天、空气动力学与自动化工程国际会议(ICAAAE 2024)

2024 International Conference on Aerospace, Aerodynamics, and Automation Engineering 一、大会信息 会议名称&#xff1a;2024年航空航天、空气动力学与自动化工程国际会议 会议简称&#xff1a;ICAAAE 2024 收录检索&#xff1a;提交Ei Compendex,CPCI,CNKI,Google Schol…

WebGL开发框架比较

WebGL开发框架提供了一套丰富的工具和API&#xff0c;使得在Web浏览器中创建和操作3D图形变得更加容易。以下是一些流行的WebGL开发框架及其各自的优缺点。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.Three.js 优点&#xff1a…

装饰器模式、代理模式、适配器模式对比

装饰器模式、代理模式和适配器模式都是结构型设计模式&#xff0c;它们的主要目标都是将将类或对象按某种布局组成更大的结构&#xff0c;使得程序结构更加清晰。这里将装饰器模式、代理模式和适配器模式进行比较&#xff0c;主要是因为三个设计模式的类图结构相似度较高、且功…

VitePress 构建的博客如何部署到 github 平台?

VitePress 构建的博客如何部署到 github 平台&#xff1f; 1. 新建 github 项目 2. 构建 VitePress 项目 2.1. 设置 config 中的 base 由于我们的项目名称为 vite-press-demo&#xff0c;所以我们把 base 设置为 /vite-press-demo/&#xff0c;需注意前后 / export default…

Docker容器:搭建LNMP架构

目录 前言 1、任务要求 2、Nginx 镜像创建 2.1 建立工作目录并上传相关安装包 2.2 编写 Nginx Dockerfile 脚本 2.3 准备 nginx.conf 配置文件 2.4 生成镜像 2.5 创建 Nginx 镜像的容器 2.6 验证nginx 3、Mysql 镜像创建 3.1 建立工作目录并上传相关安装包 3.2 编写…

java案例-读取xml文件

需求 导入依赖 <dependencies><!-- dom4j --><dependency><groupId>dom4j</groupId><artifactId>dom4j</artifactId><version>1.6.1</version></dependency> </dependencies>代码 SAXReader saxReade…

进迭时空宣布开源RISC-V芯片的AI核心技术

仟江水商业电讯&#xff08;4月29日 北京 委托发布&#xff09;4月29日&#xff0c;在“创芯生生不息——进迭时空2024年度产品发布会”上&#xff0c;进迭时空CEO、创始人&#xff0c;陈志坚博士宣布将开源进迭时空在自研RISC-V AI CPU上的核心技术&#xff0c;包括AI扩展指令…

无人机+集群组网+单兵图传:空地一体化组网技术详解

空地一体化组网技术是一种结合了无人机、集群自组网和单兵图传等多种技术的先进通信解决方案。这种技术方案的主要目的是在前线事故现场和后方指挥中心之间建立一个高效、稳定的通信链路&#xff0c;以确保信息的实时传输和指挥的顺畅进行。 首先&#xff0c;前端视频采集部分&…

自适应信号处理基础及应用——DSP学习笔记五

本专栏的图片内容都来自于老师讲课的PPT&#xff0c;本篇博客只是我个人对于上课内容的知识结构分析和梳理。 导论 自适应系统的定义、特征、形式、举例 特征 非自适应系统 • 固定参数的设计方法 • 假定事先知道了一切可能的输入条件&#xff1b;在这些条件下怎样动作&#…

word 表格 文字 上下居中

问题 word 表格 文字 上下居中 详细问题 笔者进行word 文档编辑&#xff0c;对于表格中的文本内容&#xff0c;如何进行上下居中&#xff1f; 解决方案 步骤1、选中需要进行操作的单元格 步骤2、右键 → \rightarrow →点击表格属性 步骤3、依次点击单元格 → \rightar…

Qt绘图与图形视图之自定义图元实现拖拽、拉伸、旋转功能

往期回顾 Qt绘图与图形视图之移动鼠标手动绘制任意多边形的简单介绍-CSDN博客 Qt绘图与图形视图之场景、视图架构的简单介绍-CSDN博客 Qt绘图与图形视图之基本图元绘制的简单介绍-CSDN博客 Qt绘图与图形视图之自定义图元实现拖拽、拉伸、旋转功能 一、最终效果 实现对自定义图…

应用层协议了解

一 HTTP前置知识 这篇博客会有点长&#xff0c;但对我来说非常有意义&#xff0c;这是我从一无所知到理解网络的重大突破&#xff0c;在前两个月我对网络非常恐惧&#xff0c;还十分不理解什么是网络&#xff0c;什么是协议。接下来先介绍几个概念。 1 流量 我们把数据给别人&…

GitHub Desktop进行汉化

第一步下载github桌面版 官网&#xff1a;安装 GitHub Desktop - GitHub 文档 历史版本&#xff1a;https://github.cn.uptodown.com/windows/versions 本期下载版本3.3.11进行汉化&#xff0c;最新版不一定稳定。 网站打不开的可自取&#xff1a; 3.3.11版本安装包链接&a…

【论文笔记】Language Models are Few-Shot Learners B部分

Language Models are Few-Shot Learners B 部分 回顾一下第一代 GPT-1 &#xff1a; 设计思路是 “海量无标记文本进行无监督预训练少量有标签文本有监督微调” 范式&#xff1b;模型架构是基于 Transformer 的叠加解码器&#xff08;掩码自注意力机制、残差、Layernorm&#…

排序算法:插入、希尔、选择、推排、冒泡、快速、归并排序

排序算法 目录 前言 一、排序的概念 1.1排序的概念 1.2 常见的排序算法 二、常见排序算法的实现 2.1 插入排序 2.2 希尔排序 2.3 选择排序 2.4 堆排序 2.5 冒泡排序 2.6 快速排序 2.6.1 hoare版本 2.6.2 前后指针版本 2.6.3 非递归版本 2.7 归并排序 归并排序 2.8 计数排序 三、…

【mysql】mysql中的数据类型知多少?

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

在VMware中如何快速克隆出一台虚拟机

鉴于经常需要使用新开虚拟机出来给开发团队用于测试部署&#xff0c;之前没有克隆功能之前都是需要一台装一个操作系统&#xff0c;无论是linux、windows server版或 windows 10 版&#xff0c;整个安装过程下来还是要一个来小时。后来做了装了十多次以后&#xff0c;想着试一下…

《微服务设计》读书笔记

此为阅读纽曼《微服务设计》一书后总结的读书笔记&#xff0c;点此处下载PDF文档。 一、微服务的概念 微服务&#xff08;或称微服务架构&#xff09;是一种云原生架构方法&#xff0c;其核心思想在于将单个应用拆分为众多 小型、松散耦合的服务&#xff0c;服务之间均通过网…

利用Triple U.Net结构对冷冻切片HE染色组织学图像进行核实例分割

利用Triple U.Net结构对冷冻切片H&E染色组织学图像进行核实例分割 摘要IntroductionRelated WorksDatasetProposed MethodologyDataset PreparationSegmentation BranchLoss FunctionWatershed Algorithm Nuclei Instance Segmentation of Cryosectioned H&E Stained H…

基于Springboot的校园博客系统

基于SpringbootVue的校园博客系统 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页 文章信息 系统公告 后台登录 后台首页 博主管理 文章分类管理 文章信息管理 举报投诉管…