【C++】探秘二叉搜索树

ops/2024/11/14 15:30:31/
头像
🚀个人主页:奋斗的小羊
🚀所属专栏:C++
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述

目录

  • 前言
  • 💥一、二叉搜索树
    • 💥1.1 特点
    • 💥1.2 基本操作
      • 💥1.2.1 插入
      • 💥1.2.2 查找
      • 💥1.2.3 删除
      • 💥1.2.4 中序遍历
      • 💥1.2.5 二叉搜索树的简单实现
    • 💥1.3 二叉搜索树的应用
      • 💥1.3.1 Key模型
      • 💥1.3.2 Key-Value模型
      • 💥1.3.3 性能分析


前言

本篇文章将介绍一种功能更加强大的二叉树——二叉搜索树。
相比于普通的二叉树,二叉搜索树在很多方面都有优势,尤其是在查找数据上效率明显提高,并且通过中序遍历二叉搜索树它所存储的数据是有序的。


💥一、二叉搜索树

💥1.1 特点

二叉搜索树,是一种特殊的二叉树结构,它或为空树,或者满足以下性质:

  • 左子树性质: 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
  • 右子树性质: 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值
  • 中序遍历性质: 对二叉搜索树进行中序遍历(左根右),则遍历的结果是一个按升序排列的有序序列

在这里插入图片描述

二叉搜索树和普通二叉树的框架差不太多,需要一个节点类,再封装一个完成二叉搜索树功能的类就行。

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 BSTree
{typedef BSTNode<K> Node;
public://查找bool Find(const K& key){}//插入bool Insert(const K& key){}//删除bool Erase(const K& key){}//中序遍历void InOrder(){}
private://用一个节点指针来管理二叉搜索树,缺省值为nullptrNode* _root = nullptr;
};

💥1.2 基本操作

💥1.2.1 插入

  • 树为空: 直接新增节点,连接到根节点上
  • 树不为空: 从根节点开始,将要插入的值与根节点的值进行比较。如果小于根节点的值,则进入左子树继续比较;如果大于根节点的值,则进入右子树继续比较。如果当前节点为空,则在该位置插入新节点。

插入数据要始终保持二叉搜索树的特点。另外二叉搜索树中的数据通常不允许重复
,所以如果要插入的数据在二叉搜索树中已经存在,则插入错误。

| 实现:

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

💥1.2.2 查找

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

| 实现:

bool 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 true;}}return false;
}
  • 查找过程的平均时间复杂度和最坏时间复杂度分别是O(log n)和O(n)

其中n是树中节点的数量。然而,在最坏的情况下(即树退化为链表时),时间复杂度会变为O(n)。为了保持查找效率,通常会使用平衡二叉搜索树(如AVL树、红黑树等),它们通过自平衡操作来确保树的高度保持在对数级别,在后续的文章中会介绍。

在这里插入图片描述


💥1.2.3 删除

在二叉搜索树中删除一个节点是一个稍微复杂的过程,因为它需要确保删除操作后树仍然保持二叉搜索树的属性。

  1. 查找要删除的节点: 首先,通过二叉搜索树的查找操作找到要删除的节点
  2. 处理三种情况:
  • 情况1: 如果要删除的节点是叶子节点,则直接删除该节点,并将其父节点指向它的指针设为nullptr
  • 情况2: 如果要删除的节点只有一个子节点(左子节点或右子节点),则将其父节点指向它的指针重定向到它的子节点,然后删除该节点
  • 情况3: 如果要删除的节点有两个子节点,则可以找到该节点的右子树中的最小节点(或左子树中的最大节点,但通常使用右子树中的最小节点,因为它更简单且保持了树的平衡),然后将该最小节点的值复制到要删除的节点中,并删除右子树中的那个最小节点(这实际上变成了情况1或情况2)

在这里插入图片描述

在这里插入图片描述

