learn C++ NO.19——二叉搜索树

devtools/2024/9/25 7:19:53/

简单介绍一下二叉搜索树

二叉搜索树也称为二叉排序树。它是一种具有特殊性质的二叉树。它有如下性质。
1、当前节点的左子树的值一定小于当前节点,当前节点的右子树的值一锭大于当前节点。这也就意味着,在接近完全二叉树的情况下(高度较为合适的情况下),二叉搜索树的查找效率极高(O(LogN))。但是,当极端请况下,可能就会退化成链表。所以,二叉搜素树查找的最坏实践复杂度是O(N)。
2、并且当前节点的左子树和右子树都符合二叉搜索树的性质。
3、中序遍历有序性。二叉搜素树的中序遍历是一个升序序列。

为什么学二叉搜索树

在前面二叉树的博客中,提到了二叉树的增删查改没有意义。但是搜索二叉树不一样,它的增删查改是有意义的。并且学习二叉搜索树是为了后面的AVL树和红黑树打下基础。

模拟实现一份二叉搜索树

插入接口的实现

这里用的是循环的方式遍历树。实现的思路如下,首先,需要对第一次插入做一个特殊处理。即当根节点为空时,直接new 一个节点赋值给根节点。若不是第一次插入,那么则需要遍历树,根据搜索二叉树的性质找到合适的位置插入,需要注意的是,由于适合插入的位置一定是空节点,所以需要保存一下需要插入节点的父亲节点。这样才能让新插入的节点连接到树上。
在这里插入图片描述
下面,再实现一下递归版本的插入接口。整体还是以封装一份递归子函数来实现主体逻辑。递归版本的核心就是子函数的函数头的设计,即bool _InsertR(Node*& root, const K& key)。将第一个参数设置成实参的别名,这样修改形参root其实就是修改实参。这样的话就不用管插入的位置是在父节点的左子树还是右子树了。

递归出口就是插入数据的位置,当root走到空就插入数据并返回true。

递归主体就是当插入的值比当前节点的值大,递归去右子树找到合适的位置插入。当插入的值比当前节点的值小,递归去左子树找到合适的位置插入。当插入的值等于当前节点值,说明该元素存在,返回false。

在这里插入图片描述

中序遍历的实现

采用子函数递归的方式实现,因为将根节点设置成了私有成员,类外不可用。所以提供了一个无参的主函数调用类内子函数。中序其实就是先遍历左子树,访问根,再去右子树遍历。
在这里插入图片描述

查找接口的实现

根据搜索二叉树的特性进行一次遍历即可。当前节点的值大于要查找的值时,去当前节点的左边查找。若当前节点的值小于要查找的值时,去当前节点的右边查找。若当前节点的值等于要查找的值时,直接返回true。当遍历结束后还没找到则返回false。
在这里插入图片描述
下面实现一份递归版本的代码,具体的实现思路如下,依旧采用对外提供一个主函数,在类内写一个递归子函数实现查找的逻辑。

参数部分这么设计,主函数的函数头为 bool FindR(const K& key)。递归子函数的函数头为bool _FindR(Node* root, const K& key) 。

递归子函数主体逻辑实现思路如下,递归出口为当root为空时,则表示当前查找的值不存在。递归主体为当root的值比要找的值小时,去root的右子树找。当root的值比要找的值大时,去root的左子树找。当root的值等于要找的值时,返回 true。
在这里插入图片描述

删除接口的实现

删除接口也是二叉搜索树的最核心接口。实现起来比较的复杂,下面且听我分析,由于找到带删除元素都逻辑上面已经实现过了,这里不再赘述。下面直接讲删除的逻辑。

首先,删除的情况有三种,分别是删除的这个节点是叶子节点、删除的节点的左子树或右子树为空以及删除的这个节点既有左子树又有右子树。
在这里插入图片描述
其实第1种情况和第2种情况其实是可以归为一类来看的。因为这两种情况本质上都是将被删除节点左右子树关系托孤给被删除节点的父节点。这里我就以托孤法来形容这种情况。

第3种情况既可以从右子树中找到合适的节点替换,也可以从左子树中找到合适的节点替换。然后删除8这个位置,以保持二叉搜索树的特性。从右子树找最小的值与8替换或左子树找最大的值与8替换都可以。

