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

【C++】二叉搜索树

1.概念

二叉搜索树又称二叉排序树,可以是一棵空树,不为空具有以下性质:

若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

2.实现

2.1创建节点

在这里插入图片描述

2.2查找

在这里插入图片描述
创建cur来代替_root来循环遍历
1.从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
2.最多查找高度次,走到到空,还没找到,这个值不存在。

递归版本

在私有域中封装具体实现细节,在公共域中提供接口使用,作为成员函数在类中可直接访问私有成员变量。

在这里插入图片描述
在这里插入图片描述
递归的终止条件是1.找到空节点(失败) 2.找到key所在节点(成功)
若当前的key值小于目标key值,则递归地在当前节点的右树中查找;反之在左树查找。不断缩小搜索范围,直到找到目标key值或搜索范围为空。

2.3插入

bool Insert(const k& key)
{if (_root == nullptr){_root = new Node(key);return true;}//进行左右子树遍历找到空节点处插入//保存父节点为了最后链接新节点Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}//节点值在搜索二叉树中只有唯一存在else{return false;}}//链接新节点cur = new Node(key);//判断链接在父节点的左边还是右边if (parent->_key > key){parent->_left = cur;}else{parent->_right = cur;}return true;
}

创建cur来代替_root来循环遍历,提前保存父节点
1.树为空,则直接新增节点,赋值给root指针,插入成功
2.树不空,按二叉搜索树性质查找插入位置,插入新节点。若找到key值相同的节点就插入失败,因为二叉搜索树性质决定不允许有相同key值的节点同时存在。
3.找到空节点后创建新节点,注意记得用父节点链接上形成搜索二叉树,需要判断链接在父节点的左边还是右边。

递归版本

还是在私有域封装实现细节,公共域提供接口使用,作为类的成员函数可直接访问私有成员。

在这里插入图片描述
在这里插入图片描述
递归结束条件为找到空节点位置,创建新节点进行链接。
这里的神来之笔是Node*&root,每次递归下去的是当前节点指针的引用(),这样就可以省略提前保存父节点指针的步骤,直接进行链接即可root永远是父亲右指针和左指针的别名,这样就不用判断重新链接在父指针的左还是右。

为什么循环不能传指针的引用?
引用的问题是不能改变指针指向,循环中没办法使用,递归可以因为每次都会创建新的栈帧,不同域中可以定义同名变量,每个栈帧中的引用都不同,每次递归都创建新的引用

2.4删除

删除时有三种情况
1.删除节点没有孩子
2.删除节点只有一个孩子
3.有两个孩子
解决办法:
a:1和2属于直接删除场景,删除后父节点重新链接即可(nullptr或子节点)
b:3用替换法,用左子树的最大节点(最右节点),或者右子树的最小节点(最左节点)。这样能确保交换根节点和要删除节点的key值后左子树key值依然小于根节点key值,右子树key值依然大于根节点key值,满足二叉搜索树性质。最后删除 要删除节点位置即可。

bool Erase(const k& key)
{Node* cur = _root;Node* parent = cur;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else//找到要删除的节点了{//先判断没有孩子或只有一个孩子的情况//删除节点的左树为空if (cur->_left == nullptr){//检查删除节点是根节点if (cur == _root){_root = cur->_right;}else{//判断在父节点的哪一边删除再链接if (parent->_left == cur){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}}//删除节点的右树为空else if (cur->_right == nullptr){//处理跟=根节点为要删除节点情况if (cur == _root){_root = cur->_left;}if (parent->_left == cur){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}//要删除节点有两个孩子情况else{//找替代节点,以左边为例Node* parent = cur;Node* leftmax = cur->_left;while (leftmax->_right){parent = leftmax;leftmax = leftmax->_right;}swap(cur->_key, leftmax->_key);//删除后链接if (parent->_right == leftmax){parent->_right = leftmax->_left;}if (parent->_left == leftmax){parent->_left = leftmax->_left;}cur = leftmax;}delete cur;return true;}}return false;
}

