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

『C++成长记』一颗会搜索的二叉树

🔥博客主页:小王又困了

📚系列专栏:C++

🌟人之为学,不日近则日退

❤️感谢大家点赞👍收藏⭐评论✍️

目录

一、二叉搜索树的概念

二、二叉搜索树的操作

📒2.1二叉搜索树的查找

📒2.2二叉搜索树的插入

📒2.3 二叉搜索树的删除

三、二叉搜索树的实现

📒3.1 BSTNode(结点类)

📒3.2 BSTree(二叉搜索树类)

🗒️3.2.1框架

🗒️3.2.2 插入

🗒️3.2.3 查找

🗒️3.2.4 中序遍历

🗒️3.2.5删除

🗒️3.2.6析构

🗒️3.2.7拷贝构造

四、二叉搜索树的应用

📒4.1K模型

📒4.2KV模型

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


一、二叉搜索树的概念

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

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

二、二叉搜索树的操作

📒2.1二叉搜索树的查找

  1. 从根开始比较,比根大则往右边走查找,比根小则往左边走查找。
  2. 最多查找高度次,走到到空,还没找到,这个值不存在。

小Tips:这里最多查找高度次的时间复杂度并不是O(logN),这是在理想的情况下,即这颗二叉树是一颗满二叉树或者完全二叉树。在极端情况下,这棵二叉树只有一条路径,此时最多查找高度次的时间复杂度就是O(N)。

📒2.2二叉搜索树的插入

插入的具体过程如下:

  1. 树为空:直接新增节点,赋值给 root 指针
  2. 树不空:按二叉搜索树性质查找插入位置,插入新节点。

📒2.3 二叉搜索树的删除

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

  1. 要删除的结点没有孩子结点。

  2. 要删除的结点只有左孩子结点。

  3. 要删除的结点只有右孩子结点。

  4. 要删除的结点有左、右孩子节点。

看起来待删除节点有4中情况,实际情况 a 可以与情况 b 或者 c 合并起来,我们可以分为有 0或1个孩子有2个孩子 这两种情况。因此真正的删除过程如下:

  • 情况一(要删除的结点只有左孩子):删除该结点,让被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
  • 情况二(要删除的结点只有又孩子):删除该结点,让被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
  • 情况三(要删除的结点有两个孩子):在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除

三、二叉搜索树的实现

        二插搜索树只是一种结构,它本质上是由一个个结点链接而成,因此我们首先需要定义一个结点类,这个结点用来存储数据。有了结点类之后就需要定义一个二叉搜索树的类,这个类主要是用来维护结构的,实现增删查改等功能。

📒3.1 BSTNode(结点类)

template<class K>
struct BSTNode
{K _key;BSTNode<K>* _left;BSTNode<K>* _right;BSTNode(const K& key): _key(key), _left(nullptr), _right(nullptr){}
};

📒3.2 BSTree(二叉搜索树类)

🗒️3.2.1框架

template<class K>
class BSTree
{typedef BSTNode<K> Node;
public:BinarySearckTree():_root(nullptr){}
private:Node* _root;
};

🗒️3.2.2 插入

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->_right = cur;}else{parent->_left = cur;}return true;
}

小Tips:我们要单独考虑根节点为空的情况。用 cur 找到该结点应该要插入的位置,用 parent 指向该位置的双亲结点,以实现链接关系。最后还需要判断插入到双亲结点的左侧还是右侧。我们实现的这个二叉搜索树相同的值只能插入一次,因此当插入一个值 key 的时候,如果检测到树中已经有一个结点存的是 key,那么就应该返回 false,表明插入失败。

🗒️3.2.3 查找

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

🗒️3.2.4 中序遍历

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);}

小Tips:这里的中序遍历是用递归的方式来实现的,但是递归函数是要传递参数的,所以我们要把根节点 _root 传给这个函数,但是根节点 _root 是私有的成员变量,用户是访问不到的,因此我们不能直接提供中序遍历函数给用户。正确的做法是,虽然用户访问不到根结点,但是类里面可以访问,所以我们在类里面实现一个中序遍历的子函数 _InOrder,在这个子函数中实现中序遍历的逻辑,然后我们再去给用户提供一个中序遍历的函数接口 InOrder,由它去调用 _InOrder。这样以来用户就可以正常去使用中序遍历啦。

🗒️3.2.5删除

bool Erase(const K& key)
{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{// 删除// 0-1个孩子的情况if (cur->_left == nullptr)//如果cur只有右孩子{if (parent == nullptr)//删除根结点{_root = cur->_right;}else{if (parent->_left == cur)parent->_left = cur->_right;elseparent->_right = cur->_right;}		delete cur;return true;}else if (cur->_right == nullptr)//如果cur只有左孩子{if (parent == nullptr)//删除根结点{_root = cur->_left;}else{if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;}delete cur;return true;}else{// 2个孩子的情况// 右子树的最小节点作为替代节点Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left){rightMinP = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;if (rightMinP->_left == rightMin)rightMinP->_left = rightMin->_right;elserightMinP->_right = rightMin->_right;delete rightMin;return true;}}}return false;
}

小Tips:删除节点有两个孩子时,我们要先找到右子树的最小节点 rightMin ,同时也要记录 rightMinP ,从 cur 的右子树向左一直找即可。找到后交换 currightMin 的值,这时我们就将问题转换为删除 rightMin 节点。

🗒️3.2.6析构

private:	void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}
public:~BSTree(){Destroy(_root);_root = nullptr;}