下面先讲解托孤法,通过样例来进行讲解
在这里插入图片描述

假设需要删除cur这个节点。此时cur的左子树为空,此时意味着需要将cur的右子树部分托孤给parent。具体操作就是将cur->right 赋值给parent->left 。然后删除cur。

下面再看一个特殊的托孤法的场景
在这里插入图片描述
由于cur就是根节点,意味着我们需要对这种情况进行特殊的处理。直接修改root的值为cur的右子树。

那么托孤法主要逻辑是判断当前节点的左子树为空还是右子树为空的场景作为切入点。而叶子节点恰好符合,所以,可以在这个逻辑里面进行处理。

下面提供代码,可以分别带入上面给的样例感受一下。
在这里插入图片描述

在这里插入图片描述

下面讲解一下左右都不为空的处理逻辑。以从左子树中找最大值为例,那就是定义一个变量leftMax以根节点的左子树进行赋值,再保存leftMax的父节点。然后依次遍历leftMax的右子树,便可以找到左子树的最大值。不过需要注意一个特殊情况,就是leftMax一开始就是左子树的最大值。这个逻辑需要单独处理。当找到左子树的最大值后,用左子树的最大值和被删除的元素的值进行交换。然后修改父节点的连接关系即可。

在这里插入图片描述
在这里插入图片描述

