【C++进阶】二叉搜索树(BSTree)

在这里插入图片描述

​👻内容专栏:C/C++编程
🐨本文概括:二叉搜索树的基本操作(查找、删除、插入)、二叉搜索树的应用,KV模型。
🐼本文作者:阿四啊
🐸发布时间:2023.11.22

一、二叉搜索树

1.1 二叉搜索树的概念

二叉搜索树又称二叉排序树(BST,Binary Search Tree),它或者是一棵空树,或者是具有以下性质的二叉树:

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

在这里插入图片描述

1.2 二叉搜索树的基本操作

类的创建

template<class T>
struct BSTreeNode
{BSTreeNode<T>* _left;BSTreeNode<T>* _right;T _key;BSTreeNode(const T& key):_left(nullptr),_right(nullptr),_key(key){ }
};
template<class T>
class BSTree
{
public:typedef BSTreeNode<T> Node;
private:Node* _root = nullptr;
};

在这里插入图片描述

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

查找操作

分为两种情况:
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,说明这个值不存在。

bool Find(const T& key)
{if (_root == nullptr){return false;}Node* cur = _root;while (cur != nullptr){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else{return true;}}return false;
}

插入操作

插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,需要定义cur节点指针往后寻找,按二叉搜索树性质查找插入位置,插入新节点。其中我们还需定义一个parent节点指针,是为了让插入的节点在parent节点的左边还是右边。

⚠️注意:二叉搜索树中是不允许出现相等的值的,出现相等情况的值,插入操作返回false即可。

bool Insert(const T& key)
{//树为空if (_root == nullptr){_root = new Node(key);return true;}//树不为空Node* parent = nullptr;Node* cur = _root;while (cur != nullptr){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{return false;}}if (parent->_key < key) parent->_right = new Node(key);else parent->_left = new Node(key);return true;
}

删除操作

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

  1. 要删除的结点无孩子结点。
  2. 要删除的结点只有左孩子结点。
  3. 要删除的结点只有右孩子结点。
  4. 要删除的结点有左、右孩子结点。

看起来有待删除节点有4中情况,无孩子结点也可以被分为只有左孩子或者只有孩子的情况,所以实际情况a可以与情况b或者c合并起来,因此真正的删除过程。如下:
情况a:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点 ==>直接删除
情况b:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点 ==>直接删除
情况c:在它的右子树中寻找中序下的最左节点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题。或者在它的左子树中寻找最右节点(关键码最大),用它的值填补到被删除节点中,再来处理该结点的删除问题 ==>替换法删除
直接删除:

在这里插入图片描述
替换法删除:

在这里插入图片描述
以上替换法两种方法都可行,作者便使用法二,寻找右子树的最左节点(最小节点)进行交换。另外一种方法友友们可以自己实现。

bool Erase(const T& key)
{//树为空if (_root == nullptr){return false;}Node* parent = nullptr;Node* cur = _root;while (cur != nullptr){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{//准备删除操作//情况b 要删除的结点只有右孩子结点if (cur->_left == nullptr){if (cur == _root){_root = cur->_right;}else{//左子树中if (cur == parent->_left){parent->_left = cur->_right;}else//右子树中{parent->_right = cur->_right;}}	}else if (cur->_right == nullptr)//情况a 要删除的结点只有左孩子结点{if (cur == _root){_root = cur->_left;}else{//左子树中if (cur == parent->_left){parent->_left = cur->_left;}else//右子树中{parent->_right = cur->_left;}}}else//情况c 要删除的结点有左、右孩子结点{//右子树的最小节点(最左节点)Node* parent = cur;//parent为什么初始化为cur,因为在删除根节点时为特例,否则会出现循环进不去,出现空指针解引用Node* subLeft = cur->_right;while (subLeft->_left != nullptr){parent = subLeft;subLeft = subLeft->_left;}swap(cur->_key, subLeft->_key);if (parent->_left == subLeft){parent->_left = subLeft->_right;}else{parent->_right = subLeft->_right;}}return true;}}return false;
}

我们可以在类中手写一个中序遍历,验证删除操作是否正确。
中序遍历

public:
void InOrder()
{_InOrder(_root);cout << endl;
}
private:
void _InOrder(Node* root)
{if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);
}

在main函数中进行验证

int main()
{int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTree<int> bt;for (auto e : a){bt.Insert(e);}bt.InOrder();//删除14bt.Erase(14);bt.InOrder();//删除3bt.Erase(3);bt.InOrder();//删除8bt.Erase(8);bt.InOrder();return 0;
}