代码逻辑:
1.创建cur来代替_root来循环遍历,提前保存父节点
2.找要删除节点的位置,比较当前位置key值与目标key值,不断更新父节点位置和cur节点位置,循环遍历下去,直到找到目标位置
3.依次分析删除的三种情况,注意检查删除节点是根节点的情况
在这里插入图片描述
需要将根节点更新到子树不为空的位置,这样父节点也跟着更新了(Node* parent = cur),若不更新父节点为空,会造成非法访问的问题
4.重点分析删除节点有两个孩子情况:
左边和右边情况分别创建leftmax或rightmax来记录替代节点,父节点赋值为cur,可以避免父节点为空跳过循环中父节点赋值而造成的非法访问问题。找到后交换两节点中key值,再进行删除后的重新链接

递归版本

public:
bool EraseR(const k& key)
{return _EraseR(_root, key);
}
private:
bool _EraseR(Node*& root, const k& key)
{if (root == nullptr){return false;}//寻找要删除的节点if (root->_key < key){return _EraseR(root->_right, key);}else if (root->_key > key){return _EraseR(root->_left, key);}else{//提前保存交换后要删除的根节点Node* del = root;//删除节点后的重新链接//左为空if (root->_left == nullptr){root = root->_right;}//右为空else if (root->_right == nullptr){root = root->_left;}//左右都不为空else{Node* leftmax = root->_left;while (leftmax->_right){leftmax = leftmax->_right;}swap(leftmax->_key, root->_key);return _EraseR(root->_left, key);}delete del;return true;}
}

递归终止条件1.根节点为空返回false 2.删除成功返回true
代码逻辑:
1.递归查找要删除的节点
2.当前key值等于目标key值,进行删除逻辑:
保存要删除节点到临时指针del
然后重新链接树,左子树为空,将当前节点右子树链接到父节点;右子树为空,将当前节点左子树链接到父节点;左右子树都不为空时,找到左子树的最大节点(最右节点),或右子树的最小节点(最左节点),将该节点key值与根节点key值交换,然后递归的在根节点的左子树中删除目标key值,这样目标key值所在节点从有两个孩子变成没有孩子

辨析:
return _EraseR(root->_left, key);第一个参数若用leftmax
在这里插入图片描述
删除接口传参Node*& root,用的指针的引用,传下来的是节点指针的引用,所以不需要提前保存父节点进行重新链接。但此处传leftmax指针的引用实际上是指向1节点指针的引用(当前节点指针的引用),将3这个节点删除后8节点指针指向没有改变,导致结构混乱。应该传root->_left,指向3节点,发现3的右节点为空直接指向1节点,正确完成删除

在这里插入图片描述

感受指针引用的妙处

以右子树为空的情况分析:正常情况下,root是父亲右指针或左指针的别名,直接更改父节点指向,上文已分析过
在这里插入图片描述
当要删除节点为空节点时,此时root为_root的别名,同样可以直接更改_root的指向

2.5复制

在这里插入图片描述
拷贝构造函数使用参数 t 来获取原对象的数据,然后根据这些数据构造一个新对象,新对象在内存中是完全独立的副本。复用copy函数实现深拷贝
在这里插入图片描述
递归终止条件1.传入节点为空无需复制返回空指针 2.每次递归调用中创建一个key值与原树相同的节点
通过后序遍历递归复制左子树和右子树,在递归过程中创建节点,在返回过程中链接形成树型结构

2.6销毁

在这里插入图片描述
采用后序遍历的方式销毁二叉树,释放当前节点占用内存后,记得将root指针置空
在这里插入图片描述
析构函数复用销毁的实现

3.二叉搜索树的应用

key搜索模型:

K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。上述代码实现就是该模型的应用
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

key_value模型:

每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对

