【数据结构】二叉搜索树(二叉排序树)

🌟🌟作者主页:ephemerals__

🌟🌟所属专栏:数据结构

目录

前言

一、什么是二叉搜索树

二、二叉搜索树的实现

节点

属性和接口的声明

插入

查找

删除

拷贝构造

析构

中序遍历

三、二叉搜索树的性能分析

总结


前言

        之前,我们学习了树这一数据结构,并尝试实现了堆以及链式二叉树的相关功能:

【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)_树形结构 编译器-CSDN博客

【数据结构】二叉树(c语言)(附源码)_二叉树数据结构代码-CSDN博客

今天,我们将在简单二叉树的基础上,进一步学习一种更为复杂的结构——二叉搜索树

        之前我们利用c语言实现了顺序表、链表、二叉树等数据结构。但是在实现一些复杂数据结构时,c语言显得相对繁琐,并且容易出现代码冗余的问题。由于我们现在已经具备了一定的c++代码基础,因此在之后的数据结构学习中,我们将用c++实现。

正文开始

一、什么是二叉搜索树

        二叉搜索树(Binary Search Tree),也叫二叉排序树或者二叉查找树, 是一种特殊的二叉树结构。它或者是一棵空树,或者具有以下特点:

1. 如果根节点的左子树不为空,则左子树上的所有节点都小于等于根节点的值。

2. 如果根节点的右子树不为空,则右子树上的所有节点都大于等于根节点的值。

3. 它的每一棵子树也都符合前两点特性(每棵子树也都是二叉搜索树)。

简单地说,二叉搜索树是一棵中序遍历结果有序的二叉树。

一般情况下,二叉搜索树不允许有相同的值出现。当然,你在设计二叉搜索树的时候也可以允许相同值出现,只要满足二叉搜索树的特性即可。注意:二叉搜索树不一定是一棵完全二叉树。

二、二叉搜索树的实现

        接下来,我们尝试实现二叉搜索树的结构及其常用方法。

节点

        首先是它的节点的结构。二叉搜索树的节点包含一个数据域和指向两孩子的指针:

//节点
template<class K>
struct BSTNode
{K _key;//数据域BSTNode<K>* _left;//指向左孩子的指针BSTNode<K>* _right;//指向右孩子的指针//节点的默认构造BSTNode(const K& key):_key(key), _left(nullptr), _right(nullptr){}
};

可以看到,节点的定义当中,模板参数被命名为"K"。这里的"K"通常表示(key)的类型,特别应用于key_value(键值对,可以理解为一个二元组,每一个key都有对应的value)的场景。一般情况下,key的值是不允许修改的,但是其对应的value可以修改。本次实现二叉搜索树的过程当中,我们仅实现带有key的结构,至于key_value结构,会在之后的AVL树和红黑树中实现。

属性和接口的声明

        接下来是二叉搜索树类的成员变量和成员函数的声明:

//二叉搜索树
template<class K>
class BST
{typedef BSTNode<K> Node;//重命名,简化代码
public://强制生成无参构造BST() = default;//拷贝构造BST(const BST<K>& t);//析构~BST();//插入bool Insert(const K& key);//删除bool Erase(const K& key);//查找Node* Find(const K& key);//中序遍历void Inorder();
private:Node* _root = nullptr;//根节点的指针
};

插入

        现在我们开始实现二叉搜索树节点的插入。为了保持搜索树的特性,有如下插入步骤:

1. 如果树为空,则直接将新节点赋值给根节点的指针。

2. 如果树不为空,则需要根据新节点的值进行搜索,如果某节点的值大于新节点,则向右走;若小于新节点,则向左走。走到空位置之后,按照值插入节点即可。