//删除接口——迭代版本
bool Erase(const K& key)
{Node* parent = nullptr;Node* cur = _root;while (cur){//比cur的值小if (cur->_key > key){parent = cur;cur = cur->_left;}//比cur的值大else if (cur->_key < key){parent = cur;cur = cur->_right;}else //找到了 {//大体分两种情况,1、托孤 2、找子树的最大值交换//左边为空的情况if (cur->_left == nullptr){//边界情况if (cur == _root){_root = cur->_right;}else{// 需要考虑cur所处的位置if (parent->_left == cur){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}}else if (cur->_right == nullptr)//右为空{//边界情况if (cur == _root){_root = cur->_left;}else {// 需要考虑cur所处的位置if (parent->_left == cur){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}}else//左右不为空,与左子树的最大值交换{//这里parent不能为null,否则下面特殊情况会出错parent = cur;Node* leftMax = cur->_left;while (leftMax->_right){parent = leftMax;leftMax = leftMax->_right;}swap(leftMax->_key, cur->_key);//特殊情况 _root->left == leftMaxif (parent->_left == leftMax){parent->_left = leftMax->_left;}else{parent->_right = leftMax->_left;}//符合统一删除逻辑cur = leftMax;}//统一删除逻辑delete cur;return true;}}return false;
}

下面再介绍递归版本的实现。非递归版本相比于递归版本来说是比较复杂的,但是实践当中还是推荐使用非递归版本代码。因为递归有栈溢出的风险。回到正题,下面实现递归版本代码。

首先递归的实现思路如下,先设计递归子函数的函数头,这里就说一下函数头的设计,由于删除接口会修改节点的指向,所以形参部分依旧是选择节点的指针的引用进行。bool _EraseR(Node*& root, const K& key)。主函数部分依旧是和上面的递归主函数的方案保持一致。

递归的出口部分为当root为空时,表示key不存在,直接返回false。

递归主体逻辑为当root的值比要找的值小时,去root的右子树找。当root的值比要找的值大时,去root的左子树找。当root的值等于key时,开始执行删除逻辑。首先,需要将当前节点保存一份。避免执行下面逻辑导致节点丢失,进而引发内存泄漏。删除逻辑大体分为托孤(左为空或右为空的情况)和 与左子树的最大值交换后删除(左右不为空)。

左为空和右为空的情况直接修改root的指向即可。因为这里传参传的是实参的别名,形参部分是的类型是实参的引用就可以改变实参的指向。

左和右都不为空情况下,和左子树的最大值进行交换后,再次递归从root->left的位置开始,删除key。最后,进入函数后会执行托孤部分的逻辑,将节点删除。
在这里插入图片描述

在这里插入图片描述

析构函数的实现

采用子函数递归的方式,后序遍历进行依次释放节点并将节点置空。在析构函数内部调用这个子函数即可。需要注意的是子函数的函数头的参数部分,用节点指针的引用可以改变节点的指向。
在这里插入图片描述

拷贝构造的实现

拷贝构造实现思路如下,使用一个子函数递归前序遍历依次完成节点拷贝和链接。再实现一个现代写法的拷贝赋值运算符重载即可。
在这里插入图片描述

二叉搜索树的应用场景

通常二叉搜索树有两种模型,一种是key的模型,另一种是key,value模型。上面,模拟实现实现的就是一个key模型的二叉搜索树。那这两者有什么区别呢?

key模型的二叉搜索树通常用于快速判断一个key在不在的场景。比如说校园卡刷门禁系统。此时key就是你的学号,门禁系统根据你的学号判断你是否是在校生,是就放行。

key,value模型的二叉搜索树是一个根据key值去找对应的value值的一个搜索模型。比如说一个英文词典程序,通过英文单词,去词库系统中找匹配的中文翻译。

简单搭一个key value模型的二叉搜索树

通过上面手撕的key模型的二叉搜索树,我们简单进行一下修改便可以把它改造成key value结构的二叉搜索树。

namespace xyx_kv
{template<class K, class V>struct BSTNode{BSTNode(const K& key, const V& value):_left(nullptr), _right(nullptr), _key(key), _value(value){}BSTNode* _left;BSTNode* _right;K _key;V _value;};template<class K, class V>class BSTree{public:typedef BSTNode<K, V> Node;BSTree():_root(nullptr){}BSTree(const BSTree<K, V>& t){_root = Copy(t._root);}BSTree<K, V>& operator=(BSTree<K, V> t){swap(_root, t._root);return *this;}~BSTree(){Destroy(_root);}void InOrder(){_inOrder(_root);cout << endl;}Node* FindR(const K& key){return _FindR(_root, key);}bool InsertR(const K& key, const V& val){return _InsertR(_root, key, val);}bool EraseR(const K& key){return _EraseR(_root, key);}private:void Destroy(Node*& root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}Node* Copy(Node* root){//递归出口if (root == nullptr)return nullptr;//前序遍历以此拷贝Node* newNode = new Node(root->_key, root->_value);newNode->_left = Copy(root->_left);newNode->_right = Copy(root->_right);return newNode;}bool _EraseR(Node*& root, const K& key){//key 不存在if (root == nullptr)return false;if (root->_key > key) //比root小,去root的左子树找return _EraseR(root->_left, key);else if (root->_key < key) //比root大,去root的左子树找return _EraseR(root->_right, key);else //找到了{// 1、左为空// 2、右为空// 3、左右不为空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(root->_key, leftMax->_key);return _EraseR(root->_left, key);}delete del;return true;}}bool _InsertR(Node*& root, const K& key, const V& val){if (root == nullptr){root = new Node(key, val);return true;}if (root->_key > key){return _InsertR(root->_left, key, val);}else if (root->_key < key){return _InsertR(root->_right, key, val);}else{return false;}}Node* _FindR(Node* root, const K& key){if (root == nullptr)return nullptr;if (root->_key > key){return _FindR(root->_left, key);}else if (root->_key < key){return _FindR(root->_right, key);}else{return root;}}void _inOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << ":" << root->_value << endl;_InOrder(root->_right);}private:Node* _root;};

下面简单写一个英文词典简易程序带大家感受一下key value搜索模型的二叉搜索树。
在这里插入图片描述

下面再看一个根据经典的key value模型的应用,统计string类数据出现的次数。
在这里插入图片描述
介绍完了key 模型和 key value模型的二叉搜索树。其实在实际使用中库里面有现成的方案可供我们使用。key模型对应的是set,key value模型对应的是map。有些许不同的是它们底层用的是红黑树(平衡二叉搜索树)来实现的。后面还会详细介绍。

总结

二叉搜索树是一种特殊的二叉树。它不仅严格要求当前节点的左子树的值小于当前节点的值,还严格要求当前节点的右子树的值大于当前节点的值。它的搜索模型有两种分别是key模型和key value 模型。对应了快速判断在不在的场景和根据关键值辅助查找数据的场景。

学习二叉搜索树的意义在于对于一些查找算法有了新的理解。和二分查找相比搜索二叉树在实景使用中更加优秀,虽然二分查找看起来很快,但是在实践中其实使用场景比较局限,二分查找需要数据具有二义性(有序性其实就是二义性)。而搜索二叉树相比于二分查找使用场景更多。

虽然在理想情况下(接近完全二叉树),二叉搜索树的查找效率是O(LogN)的。但是,二叉搜索树也伴随着特殊情况下,高度过高导致查找效率退化至O(N)。所以,可以认为二叉搜索树的查找的实际时间复杂度为O(N)。那如何让搜索二叉树的结构始终处于一个理想的状态呢?那就要引入平衡因子的概念,是二叉搜索树升级成AVL树和红黑树。这在后续的文章中在详细的介绍了。

好了本篇文章就到这里,感谢您的阅读!如有错误请指出。参考代码,点击跳转


http://www.ppmy.cn/devtools/116851.html

相关文章

vue2 文本溢出那些事

概述 平时开发中,如果标题超出多行,我们希望出现省略号,并且为其添加简单提示。但是,如果我们在全局写公共class类,行数不好控制。如果统一加title=xxx,又会出现文本是否超出都会出现title的现象。 那么到底该如何实现?下面这里给出了一些参考思路和对应的代码 相关可…

面经 | webpack

webpack webpackloader基本语法rules自定义loader 你可以写哪些loader&#xff1f;常见loader pluginwebpack生命周期 [参考](https://blog.csdn.net/qq_17335549/article/details/137561075)常见plugin webpack 一个打包工具&#xff0c;就和npm是一个包管理工具差不多。一般…

openai最新o1上线(2024年09月12日)

gpt-4o-2024-08-06输出文本价格 10美元/M o1-preview输出价格 60美元/M https://lmarena.ai/?leaderboard 数字9.11和9.8谁大些 人工智能学习网站 https://chat.xutongbao.top/

使用 Puppeteer-Cluster 和代理进行高效网络抓取: 完全指南

文章目录 一、介绍&#xff1f;二、什么是 Puppeteer-Cluster&#xff1f;三、为什么代理在网络抓取中很重要&#xff1f;四、 为什么使用带代理的 Puppeteer-Cluster&#xff1f;五、分步指南&#xff1a; 带代理的 Puppeteer 群集5.1. 步骤 1&#xff1a;安装所需程序库5.2. …

开箱元宇宙| 探索 Great Entertainment Group 如何利用 Web3 和数字创新重新定义活动体验

有没有想过 Web3 等尖端技术是如何改变娱乐行业的&#xff1f;在本期「开箱元宇宙」系列中&#xff0c;我们与 Great Entertainment Group (GEG) 的 Web3 顾问 Rob Lacey 深度访谈&#xff0c;探讨这家充满活力的公司如何在其活动中开拓数字创新。 与我们一起揭示 GEG 如何将 …

git eslint扩展,解决git提交因为空格差异而报错

项目场景&#xff1a; 在前端项目开发中&#xff0c;经常会使用eslint,这个方法的好处就是严格要求代码格式。让代码更为严谨。 问题描述 以为eslint格式过于严谨&#xff0c;在git提交的时候&#xff0c;经常会因为一个多了一个空格导致代码提交失败。 原因分析&#xff1a;…

Spring Boot 学习和使用

文章目录 前言一、Spring Boot简介二、核心特性三、核心注解四、快速入门五、学习资源总结前言 Spring Boot是一款开源的Java Web应用框架,旨在简化Spring应用的初始搭建以及开发过程。以下是Spring Boot入门的详细介绍: 一、Spring Boot简介 Spring Boot通过整合Spring技术…

EtherCAT转Profient协议网关简述

Profinet 转 EtherCAT 的连接与通信问题一直是许多人关注的焦点&#xff0c;也常常给人们带来诸多困惑。在此&#xff0c;我们将深入剖析这一问题&#xff0c;并为大家提供切实可行的解决方案。WL-PN-ECATM型设备在这方面表现卓越&#xff0c;能够有效解决这一难题。接下来&…