namespace key_value
{template<class k, class v >struct BSTreeNode{BSTreeNode<k,v>* _left;BSTreeNode<k,v>* _right;k _key;v _value;BSTreeNode(const k& key,const v&value):_left(nullptr), _right(nullptr), _key(key),_value(value){}};template<class k,class v>class BSTree{typedef BSTreeNode<k,v> Node;public:BSTree():_root(nullptr){}//封装一次可以通过类直接调用void _Inorder(){Inorder(_root);cout << endl;}Node* FindR(const k& key){return _FindR(_root, key);}bool InsertR(const k& key,const v&value){return _InsertR(_root, key,value);}bool EraseR(const k& key){return _EraseR(_root, key);}private:void Inorder(Node* _root){if (_root == nullptr){return;}Inorder(_root->_left);cout << _root->_key << ":"<<_root->_value<<endl;Inorder(_root->_right);}bool _EraseR(Node*& root, const k& key){if (root == nullptr){return false;}//寻找要删除的节点if (root->_key < key){return _EraseR(root->_right, key);}else if (root->_key > key){return _EraseR(root->_left, key);}else{//提前保存交换后要删除的根节点Node* del = root;//删除节点后的重新链接//左为空if (root->_left == nullptr){root = root->_right;}//右为空else if (root->_right == nullptr){root = root->_left;}//左右都不为空else{Node* leftmax = root->_left;while (leftmax->_right){leftmax = leftmax->_right;}swap(leftmax->_key, root->_key);return _EraseR(root->_left, key);}delete del;return true;}}bool _InsertR(Node*& root, const k& key,const v&value){if (root == nullptr){root = new Node(key,value);return true;}if (root->_key < key){return _InsertR(root->_right, key,value);}else if (root->_key > key){return _InsertR(root->_left, key,value);}else{return false;}}Node* _FindR(Node* root, const k& key){if (root == nullptr){return nullptr;}if (root->_key < key){return _FindR(root->_right, key);}else if (root->_key > key){return _FindR(root->_left, key);}else{return root;}}Node* _root;};
}

实现思路与key模型基本相同,只是节点中多增加了_value成员变量,BSTree类中多增加了一个模板参数v,实现插入功能时要同时插入value值。

  • 测试代码
void testBSTree1()
{key_value::BSTree<string, string> dict;dict.InsertR("mint", "薄荷");dict.InsertR("forever", "永远");dict.InsertR("love", "相爱");string str;while (cin >> str){key_value::BSTreeNode<string, string>* ret = dict.FindR(str);if (ret){cout << ret->_value << endl;}else{cout << "无此单词" << endl;}}
}

输入CTRLz+换行结束输入,正常退出ctrlc直接关掉程序

http://www.xdnf.cn/news/46819.html

相关文章:

  • 线性回归之正则化(regularization)
  • C++入门基础:引用,auto自动关键字,内联函数,范围for循环
  • 【iOS】alloc init new底层原理
  • 代收代付到底是什么?
  • 【英语语法】词法---副词
  • AIGC赋能插画创作:技术解析与代码实战详解
  • 大模型应用案例:主动提问式的 AI 面试官(接入 DeepSeek)
  • 【特殊场景应对3】创意岗简历骚操作:作品集链接的正确打开方式
  • deepseek + kimi制作PPT
  • 01背包简介
  • LeetCode第159题_至多包含两个不同字符的最长子串
  • Kubernetes相关的名词解释-关于组件分类(8)
  • 插叙的作用
  • 【2025软考高级架构师】——计算机系统基础(7)
  • gma 2.1.4 (2025.04.18) | GmaGIS V0.0.1a3 更新日志
  • 【读书笔记·VLSI电路设计方法解密】问题64:什么是芯片的功耗分析
  • JavaWeb 1.HTML+CSS (黑马JavaWeb课程笔记)
  • 交换机端口安全
  • C++学习之游戏服务器开发⑩ZINX的TCP通道实现
  • 基于 Vue3 + ECharts + GeoJson 实现区域地图钻取功能详解
  • 大模型在胆管结石(无胆管炎或胆囊炎)预测及治疗方案制定中的应用研究
  • 【perf】perf工具的使用生成火焰图
  • 自由的控件开发平台:飞帆中使用 css 和 js 库
  • 如何优雅地实现全局唯一?深入理解单例模式
  • uniapp微信小程序实现sse
  • 深度学习优化器详解:SGD、Adam与AdamW
  • C#/.NET/.NET Core技术前沿周刊 | 第 35 期(2025年4.14-4.20)
  • docker 安装 MySQL
  • 【Oracle专栏】函数中SQL拼接参数 报错处理
  • 【网络原理】TCP协议如何实现可靠传输(确认应答和超时重传机制)