从上面两种不同的情况来看,找到目标节点右子树中的最小节点后,这个最小节点是它父节点的左孩子还是右孩子都是有可能的,所以在替换掉目标节点的值后,删除右子树中的最小节点之前,需要判断一下右子树中的最小节点的孩子是跟在其父节点的左还是右。

| 实现:

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->_left == nullptr){if (_root->_key == key){_root = _root->_right;}else{if (parent->_left == cur){parent->_left = cur->_right;}else if (parent->_right == cur){parent->_right = cur->_right;}}delete cur;cur = nullptr;return true;}else if (cur->_right == nullptr){if (_root->_key == key){_root = _root->_left;}else{if (parent->_left == cur){parent->_left = cur->_left;}else if (parent->_right == cur){parent->_right = cur->_left;}}delete cur;cur = nullptr;return true;}//有两个孩子else{//用要删除节点的右子树的最小节点代替删除节点parent = cur;Node* MinNode = cur->_right;while (MinNode->_left){parent = MinNode;MinNode = MinNode->_left;}cur->_key = MinNode->_key;if (parent->_left == MinNode){parent->_left = MinNode->_right;}else{parent->_right = MinNode->_right;}delete MinNode;return true;}}}return false;
}

找到要删除的节点后,删除之前有几个值得特别注意的点:

  • 要删除的节点没有孩子或只有一个孩子可以归为同一种情况处理
  • 要删除的节点可能是当前的根节点,如果是根节点要单独特殊处理,因为根节点没有父节点,和其他节点的删除处理方法不一样
  • 如果目标节点有两个孩子,则需要先找到其右子树中的最小节点,让最小节点的值替换掉目标节点的值,这个最小节点一定是叶子节点或者只有一个右孩子(两种情况可以视为同一情况处理),最小节点的右孩子要根据最小节点是其父节点的左孩子还是右孩子来确定接在哪里

💥1.2.4 中序遍历

二叉搜索树的中序遍历和普通二叉树的中序遍历是一样的。

void InOrder(const Node* root)
{if (root == nullptr){return;}InOrder(root->_left);cout << root->_key << " ";InOrder(root->_right);
}

但是这样写有一个问题,当我们调用中序遍历函数时,需要传一个二叉搜索树的根节点指针,而_rootBSTree类中的私有成员变量,在类外面不能使用,所以我们需要想一些办法。
既然类的私有成员变量在类外面不能使用,那我们就不在类外面使用了,可以通过函数调用在类里面间接的使用_root成员变量。

template<class K>
class BSTree
{typedef BSTNode<K> Node;
public://查找bool Find(const K& key){}//插入bool Insert(const K& key){}//删除bool Erase(const K& key){}//中序遍历void InOrder(){_InOrder(_root);cout << endl;}
private:void _InOrder(const Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}Node* _root = nullptr;
};

💥1.2.5 二叉搜索树的简单实现

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 BSTree
{typedef BSTNode<K> Node;
public:bool 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 true;}}return false;}bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_key > key){parent = cur;cur = cur->_left;}else if (cur->_key < key){parent = cur;cur = cur->_right;}else{return false;}}if (parent->_key > key){parent->_left = new Node(key);}else{parent->_right = new Node(key);}}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->_left == nullptr){if (_root->_key == key){_root = _root->_right;}else{if (parent->_left == cur){parent->_left = cur->_right;}else if (parent->_right == cur){parent->_right = cur->_right;}}delete cur;cur = nullptr;return true;}else if (cur->_right == nullptr){if (_root->_key == key){_root = _root->_left;}else{if (parent->_left == cur){parent->_left = cur->_left;}else if (parent->_right == cur){parent->_right = cur->_left;}}delete cur;cur = nullptr;return true;}//有两个孩子else{//用要删除节点的右子树的最小节点代替删除节点parent = cur;Node* MinNode = cur->_right;while (MinNode->_left){parent = MinNode;MinNode = MinNode->_left;}cur->_key = MinNode->_key;if (parent->_left == MinNode){parent->_left = MinNode->_right;}else{parent->_right = MinNode->_right;}delete MinNode;return true;}}}return false;}void InOrder(){_InOrder(_root);cout << endl;}
private:void _InOrder(const Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}Node* _root = nullptr;
};