验证结果:

1 3 4 6 7 8 10 13 14
1 3 4 6 7 8 10 13
1 4 6 7 8 10 13
1 4 6 7 10 13

1.3 二叉搜索树的基本操作(递归版本)

查找操作

public:
bool FindR(const T& key){return _FindR(_root, key);}
private:
bool _FindR(Node* root, const T& key)
{if (root == nullptr){return false;}if (root->_key < key){return _FindR(root->_right, key);}else if (root->_key > key){return _FindR(root->_left, key);}else{return true;}
}

插入操作

我们发现你插入操作的难点在于如何进行链接节点,这里我们只需在形参部分给Node* root添加上&之后,这一点很巧妙,然后我们执行root = new Node(key),就能成功链接新插入的节点。不懂的友友们可以试着画一画递归展开细节图。

public:
bool InsertR(const T& key)
{return _InsertR(_root, key);
}
private:
bool _InsertR(Node*& root, const T& key)
{if (root == nullptr){root = new Node(key);return true;}if (root->_key < key){return _InsertR(root->_right, key);}else if (root->_key > key){return _InsertR(root->_left, key);}else{return false;}
}

删除操作

这里删除操作和插入操作类似,如果删除节点然后链接,一样使用了巧妙的引用操作。

bool EraseR(const T& key)
{return _EraseR(_root, key);
}
private:
bool _EraseR(Node*& root,const T& 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{//删除if (root->_left == nullptr){Node* del = root;root = root->_right;delete del;}else if (root->_right == nullptr){Node* del = root;root = root->_left;delete del;}else{//在右子树中寻找最左节点(最小节点)Node* subLeft = root->_right;while (subLeft->_left != nullptr){subLeft = subLeft->_left;}swap(root->_key, subLeft->_key);//转换成在子树中去递归删除return _EraseR(root->_right, key);}}
}

这里要着重说明一下删除左右孩子都存在的节点,该如何去递归操作,举例说明删除3,我们利用循环去找删除节点右子树中的最左节点,找到之后交换两个节点的值,那么subLeft节点如何删除?这里我们不再使用parent节点,然后条件判断,我们可以转换成在交换前的右子树中去递归删除,如下图,蓝色圆圈标记。
在这里插入图片描述

1.4 二叉搜索树的应用

  1. K模型K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
    以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
    1.2部分我们实现的就是K模型的底层基本能逻辑。

  2. KV模型每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见
    a.比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
    b.再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

// 改造二叉搜索树为KV结构
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{public:typedef BSTreeNode<K, V> Node;bool Insert(const K& key, const V& value);Node* Find(const K& key);void InOrder();bool Erase(const K& key);private:Node* _root = nullptr;};
//KV模型
//1.英汉词典
int main()
{Key_Vaule::BSTree<string, string> dictionary;dictionary.Insert("sort", "排序");dictionary.Insert("left", "左边");dictionary.Insert("right", "右边");dictionary.Insert("insert", "插入");dictionary.Insert("erase", "删除");string str;while (cin >> str){Key_Vaule::BSTreeNode<string, string>* ret = dictionary.Find(str);if (ret){//找到单词输出cout << ret->_value << endl;}else{//未找到单词cout << "No words found!" << endl;}}
}//2统计单词次数
int main()
{// 统计水果出现的次数string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };Key_Vaule::BSTree<string, int> countWords;for (auto& e : arr){Key_Vaule::BSTreeNode<string, int>* ret = countWords.Find(e);if (ret == nullptr){countWords.Insert(e, 1);}else{ret->_value++;}}countWords.InOrder();return 0;
}

1.5 二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么等待后续我们的AVL树和红黑树章节讲到再说。

二、二叉搜索树源码

👉😉 gitee ==> 二叉搜索树的基本实现及二叉搜索树的应用(K模型、KV模型)

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

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

相关文章

数据结构-快速排序“人红是非多”?看我见招拆招

目录 1.快速排序 Hoare版本&#xff1a; 挖坑法&#xff1a; 前后指针版本: 快速排序的时间复杂度 2.快速排序的优化 三数取中法选key 随机数选key 三路划分法 3. 非递归实现快速排序 1.快速排序 快速排序一共有三种版本&#xff1a;Hoare版本、挖坑法、前后指针版本…

etoken是什么意思,有什么作用?

EToken是一种数字货币&#xff0c;它是由以太坊区块链平台发行的智能合约&#xff0c;旨在为以太坊生态系统提供一种安全、可靠、去中心化的交易媒介。EToken具有多种作用&#xff0c;下面将详细介绍。 一、EToken的定义和发行 EToken是由以太坊智能合约创建的数字货币&#xf…

MONGODB 的基础 NOSQL注入基础

首先来学习一下nosql 这里安装就不进行介绍 只记录一下让自己了解mongodb ubuntu 安装后 进入 /usr/bin ./mongodb即可进入然后可通过 进入的url链接数据库 基本操作 show dbshow dbsshow tablesuse 数据库名插入数据db.admin.insert({json格式的数据})例如 db.admin.inse…

【从入门到起飞】JavaSE—多线程(2)(生命周期,线程安全问题,同步方法)

&#x1f38a;专栏【JavaSE】 &#x1f354;喜欢的诗句&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。 &#x1f386;音乐分享【如愿】 &#x1f384;欢迎并且感谢大家指出小吉的问题&#x1f970; 文章目录 &#x1f354;生命周期&#x1f384;线程的安全问题&#…

(内部资料)收下这几个人工智能学习秘籍!

秘籍一&#xff1a;练好基本功 学习基础知识&#xff1a;人工智能涉及多个学科领域&#xff0c;包括数学、计算机科学、统计学等。因此&#xff0c;学习基础知识是非常重要的。您可以通过学习线性代数、概率论和微积分等数学基础知识&#xff0c;以及掌握Python编程语言和常用…

Mybatis一级缓存和二级缓存原理剖析与源码详解

Mybatis一级缓存和二级缓存原理剖析与源码详解 在本篇文章中&#xff0c;将结合示例与源码&#xff0c;对MyBatis中的一级缓存和二级缓存进行说明。 MyBatis版本&#xff1a;3.5.2 文章目录 Mybatis一级缓存和二级缓存原理剖析与源码详解⼀级缓存场景一场景二⼀级缓存原理探究…

Fiddler模拟弱网环境

1.设置弱网&#xff1a;Rules-》Customize Rules 上传速度&#xff1a;1KB/300ms1KB/0.3s3.33KB/s 下载速度&#xff1a;1KB/150ms1KB/0.15s6.67KB/s 2.启动弱网&#xff1a;Rules-》Performance-》Simulate Modem Speeds 开启后&#xff0c;此项为勾选状态 3.验证弱网生效…

推荐系统概述(PPT)

参考资料&#xff1a; 推荐系统系列之推荐系统概览&#xff08;上&#xff09; | 亚马逊AWS官方博客推荐系统系列之推荐系统概览&#xff08;下&#xff09; | 亚马逊AWS官方博客 目录如下&#xff1a; 推荐系统简介 推荐系统中常见概念 推荐系统中常用的评价指标 首页推荐…

C# Onnx 百度PaddleSeg发布的实时人像抠图PP-MattingV2

目录 效果 模型信息 项目 代码 下载 效果 图片源自网络侵删 模型信息 Inputs ------------------------- name&#xff1a;img tensor&#xff1a;Float[1, 3, 480, 640] --------------------------------------------------------------- Outputs -----------------…

Linux运行jmeter报错java.sql.SQLException:Cannot create PoolableConnectionFactory

在性能测试过程中遇见1个问题&#xff0c;终于解决了&#xff0c;具体问题如下。 问题 在windows电脑写jmeter脚本连接数据库连接成功 然后把该脚本放到Linux服务器上面&#xff0c;并把jmeter mysql驱动放到服务器上面&#xff0c;修改jmeter的mysql驱动路径信息 注意&…

系统试运行方案

系统试运行的目的&#xff1a; 试运行目的通过既定时间段的试运行&#xff0c;全面考察项目建设成果。并通过试运行发现项目存在的问题&#xff0c;从而进一步完善项目建设内容&#xff0c;确保项目顺利通过竣工验收并平稳地移交给运行管理单位。通过实际运行中系统功能与性能的…

感恩节的习俗 Custom of Family Dinner

感恩节是美国最普遍庆祝的传统节日之一。在每年11月的第四个星期四&#xff0c;感恩节如期而至。Thanksgiving is one of the most universally celebrated traditional American holidays. Every year, Thanksgiving arrives on the fourth Thursday of November without fail…