小Tips:这里析构使用后序遍历,先去释放左孩子和右孩子的空间资源,最后释放根节点。

🗒️3.2.7拷贝构造

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;}
public:BSTree(const BSTree<K>& t){_root = Copy(t._root);}

四、二叉搜索树的应用

📒4.1K模型

        K模型即只有一个 Key 作为关键码,结构中只需要存储 Key 即可,关键码即为需要搜索到的值。比如:给一个单词 word,判断该单词是否拼写正确,具体方式如下:

  • 以词库中所有单词集合中的每个单词作为 Key,构建一颗二叉搜索树。
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

上面这颗二叉搜索树就是 Key 模型,因为这颗树的结点里面只能存储一个值,这个值就是 Key。

📒4.2KV模型

       KV 模型即每一个关键码 Key,都有与之对应的的值 Value,即 <Key,Value> 的键值对。这种方式在现实生活中十分常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文 <word,Chinese> 就构成一种键值对。
  • 再比如统计单词次数,统计成功后,给定单词就可以快速找到其出现的次数,单词与其出现次数就是 <word,count> 就构成一种键值对。
namespace keyValue
{template<class K, class V>struct BSTNode{// pair<K, V> _kv;K _key;V _value;BSTNode<K, V>* _left;BSTNode<K, V>* _right;BSTNode(const K& key, const V& value):_key(key), _value(value), _left(nullptr), _right(nullptr){}};template<class K, class V>class BSTree{typedef BSTNode<K, V> Node;public:BSTree() = default;BSTree(const BSTree<K, V>& t){_root = Copy(t._root);}~BSTree(){Destroy(_root);_root = nullptr;}bool Insert(const K& key, const V& value){if (_root == nullptr){_root = new Node(key, value);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, value);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;}Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else{return cur;}}return nullptr;}bool Erase(const K& key){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{// 删除// 0-1个孩子的情况if (cur->_left == nullptr){if (parent == nullptr){_root = cur->_right;}else{if (parent->_left == cur)parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;return true;}else if (cur->_right == nullptr){if (parent == nullptr){_root = cur->_left;}else{if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;}delete cur;return true;}else{// 2个孩子的情况// 右子树的最小节点作为替代节点Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left){rightMinP = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;if (rightMinP->_left == rightMin)rightMinP->_left = rightMin->_right;elserightMinP->_right = rightMin->_right;delete rightMin;return true;}}}return false;}void InOrder(){_InOrder(_root);cout << endl;}private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << ":" << root->_value << endl;_InOrder(root->_right);}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}Node* Copy(Node* root){if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_key, root->_value);newRoot->_left = Copy(root->_left);newRoot->_right = Copy(root->_right);return newRoot;}private:Node* _root = nullptr;};
}

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

        插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二插搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树。

最优情况下:二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log2n 。

最差情况下:二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N/2。如果退化成了单支树,那么二叉搜索树的性能就失去了。此时就需要用到即将登场的 AVL 树和红黑树了。


🎁结语: 

     本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位读者三连支持。文章有问题可以在评论区留言,博主一定认真认真修改,以后写出更好的文章。你们的支持就是博主最大的动力。

http://www.dtcms.com/a/442302.html

相关文章:

  • 【经验分享】JWE 详解:比 JWT 更安全的令牌技术
  • 【连载6】数据库未来发展趋势展望,附例子,避坑指南以及面试题
  • 【深度学习计算机视觉】09:语义分割和数据集——核心概念与关键技术解析
  • 直播网站建设重庆数据分析师35岁以后怎么办
  • 【Ray大模型分布式训练】
  • 浦东做营销网站天津网站建设制作
  • 网站建设网银江西门户网站建设
  • [初学C语言]C语言数据类型和变量
  • 资源提示符
  • 人机协同如何突破功能分配的 “天花板”?
  • Spring Cloud Netflix Ribbon:微服务的客户端负载均衡利器
  • Docker 数据卷与存储机制(持久化与共享实战)
  • 做环评工作的常用网站电商网站分析
  • 【常用字符串相关函数】
  • unsigned 是等于 unsigned int
  • 营销型企业网站建设案例网站建设功能分为几种
  • 2058. 找出临界点之间的最小和最大距离
  • Leetcode 347. 前 K 个高频元素 堆 / 优先队列
  • python 做网站 案例那个网站专利分析做的好
  • 获得场景视频API开发(01):CC视频平台分片上传服务的设计与实践
  • Spring Boot整合缓存——Ehcache缓存!超详细!
  • 数据结构 之 【LRU Cache】(注意list的splice接口函数)
  • 专门做封面的网站免费做网站怎么做网站吗
  • 25秋新三上语文1-8单元作文习作指导含填空练习+三年级上册语文各单元习作范文/写作技巧/填空练习+完整电子版可下载打印
  • 拥抱终端:Linux 新手命令行入门指南
  • wordpress 512做网站优化的协议书
  • 设计稿秒出“热力图”:AI预测式可用性测试工作流,上线前洞察用户行为
  • 种完麦子,就往南走
  • 像素时代网站建设手机站设计菏泽网站建设推广
  • python爬虫scrapy框架使用