3. 如果要插入重复的值,则遇到相同值的节点之后可以向左走,也可以向右走,直到找到空位置插入。(我们的实现默认不支持插入重复值

代码实现:

//插入
bool Insert(const K& key)
{if (_root == nullptr)//树为空,直接插入{_root = new Node(key);return true;}else//树不为空,进行搜索{Node* cur = _root;//从根节点开始搜索Node* parent = nullptr;//记录cur的父亲节点while (cur)//走到空为止{if (key < cur->_key)//值小向左走{parent = cur;cur = cur->_left;}else if (key > cur->_key)//值大向右走{parent = cur;cur = cur->_right;}else//这里不允许重复的值插入{return false;}}//此时cur走到空,进行插入cur = new Node(key);if (key < parent->_key)//值小,往左边插{parent->_left = cur;}else//值大,往右边插{parent->_right = cur;}return true;}
}

这里我们定义了一个parent指针,用于记录cur的父亲节点。当cur走到空时,parent刚好指向插入位置的父亲节点,然后与parent值进行大小比较,插入新节点。

查找

        二叉搜索树的特性使其具备了较高的查找效率。查找步骤如下:

1. 从根节点开始,与要查找的值进行比较,如果要找的值比根小,则向左走,否则向右走。循环往复。

2. 如果走到空,则说明不存在,返回空指针。

3. 如果不支持插入重复值,找到后返回节点地址。

4. 如果支持插入重复值,则找到后一般返回中序遍历结果的第一个节点

代码实现:

//查找
Node* Find(const K& key)
{Node* cur = _root;//从根节点开始搜索while (cur)//走到空为止{if (key < cur->_key)//值小向左走{cur = cur->_left;}else if (key > cur->_key)//值大向右走{cur = cur->_right;}else//相等说明找到了,返回节点地址{return cur;}}//走到空说明没找到,返回空指针return nullptr;
}

删除

        删除节点是所有接口中实现难度最大的一个。我们默认需要按值来删除元素,所以要先进行查找,找到之后再删除该节点。删除节点之后,为了保持二叉搜索树的原有特性,就需要调整其他节点。根据被删除的节点,有四种情况需要分析讨论:

1. 被删除节点的左右孩子均为空

2. 被删除的节点只有左孩子

3. 被删除的节点只有右孩子

4.  被删除的节点既有左孩子,又有右孩子

这里说明一下寻找右子树最小值的方法:从右孩子开始,一直访问其左孩子节点,直到访问到空为止。这里访问到的最后一个节点即是右子树的最小值。(左子树最大值也同理)

接下来,我们尝试实现删除节点的操作:

//删除
bool Erase(const K& key)
{//首先按值查找要被删除的节点Node* cur = _root;Node* parent = nullptr;//记录父亲节点while (cur){if (key < cur->_key)//值小向左走{parent = cur;cur = cur->_left;}else if (key > cur->_key)//值大向右走{parent = cur;cur = cur->_right;}else//找到了,开始删除{//有一个左孩子的情况 和 没有孩子的情况if (cur->_right == nullptr){//特殊情况,要删除的节点是根节点if (cur == _root){_root = cur->_left;//直接让根指针指向左孩子}else{//先判断自己是父亲节点的哪一个孩子//然后将左孩子托付给父亲节点if (cur == parent->_left)parent->_left = cur->_left;elseparent->_right = cur->_left;}delete cur;//删除该节点return true;}//有一个右孩子的情况else if (cur->_left == nullptr){//特殊情况,要删除的节点是根节点if (cur == _root){_root = cur->_right;//直接让根指针指向右孩子}else{//先判断自己是父亲节点的哪一个孩子//然后将右孩子托付给父亲节点if (cur == parent->_left)parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;//删除该节点return true;}//有两个孩子的情况else{//首先寻找右子树的最小值Node* rightMin = cur->_right;//从右孩子开始搜索Node* rightMinParent = cur;//记录最小值的父亲节点while (rightMin->_left)//left走到空,rightMin即为右子树的最小值{rightMinParent = rightMin;rightMin = rightMin->_left;//一直向左走,找最小值}//将最小值赋值给被删除节点cur->_key = rightMin->_key;//将替代节点的右孩子托付给父亲//先判断自己是父亲节点的哪一个孩子if (rightMin == rightMinParent->_left)rightMinParent->_left = rightMin->_right;elserightMinParent->_right = rightMin->_right;delete rightMin;//删除该节点return true;}}}//没找到,返回falsereturn false;
}

这里有两点需要注意:

1. 第一种情况可以和第二/第三种情况合并,将空指针托付给父亲节点不会影响整体操作。

2. 第四种情况当中,如果我们找的是右子树的最小值,那么它或是叶子节点(第一种情况),或只有右孩子(第三种情况),不会有左孩子;如果我们找的是左子树的最大值,那么它或是叶子节点(第一种情况),或只有左孩子(第二种情况),不会有右孩子。所以代码中可以非常确定托付哪一个孩子

拷贝构造

        接下来我们实现拷贝构造。该接口比较简单,直接递归拷贝即可。代码如下:

//拷贝构造
BST(const BST<K>& t)
{_root = _Copy(t._root);
}Node* _Copy(Node* root)
{if (root == nullptr){return nullptr;}Node* newRoot = new Node(root->_key);//创建新的根节点newRoot->_left = _Copy(root->_left);//拷贝左子树newRoot->_right = _Copy(root->_right);//拷贝右子树return newRoot;//返回根节点
}

析构

        与拷贝构造相同,析构时递归释放每一个节点。代码如下:

//析构
~BST()
{_Destroy(_root);
}
void _Destroy(Node* root)
{if (root == nullptr){return;}_Destroy(root->_left);//销毁左子树_Destroy(root->_right);//销毁右子树delete root;//删除根节点
}

中序遍历

        由于二叉搜索树的中序遍历结果是有序的,所以我们来实现一个中序遍历接口。中序遍历的代码与传统的二叉树完全相同。

实现如下:

//中序遍历
void Inorder()
{_Inorder(_root);cout << endl;
}
void _Inorder(Node* root)
{if (root == nullptr){return;}_Inorder(root->_left);//遍历左子树cout << root->_key << ' ';//访问根节点_Inorder(root->_right);//遍历右子树
}

最后,我们来使用一下我们实现的二叉搜索树:

int main()
{BST<int> t;int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };for (auto& e : a)//循环插入{t.Insert(e);t.Inorder();}cout << endl;for (auto& e : a)//循环删除{t.Erase(e);t.Inorder();}return 0;
}

 运行结果:

程序全部代码

二叉搜索树的全部实现代码如下:

#include <iostream>
using namespace std;//节点
template<class K>
struct BSTNode
{K _key;//数据域BSTNode<K>* _left;//指向左孩子的指针BSTNode<K>* _right;//指向右孩子的指针//节点的默认构造BSTNode(const K& key):_key(key), _left(nullptr), _right(nullptr){}
};//二叉搜索树
template<class K>
class BST
{typedef BSTNode<K> Node;//重命名,简化代码
public://强制生成无参构造BST() = default;//拷贝构造BST(const BST<K>& t){_root = _Copy(t._root);}//析构~BST(){_Destroy(_root);}//插入bool Insert(const K& key){if (_root == nullptr)//树为空,直接插入{_root = new Node(key);return true;}else//树不为空,进行搜索{Node* cur = _root;//从根节点开始搜索Node* parent = nullptr;//记录cur的父亲节点while (cur)//走到空为止{if (key < cur->_key)//值小向左走{parent = cur;cur = cur->_left;}else if (key > cur->_key)//值大向右走{parent = cur;cur = cur->_right;}else//这里不允许重复的值插入{return false;}}//此时cur走到空,进行插入cur = new Node(key);if (key < parent->_key)//值小,往左边插{parent->_left = cur;}else//值大,往右边插{parent->_right = cur;}return true;}}//删除bool Erase(const K& key){//首先按值查找要被删除的节点Node* cur = _root;Node* parent = nullptr;//记录父亲节点while (cur){if (key < cur->_key)//值小向左走{parent = cur;cur = cur->_left;}else if (key > cur->_key)//值大向右走{parent = cur;cur = cur->_right;}else//找到了,开始删除{//有一个左孩子的情况 和 没有孩子的情况if (cur->_right == nullptr){//特殊情况,要删除的节点是根节点if (cur == _root){_root = cur->_left;//直接让根指针指向左孩子}else{//先判断自己是父亲节点的哪一个孩子//然后将左孩子托付给父亲节点if (cur == parent->_left)parent->_left = cur->_left;elseparent->_right = cur->_left;}delete cur;//删除该节点return true;}//有一个右孩子的情况else if (cur->_left == nullptr){//特殊情况,要删除的节点是根节点if (cur == _root){_root = cur->_right;//直接让根指针指向右孩子}else{//先判断自己是父亲节点的哪一个孩子//然后将右孩子托付给父亲节点if (cur == parent->_left)parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;//删除该节点return true;}//有两个孩子的情况else{//首先寻找右子树的最小值Node* rightMin = cur->_right;//从右孩子开始搜索Node* rightMinParent = cur;//记录最小值的父亲节点while (rightMin->_left)//left走到空,rightMin即为右子树的最小值{rightMinParent = rightMin;rightMin = rightMin->_left;//一直向左走,找最小值}//将最小值赋值给被删除节点cur->_key = rightMin->_key;//将替代节点的右孩子托付给父亲//先判断自己是父亲节点的哪一个孩子if (rightMin == rightMinParent->_left)rightMinParent->_left = rightMin->_right;elserightMinParent->_right = rightMin->_right;delete rightMin;//删除该节点return true;}}}//没找到,返回falsereturn false;}//查找Node* Find(const K& key){Node* cur = _root;//从根节点开始搜索while (cur)//走到空为止{if (key < cur->_key)//值小向左走{cur = cur->_left;}else if (key > cur->_key)//值大向右走{cur = cur->_right;}else//相等说明找到了,返回节点地址{return cur;}}//走到空说明没找到,返回空指针return nullptr;}//中序遍历void Inorder(){_Inorder(_root);cout << endl;}
private:Node* _Copy(Node* root){if (root == nullptr){return nullptr;}Node* newRoot = new Node(root->_key);//创建新的根节点newRoot->_left = _Copy(root->_left);//拷贝左子树newRoot->_right = _Copy(root->_right);//拷贝右子树return newRoot;//返回根节点}void _Destroy(Node* root){if (root == nullptr){return;}_Destroy(root->_left);//销毁左子树_Destroy(root->_right);//销毁右子树delete root;//删除根节点}void _Inorder(Node* root){if (root == nullptr){return;}_Inorder(root->_left);//遍历左子树cout << root->_key << ' ';//访问根节点_Inorder(root->_right);//遍历右子树}Node* _root = nullptr;//根节点的指针
};

三、二叉搜索树的性能分析

