数据结构-二叉搜索树(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 编写…

设计模式(三)、模板方法设计模式

模式定义 模板方法模式(Template Method Pattern):定义一个操作中算法的框架而将一些步骤延迟到子类中&#xff0c;模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤 模式结构 模板方法模式包含如下角色: AbstractClass: 抽象类 ConcreteClass:具体…

SSH远程直连服务器docker容器的jupyter

SSH远程直连服务器docker容器的jupyter 动机&#xff1a;最近在公司服务器使用jupyter出现了点问题&#xff0c;也不知道怎么回事&#xff0c;jupyter lab打开都没问题&#xff0c;但是准备打开一个ipynb文件时就卡住了&#xff0c;啥反应没有&#xff0c;ctrlC 也不能关掉jupy…

JAVA——抽象类

抽象类 Java中的抽象类是一种特殊的类&#xff0c;它不能被实例化&#xff0c;即不能直接创建对象&#xff0c;只能作为其他类的基类&#xff08;父类&#xff09;来使用。抽象类主要用于定义一些通用的属性和方法&#xff0c;这些方法可以在子类中得到具体的实现。 抽象类使…

通过iptables限制docker 容器的运行端口

通过在iptables DOCKER-USER 添加规则&#xff0c;即可实现所有外部网络都无法访问docker中的服务&#xff1a; iptables -I DOCKER-USER -i enp0s3 -j DROP 规则&#xff1a;所有从外部网络进入的数据包&#xff0c;直接被丢弃。 DOCKER-USER链是上述FORWARD链中第一个规则匹…

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绘图与图形视图之自定义图元实现拖拽、拉伸、旋转功能 一、最终效果 实现对自定义图…

HTML中datalist的用法

在HTML中&#xff0c;<datalist>元素用于为<input>元素提供预定义的选项列表&#xff0c;供用户从中选择。通常&#xff0c;它配合<input>元素的list属性一起使用。以下是如何使用<datalist>元素的简单示例&#xff1a; <!DOCTYPE html> <h…

android studio SQLite数据库的简单使用

在Android Studio中使用数据库可以有多种方式&#xff0c;常见的几种方式包括使用SQLite数据库和使用 SQLite数据库 SQLite是一款轻量级的关系型数据库管理系统&#xff0c;在Android中被广泛使用。要在Android Studio中使用SQLite数据库&#xff0c;需要先创建一个数据库帮助…

leetcode刷题:两数之和

面试造火箭&#xff0c;工作拧螺丝&#xff0c;话虽如此&#xff0c;背背八股文&#xff0c;刷刷算法题&#xff0c;也可以提高自己的编程素养&#xff0c;一切目的是为了上岸&#xff0c;在此就不咬文嚼字&#xff0c;追求茴香豆的茴有几种写法了&#xff0c;换句话说&#xf…

vue2 通过设置devServer.port端口号,启动测试服务后端口失效/自动切换端口

vue2 设置端口号小于1990&#xff08;通过设置devServer.port&#xff09; 启动测试服务后端口失效/自动切换端口 问题描述 在配置文件vue.config.js中 module.exports {devServer: {host: localhost,port: 1890,// ...}项目创建后一直使用1890&#xff0c;能正常启动local…

官网设计UI设计需要考虑哪些?

响应式布局&#xff1a; 使用响应式设计技术&#xff0c;确保网站能够自动适应不同设备的屏幕大小和分辨率。这包括使用流式布局、弹性布局和媒体查询等技术。 移动优先&#xff1a; 采用移动优先的设计策略&#xff0c;即首先设计适用于小屏幕设备的界面&#xff0c;然后逐渐…