💥1.3 二叉搜索树的应用

💥1.3.1 Key模型

在K模型中,每个节点仅存储一个关键码(即Key),这个关键码用于确定节点在树中的位置,并满足二叉搜索树的性质。

  • 查找: 给定一个关键码,在二叉搜索树中查找是否存在对应的节点。查找过程从根节点开始,根据关键码与节点关键码的比较结果,决定是向左子树查找还是向右子树查找,直到找到对应的节点或确定不存在该关键码。
  • 应用场景: 可以用来拼写检查, 在文本编辑器或浏览器中,可以使用二叉搜索树的K模型来存储字典中的单词,以便快速检查用户输入的单词是否存在拼写错误。

我们上面实现的二叉搜索树就是一个简单的Key模型。


💥1.3.2 Key-Value模型

定义:

  • KV模型中的每个节点包含一个键值(Key)和一个数据值(Value),键值用于确定节点在树中的位置,数据值用于存储节点的附加信息
  • 节点的键值仍然必须满足二叉搜索树的性质,但数据值可以是任意类型或对象

应用场景:

  • 简单词典: 在英汉词典中,英文单词作为Key,其对应的中文解释作为Value。用户可以通过输入英文单词快速找到其对应的中文含义
  • 车库收费: 车进入车库时存储车牌号作为Key,进入的时间作为Value,当车从车库出来时按车牌号查找对应进入车库的时间
  • 计数器: 在需要统计元素出现次数的场景中,可以将元素作为Key,其出现的次数作为Value

这里我们以上面实现的二叉搜索树为基础,实现一个简单词典的KV模型。

template<class K, class V>
struct BSTNode
{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: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;}bool Insert(const K& key, const V& value){if (_root == nullptr){_root = new Node(key, value);return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_key > key){parent = cur;cur = cur->_left;}else if (cur->_key < key){parent = cur;cur = cur->_right;}else{return false;}}if (parent->_key > key){parent->_left = new Node(key, value);}else{parent->_right = new Node(key, value);}}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->_left == nullptr){if (_root->_key == key){_root = _root->_right;}else{if (parent->_left == cur){parent->_left = cur->_right;}else if (parent->_right == cur){parent->_right = cur->_right;}}delete cur;cur = nullptr;return true;}else if (cur->_right == nullptr){if (_root->_key == key){_root = _root->_left;}else{if (parent->_left == cur){parent->_left = cur->_left;}else if (parent->_right == cur){parent->_right = cur->_left;}}delete cur;cur = nullptr;return true;}//有两个孩子else{//用要删除节点的右子树的最小节点代替删除节点parent = cur;Node* MinNode = cur->_right;while (MinNode->_left){parent = MinNode;MinNode = MinNode->_left;}cur->_key = MinNode->_key;if (parent->_left == MinNode){parent->_left = MinNode->_right;}else{parent->_right = MinNode->_right;}delete MinNode;return true;}}}return false;}void InOrder(){_InOrder(_root);cout << endl;}
private:void _InOrder(const Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " " << root->_value << endl;_InOrder(root->_right);}Node* _root = nullptr;
};

请添加图片描述


💥1.3.3 性能分析

KV模型的二叉搜索树在查找、插入和删除操作上的性能通常取决于树的高度。在最坏的情况下(即树蜕化为单边树时),这些操作的时间复杂度会退化为O(n)。
可以使用平衡二叉搜索树来提高性能:

  • AVL树: 任何节点的两个子树的高度最大差别为一,这通过自平衡操作(如左旋、右旋)来维持
  • 红黑树: 一种自平衡二叉查找树,它通过每个节点附加一个表示颜色的位(红色或黑色)和通过确保树满足某些性质(如从根到叶子的最长的可能路径不多于最短的可能路径的两倍长)来保持平衡