        接下来,我们简单分析一下二叉搜索树的性能。 

设二叉树的节点数为N

在较好情况下,二叉搜索树接近于完全二叉树的状态,它的高度为log₂N

在极端情况下,二叉搜索树是单支树的状态,高度为N

综合而言, 二叉搜索树增、删、改的时间复杂度为O(N)

        在较好情况下,其增删改的效率可接近于O(logN),但是它的效率受高度影响较大。所以传统的二叉搜索树显然不满足我们高效查找的需求。之后我们会学习自平衡的二叉搜索树(如AVL树、红黑树),它们会尽量降低二叉搜索树的高度,提高效率。

总结

        今天我们学习了一种特殊的二叉树结构--二叉搜索树,它的特性使其具备了较好的查找效率。掌握二叉搜索树的知识,将为我们后续学习STL中的setmap容器打下坚实的基础。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

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

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

相关文章

【接口自动化测试】一文从3000字从0到1详解接口测试用例设计

接口自动化测试是软件测试中的一种重要手段&#xff0c;它能有效提高测试效率和测试覆盖率。在进行接口自动化测试之前&#xff0c;首先需要进行接口测试用例的设计。本文将从0到1详细且规范的介绍接口测试用例设计的过程&#xff0c;帮助读者快速掌握这一技能。 一、了解接口…

使用 PDF API 合并 PDF 文件

内容来源&#xff1a; 如何在 Mac 上合并 PDF 文件 1. 注册与认证 您可以注册一个免费的 ComPDFKit API 帐户&#xff0c;该帐户允许您在 30 天内免费无限制地处理 1,000 多个文档。 ComPDFKit API 使用 JSON Web Tokens 方法进行安全身份验证。从控制面板获取您的公钥和密钥&…

微服务即时通讯系统的实现(服务端)----(2)

目录 1. 语音识别子服务的实现1.1 功能设计1.2 模块划分1.3 模块功能示意图1.4 接口的实现 2. 文件存储子服务的实现2.1 功能设计2.2 模块划分2.3 模块功能示意图2.4 接口的实现 3. 用户管理子服务的实现3.1 功能设计3.2 模块划分3.3 功能模块示意图3.4 数据管理3.4.1 关系数据…

Scala—列表(可变ListBuffer、不可变List)用法详解

Scala集合概述-链接 大家可以点击上方链接&#xff0c;先对Scala的集合有一个整体的概念&#x1f923;&#x1f923;&#x1f923; 在 Scala 中&#xff0c;列表&#xff08;List&#xff09;分为不可变列表&#xff08;List&#xff09;和可变列表&#xff08;ListBuffer&…

Android 系统之Init进程分析

1、Init进程流程 2、Init细节逻辑 2.1 Init触发shutdown init进程触发系统重启是一个很合理的逻辑&#xff0c;为什么合理&#xff1f; init进程是android世界的一切基石&#xff0c;如果android世界的某些服务或者进程出现异常&#xff0c;那么会导致整个系统无法正常使用…

NVR录像机汇聚管理EasyNVR多个NVR同时管理基于B/S架构的技术特点与能力应用

EasyNVR视频融合平台基于云边端协同设计&#xff0c;能够轻松接入并管理海量的视频数据。该平台兼容性强、拓展灵活&#xff0c;提供了视频监控直播、录像存储、云存储服务、回放检索以及平台级联等一系列功能。B/S架构使得EasyNVR实现了视频监控的多元化兼容与高效管理。 其采…

使用ffmpeg命令实现视频文件间隔提取帧图片

将视频按每隔五秒从视频中提取一张图片 使用 ffmpeg 工具&#xff0c;通过设置 -vf&#xff08;视频过滤器&#xff09;和 -vsync 选项 命令格式 ffmpeg -i input_video.mp4 -vf "fps1/5" output_%03d.png 解释&#xff1a; -i input_video.mp4&#xff1a;指定输…

Java安全—原生反序列化重写方法链条分析触发类

前言 在Java安全中反序列化是一个非常重要点&#xff0c;有原生态的反序列化&#xff0c;还有一些特定漏洞情况下的。今天主要讲一下原生态的反序列化&#xff0c;这部分内容对于没Java基础的来说可能有点难&#xff0c;包括我。 序列化与反序列化 序列化&#xff1a;将内存…

现代网络架构PCI DSS合规范围确定和网络分割措施实施探讨

本文为atsec和作者技术共享类文章&#xff0c;旨在共同探讨信息安全业界的相关话题。未经许可&#xff0c;任何单位及个人不得以任何方式或理由对本文的任何内容进行修改。转载请注明&#xff1a;atsec信息安全和作者名称 1 引言 支付卡行业数据安全标准 &#xff08;P…

docker快速部署gitlab

文章目录 场景部署步骤默认账号密码效果 场景 新增了一台机器, 在初始化本地开发环境&#xff0c;docker快速部署gitlab 部署步骤 编写dockerfile version: 3.7services:gitlab:image: gitlab/gitlab-ce:latestcontainer_name: gitlabrestart: alwayshostname: gitlabenviron…

Kubernetes 01

MESOS&#xff1a;APACHE 分布式资源管理框架 2019-5 Twitter退出&#xff0c;转向使用Kubernetes Docker Swarm 与Docker绑定&#xff0c;只对Docker的资源管理框架&#xff0c;阿里云默认Kubernetes Kubernetes&#xff1a;Google 10年的容器化基础框架&#xff0c;borg…

芯科科技率先支持Matter 1.4,推动智能家居迈向新高度

Matter 1.4引入核心增强功能、支持新设备类型&#xff0c;持续推进智能家居互联互通 近日&#xff0c;连接标准联盟&#xff08;Connectivity Standard Alliance&#xff0c;CSA&#xff09;发布了Matter 1.4标准版本。作为连接标准联盟的重要成员之一&#xff0c;以及Matter标…

Redis 分布式锁实现方案

一、概述 分布式锁&#xff0c;即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题&#xff0c;而分布式锁&#xff0c;就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是&#xff0c;分布式系统中竞争共享资源的最小粒度从线程升级…

数据结构-最小生成树

一.最小生成树的定义 从V个顶点的图里生成的一颗树&#xff0c;这颗树有V个顶点是连通的&#xff0c;有V-1条边&#xff0c;并且边的权值和是最小的,而且不能有回路 二.Prim算法 Prim算法又叫加点法&#xff0c;算法比较适合稠密图 每次把边权最小的顶点加入到树中&#xff0…

ASP.NET Web(.Net Framework) Http服务器搭建以及IIS站点发布

ASP.NET Web&#xff08;.Net Framework&#xff09; Http服务器搭建以及IIS站点发布 介绍创建ASP.NET Web &#xff08;.Net Framework&#xff09;http服务器创建项目创建脚本部署Http站点服务器测试 Get测试编写刚才的TestWebController.cs代码如下测试写法1测试写法2 Post测…

【AI系统】昇腾 AI 架构介绍

昇腾 AI 架构介绍 昇腾计算的基础软硬件是产业的核⼼&#xff0c;也是 AI 计算能⼒的来源。华为&#xff0c;作为昇腾计算产业⽣态的⼀员&#xff0c;是基础软硬件系统的核⼼贡献者。昇腾计算软硬件包括硬件系统、基础软件和应⽤使能等。 而本书介绍的 AI 系统整体架构&#…

org.apache.commons.lang3包下的StringUtils工具类的使用

前言 相信平时在写项目的时候&#xff0c;一定使用到StringUtils.isEmpty()&#xff1b;StringUtils.isBlank();但是你真的了解他们吗&#xff1f; 也许你两个都不知道&#xff0c;也许你除了isEmpty/isNotEmpty/isNotBlank/isBlank外&#xff0c;并不知道还有isAnyEmpty/isNon…

vscode中json文件的注释飘红

vscode的json文件 添加注释&#xff0c;提示json中不允许有注释&#xff0c;点编辑器最下面的json&#xff0c;如下图 然后选择如上图的json with comments就好了

【AI日记】24.11.30 kaggle 比赛 Titanic-3

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 工作 内容&#xff1a;学习 kaggle 入门比赛 Titanic - Machine Learning from Disaster&#xff0c;学习机器学习课程时间&#xff1a;5 小时评估&#xff1a;继续 读书 书名&#xff1a;美丽新世界时间&a…

抓包之查看websocket内容

写在前面 本文看下websocket抓包相关内容。 1&#xff1a;正文 websocket基础环境搭建参考这篇文章。 启动后&#xff0c;先看chrome的network抓包&#xff0c;这里我们直接使用is:running来过滤出websocket的请求&#xff1a; 可以清晰的看到发送的内容以及响应的内容。在…