关于平衡二叉搜索树的介绍还请在我专栏 C++ 中查找相关文章。
在平均情况下,这些操作的时间复杂度可以接近O(log n),这使得KV模型的二叉搜索树成为一种高效的数据结构。


本篇文章的学习就到这了,如果您觉得在本文中有所收获,还请留下您的三连支持哦~

头像

http://www.ppmy.cn/ops/113793.html

相关文章

Django学习实战篇五(适合略有基础的新手小白学习)(从0开发项目)

前言&#xff1a; 本章中&#xff0c;我们开始引入前端框架Bootstrap 来美化界面。在前面的章节中&#xff0c;我们通过编写后端代码来处理数据。数据之于网站&#xff0c;就相当于灵魂之于人类。而网站的前端就相当于人的形体外貌。其中HTML是骨架&#xff0c;而CSS是皮肤&…

前端进阶,使用Node.js做中间层,实现接口转发和服务器渲染

在Web开发中&#xff0c;Node.js经常被用作中间层&#xff08;也称为后端或服务器端&#xff09;&#xff0c;用于处理各种任务&#xff0c;包括接口转发&#xff08;API Gateway&#xff09;、服务器渲染&#xff08;Server-Side Rendering, SSR&#xff09;等。下面我将分别解…

一周热门|重磅!AI无限学习、进化,研究登上Nature;Meta提出多模态模型训练方法Transfusion

大模型周报将从【企业动态】【技术前瞻】【政策法规】【专家观点】四部分&#xff0c;带你快速跟进大模型行业热门动态。 01 企业动态 Ideogram 推出文生图模型 Ideogram 2.0 日前&#xff0c;Ideogram 推出了新版本文本到图像模型 Ideogram 2.0。据介绍&#xff0c;Ideogra…

二层、三层网络基本原理

文章目录 二层网络整体拓扑相关配置配置namespace创建switch创建veth设备配置veth的IP启动veth 测试 三层网络配置vm1配置vm2配置 测试 二层网络 我们用Linux bridge模拟现实中的switch&#xff0c;用namespace模拟连接在交换机上的pc 整体拓扑 ------------------ ----…

Flask-SQLAlchemy一对多 一对一 多对多关联

一. 组织一个 Flask 项目通常需要遵循一定的结构&#xff0c;以便代码清晰、可维护。下面是一个典型的 Flask 项目结构&#xff1a; my_flask_app/ │ ├── app/ │ ├── __init__.py │ ├── models.py │ ├── views.py │ ├── forms.py │ ├── tem…

vue无法通过页面路径访问提示404,通过nginx配置处理

部署vue项目时&#xff0c;可以通过IP的方式访问主页&#xff0c;当进入特定页面在刷新时&#xff0c;因为浏览器通过URL地址进行请求&#xff0c;就提示404错误。 每次都需要重新从主页进入&#xff0c;这里是因为nginx配置的问题&#xff0c;在nginx里增加一行重定向的设置 …

蓝桥杯-STM32G431RBT6(UART解析字符串sscanf和解决串口BUG)

一、C语言常识 printf和sprintf的主要区别在于它们的功能和用途&#xff1a; printf&#xff1a;主要用于将格式化的数据输出到标准输出&#xff08;如屏幕&#xff09;。sprintf&#xff1a;则是将格式化的数据存储到一个指定的字符串缓冲区中&#xff0c;而不是直接输出。 pr…

CentOS7更换阿里云yum更新源

目前CentOS内置的更新安装源经常报错无法更新&#xff0c;或者速度不够理想&#xff0c;这个时候更换国内的镜像源就是一个不错的选择。 备份内置更新源 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 下载阿里云repo源&#xff08;需要系统…