【C++】从零开始构建二叉搜索树

在这里插入图片描述
送给大家一句话:
我们始终有选择的自由。选错了,只要真诚的反思,真诚面对,也随时有机会修正错误和选择。
– 《奇迹男孩(电影)》

💻💻💻💻💻💻💻💻
💗💗💗💗💗💗💗💗


⚜️从零开始构建二叉搜索树⚜️

  • ✅1 前言
  • ✅2 二叉搜索树(BST)
    • 2.1 什么是二叉搜索树
    • 2.2 二叉搜索树的功能
  • ✅3 实现二叉搜索树
    • 3.1 整体框架
    • 3.2 插入功能
    • 3.3 中序遍历
    • 3.4 搜索功能
    • 3.5 删除操作
  • ✅4 应用一下
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

✅1 前言

在之前初阶数据结构的篇章中,我们学习过二叉树的基础知识稍微复习一下

  1. 二叉树的度不超过 2
  2. 二叉树可以通过数组或链表结构记录(左孩子右兄弟法)

普通的二叉树没有特别的性质,今天我们就来赋予其一个全新的性质来满足高速搜索的需求 ,并为后序的map与set做铺垫 ,二叉搜索树的特性了解,有助于更好的理解map和set的特性

✅2 二叉搜索树(BST)

2.1 什么是二叉搜索树

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

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树
  • 注意通常二叉搜索树不会有相同的键值

这些性质使得二叉搜索树成为一种高效的搜索工具。在大部分情况下,对于包含 n 个节点的二叉搜索树,搜索、插入和删除等操作的时间复杂度为 O(logn)。然而,在某些情况下,二叉搜索树可能会出现不平衡的情况,导致时间复杂度激增至 O(n)。为了解决这个问题,出现了进阶版的 AVL 树和红黑树

AVL 树红黑树 都是在保持二叉搜索树基本性质的基础上,通过旋转和重新平衡等操作,确保树的高度保持在一个相对平衡的状态,从而保证了操作的时间复杂度始终为 O(logn)。它们的出现大大提高了二叉搜索树在实际应用中的性能和稳定性。
我们常常会选择使用 AVL 树或红黑树来解决搜索问题

今天,我们主要来学习二叉搜索树,为后序的学习打好基础!!!

2.2 二叉搜索树的功能

二叉搜索树(Binary Search Tree, BST)是一种非常实用的数据结构,用于存储具有可比较键的数据项。其功能和应用场景非常广泛,主要包括以下几点:

✨核心功能✨

  1. 🎈搜索:提供高效的搜索功能,允许快速查找特定键值的数据项。如果树保持平衡,搜索的平均时间复杂度可以保持在 O(log n)。
  2. 🎈插入和删除:允许在保持树结构的前提下添加和移除节点。插入和删除操作也尽量维持树的平衡,以避免性能下降。
  3. 🎈排序:可以中序遍历二叉搜索树以获得有序的数据序列,这对于数据排序和报表生成等功能非常有用。

✨应用场景✨

  1. 🎈数据库管理系统:许多数据库索引就是使用二叉搜索树或其变种(如B树、红黑树)来实现的,以便快速地查询和更新数据。
  2. 🎈符号表应用:在编译器实现中,二叉搜索树可以用来构建和管理符号表,以支持变量名的快速查找和属性的存取。
  3. 🎈优先队列实现:通过特定方式实现的二叉搜索树(如二叉堆),可以用于实现优先队列,支持快速插入元素和删除最小或最大元素的操作。

接下来我们就根据其性质,来实现二叉搜索树 ❗ ❗ ❗

✅3 实现二叉搜索树

3.1 整体框架

🎇首先我们需要搭建一个整体的框架,设计节点结构体和二叉搜索树类

我们创建一个节点结构体:

  1. 包括左右指针
  2. 键值记录节点值

二叉搜索树类仅需要需要一个根节点足矣!

//节点结构体 
template<class K>
struct BSTreeNode
{//构造函数BSTreeNode(K key = 0): _key(key),_right(nullptr),_left(nullptr){}//左右指针BSTreeNode* _right;BSTreeNode* _left;//键值K _key;};
//树的结构
template<class K>
class BSTree
{
public:typedef BSTreeNode<K>* Node*;private:Node* _root = nullptr;
};

有了框架,我们就来逐个实现功能!!!

3.2 插入功能

❤️‍🔥根据二叉搜索树的性质来寻找到合适的位置即可,注意:

  1. 需要一个当前节点指针和父节点指针,因为插入需要父节点来进行!!!
  2. 如果根节点为空指针,那么直接赋值给根节点就可以
  3. 小于当前键值就放左边,大于当前键值就放右边,直到找到合适位置
void Insert(K key)
{//先判断是否为空if (_root == nullptr){_root = new BSTreeNode<K>(key);}//不为空 就寻找合适位置进行插入else{Node* cur = _root;Node* parent= nullptr;//寻找合适位置while (cur != nullptr){//小于当前键值就放左边if (key < cur->_key) {parent = cur;cur = cur->_left;}else{parent = cur;cur = cur->_right;}}//创建节点cur = new BSTreeNode<K>(key);//已经找到合适位置://大于父节点就放在右边if (key > parent->_key){parent->_right = cur;}//反之放在左边else{parent->_left = cur;}}
}

❤️‍🔥 这样就写好了,为了方便我们的调试,我们赶紧来写一个中序遍历!!!

3.3 中序遍历

递归版本的中序遍历很简单😎😎😎:

//嵌套一层来换行
void InOrder()
{_InOrder(this->_root);cout << endl;
}
//中序遍历
void _InOrder(Node* cur)
{if (cur == nullptr) return;_InOrder(cur->_left);cout << cur->_key << " ";_InOrder(cur->_right);
}

逻辑比较很好理解奥!!!
我们测试一下:

void BSTreeTest1()
{BSTree<int> tree;int arr[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };for (auto s : arr){tree.Insert(s);}tree.InOrder();//cout << tree.Find(5) << endl;//cout << tree.Find(1) << endl;
}

来看效果:
在这里插入图片描述
🎉🎉🎉🎉🎉🎉🎉🎉

👌👌👌,也验证了我们的插入没有问题了!!!

3.4 搜索功能

搜索功能直接套用刚才的插入就可以了

bool Find(K key)
{//先判断是否为空if (_root == nullptr){return false;}//不为空 就寻找合适位置进行插入else{Node* cur = _root;while (cur != nullptr){if (key == cur->_key){return true;}//小于当前键值就在左边else if (key < cur->_key){cur = cur->_left;}//反之在右边else{cur = cur->_right;}}//没有找到return false;}
}

❤️‍🔥这样寻找就实现了❤️‍🔥

3.5 删除操作

⚠️前方高能预警⚠️

删除操作是二叉搜索树最关键,也是最复杂度的功能!!!
🔆🔆🔆先请来如来佛祖🔆🔆🔆

//大佛镇压bug

//                          _ooOoo_                               //
//                         o8888888o                              //
//                         88" . "88                              //
//                         (| - - |)                              //
//                         O\  -  /O                              //
//                      ____/`---'\____                           //
//                    .'  \\|     |//  `.                         //
//                   /  \\|||  :  |||//  \                        //
//                  /  _||||| -:- |||||-  \                       //
//                  |   | \\\  -  /// |   |                       //
//                  | \_|  ''\---/''  |   |                       //
//                  \  .-\__  `-`  ___/-. /                       //
//                ___`. .'  /--.--\  `. . ___                     //
//              ."" '<  `.___\_<|>_/___.'  >'"".                  //
//            | | :  `- \`.;`\ _ /`;.`/ - ` : | |                 //
//            \  \ `-.   \_ __\ /__ _/   .-` /  /                 //
//      ========`-.____`-.___\_____/___.-`____.-'========         //
//                           `=---='                              //
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        //
//         佛祖保佑          永无BUG          永不修改               //

我们不能轻举妄动,先来分析一下可能会出现的情况:

  1. 首先我们要找到被删除的节点
  2. 然后是删除,这个删除不能随便删除奥,先分析一下可能出现的情况:
    • 1️⃣要删除的结点无孩子结点
    • 2️⃣要删除的结点只有左孩子结点
    • 3️⃣要删除的结点只有右孩子结点
    • 4️⃣要删除的结点有左、右孩子结点
  3. 分析可能出现的情况,1️⃣2️⃣3️⃣可以归为一类A,4️⃣归为一类B

🎁先来看A类🎁
A类由于被删除的节点只有一边有节点,所以只需要把父节点指向它的指针指向它的子节点就可以!

但是要考虑一个特殊情况 ❗ ❗ ❗如果被删除的节点没有父节点(也就是删除根节点时),需要特殊处理:直接把根节点更新就可以

	bool Erase(K key){Node* cur = _root;Node* parent = nullptr;//首先需要找到需要删除的节点while (cur != nullptr){//小于当前键值就放左边if (key < cur->_key){parent = cur;cur = cur->_left;}else if(key > cur->_key){parent = cur;cur = cur->_right;}//找到了else{//A 类if (cur->_left == nullptr || cur->_right == nullptr){//因为有一边是没有元素的,所以只需要把父节点指向它的指针指向它的子节点if (cur->_left == nullptr){//没有父亲if (parent == nullptr){_root = cur->_right;}else if (cur == parent->_left) parent->_left = cur->_right;else parent->_right = cur->_right;delete cur;}else{//没有父亲if (parent == nullptr){_root = cur->_left;}//是左子节点,就改变父节点的左指针else if (cur == parent->_left) parent->_left = cur->_left;//是右子节点,就改变父节点的右指针else parent->_right = cur->_left;delete cur;}}// B类!else{//...}return true;}}//没找到要删除的值就返回falsereturn false;}

🎁再来看B类🎁
因为需要删除的节点左右指针都有值,所以不能通过上述的办法来进行操作!!!
所以采取替换法:

替换法:找一个值来替代当前值,因为不能原本的树的结构,所以要找到符合条件的值。根据二叉搜索树的性质可以找:左子树最大值或右子树最小值
🔅🔅🔅🔅🔅🔅

替换之后我们看图:
在这里插入图片描述
⚠️由于rightMin是右子树的最小值,那么它就不会有左子树,所以这时候时间将rightMinparent的指向它的指针指向它的子节点就可以。

//....// B类比较复杂!
else
{//这个情况需要找到该位置的替代值//选择左子树的最大值 或 右子树的最小值//这里我们选择右子树的最小值Node* rightMin = cur->_right;Node* rightMinparent = nullptr;//寻找右子树的最小值while (rightMin->_left){rightMinparent = rightMin;rightMin = rightMin->_left;}//找到最小值 -> 交换swap(rightMin->_key, cur->_key);//所以这时候不可以直接删除//需要判断一下对应指针!!!if (rightMinparent->_right == rightMin){rightMinparent->_right = rightMin->_right;}else{rightMinparent->_left = rightMin->_right;}delete rightMin;
}
return true;//...

再来看特殊情况:如果删除的是 8 (根节点)
在这里插入图片描述
这样因为rightMin已经是右子树最小值了,所以不会进入查找循环,rightMinparent就不会被赋值,就出现野指针了,所以要给其赋初始值:

Node* rightMinparent = cur;

💯💯💯💯💯💯
这样就写好了!!!
测试一下:

void BSTreeTest2()
{BSTree<int> tree;int arr[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };for (auto s : arr){tree.Insert(s);}tree.InOrder();for (auto s : arr){tree.Erase(s);tree.InOrder();}
}

🎊来看效果🎊:
在这里插入图片描述

这样我们的二叉搜索树就完成了!!!

🤞🤞🤞

✅4 应用一下

刚才我们建立的是最简单的二叉搜索树,接下来我们可以将他应用到实践中:

  1. 😁K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
    • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
    • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
  2. 😍KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
    • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
    • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对

我们现在做一个水果统计的功能,我们就需要设置合适的 K V映射 (key-val)

template<class K , class V>
struct BSTreeNode
{//构造函数BSTreeNode(K key = 0 , V val = 0): _key(key),_value(val),_right(nullptr),_left(nullptr){}//左右指针BSTreeNode* _right;BSTreeNode* _left;//键值K _key;V _value;
};

其他代码也对应修改!!!

我们来测试一下:

void BSTreeTest3()
{BSTree<string , int> countTree;string arr[] = { "苹果", "西瓜", "苹果", "西瓜","草莓", "苹果", "苹果", "草莓","西瓜","苹果", "香蕉", "苹果", "香蕉" ,"草莓","芒果"};for (const auto& str : arr){// 先查找水果在不在搜索树中// 1、不在,说明水果第一次出现,则插入<水果, 1>// 2、在,则查找到的节点中水果对应的次数++//BSTreeNode<string, int>* ret = countTree.Find(str);auto ret = countTree.Find(str);if (ret == NULL){countTree.Insert(str , 1);}else{ret->_value++;}}countTree.InOrder();
}

🎆🎆🎆来看效果🎆🎆🎆
在这里插入图片描述
🎆🎆🎆成功统计🎆🎆🎆

二叉搜索树还有许多应用场景,大家可以自行探索使用!!!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

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

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

相关文章

数据资源入表难在哪?今晚带你一一弄懂(文末有福利)

​本周&#xff0c;我们即将开启数据要素系列直播《星光对话》的第四期&#xff0c;将由讲师-星光数智首席数据架构师 魏战松&#xff0c;于今晚19:00带来《数据资源入表和运营路径》的主题分享。 精彩内容提前知&#xff1a; 1、入表流程及各阶段参与方 2、入表难点和注意事项…

Android中使用Palette让你的页面UI优雅起来

文章目录 1. 什么是Palette2. 引入Palette3. 使用 Palette3.1 同步方式3.2 异步方式3.3 获取色调值 4. 举例4.1 布局文件 activity_palette_list.xml ⬇️4.2 Activity&#xff1a;PaletteListActivity⬇️4.3 列表Adapter&#xff1a;PaletteListAdapter ⬇️4.4 列表item布局…

「Python绘图」绘制同心圆

python 绘制同心圆 一、预期结果 二、核心代码 import turtle print("开始绘制同心圆") # 创建Turtle对象 pen turtle.Turtle() pen.shape("turtle") # 移动画笔到居中位置 pen.pensize(2) #设置外花边的大小 # 设置填充颜色 pen.fillcolor("green&…

java 并发线程应用

java 并发线程相关 线程状态 新建(NEW): 创建后尚未启动。可运行(RUNABLE): 正在 Java 虚拟机中运行。但是在操作系统层面,它可能处于运行状态,也可能等待资源调度(例如处理器资源),资源调度完成就进入运行状态。所以该状态的可运行是指可以被运行,具体有没有运行要看底层…

【C++算法】堆相关经典算法题

1.最后一块石头的重量 其实就是一个模拟的过程&#xff1a;每次从石堆中拿出最大的元素以及次大的元素&#xff0c;然后将它们粉碎&#xff1b;如果还有剩余&#xff0c;就将剩余的石头继续放在原始的石堆里面重复上面的操作&#xff0c;直到石堆里面只剩下一个元素&#xff0c…

[C/C++] -- 装饰器模式

装饰器模式是一种结构型设计模式&#xff0c;它允许在不改变原始对象的基础上动态地扩展其功能。这种模式通过将对象包装在装饰器类的对象中来实现&#xff0c;每个装饰器对象都包含一个原始对象&#xff0c;并可以在调用原始对象的方法之前或之后执行一些额外的操作。 装饰器…

自学C语言能达到什么境界呢?

C 语言是一门广泛应用于系统软件、嵌入式系统、游戏开发等领域的编程语言。那么&#xff0c;通过自学 C 语言&#xff0c;能够达到什么样的境界呢&#xff1f; 就像学习小提琴一样&#xff0c;仅凭自学也可以达到一定的水平&#xff0c;能够自娱自乐&#xff0c;在亲友聚会时表…

Xed编辑器开发第一期:使用Rust从0到1写一个文本编辑器

这是一个使用Rust实现的轻量化文本编辑器。学过Rust的都知道&#xff0c;Rust 从入门到实践中间还隔着好几个Go语言的难度&#xff0c;因此&#xff0c;如果你也正在学习Rust,那么恭喜你&#xff0c;这个项目被你捡到了。本项目内容较多&#xff0c;大概会分三期左右陆续发布&a…

NAS导航面板Sun-Panel

什么是 Sun-Panel &#xff1f; Sun-Panel 是一个服务器、NAS 导航面板、Homepage、浏览器首页。 软件主要特点&#xff1a; &#x1f349; 界面简洁&#xff0c;功能强大&#xff0c;资源消耗低&#x1f34a; 简单易用&#xff0c;可视化操作&#xff0c;零代码使用&#x1f…

python怎么安装matplotlib

1、登陆官方网址“https://pypi.org/project/matplotlib/#description”&#xff0c;下载安装包。 2、选择合适的安装包&#xff0c;下载下来。 3、将安装包放置到python交互命令窗口的当前目录下。 4、打开windows的命令行窗口&#xff0c;通过"pip install"这个命令…

新质生产力之工业互联网产业链

随着全球经济的数字化转型&#xff0c;新基建的概念逐渐成为推动工业发展的关键动力。在这一转型过程中&#xff0c;工业互联网作为新基建的核心组成部分&#xff0c;正逐渐塑造着未来工业的面貌。那么工业互联网产业链是如何构成的&#xff0c;以及它如何成为推动工业4.0和智能…

CRMEB开源打通版/标准版v4电商商城系统小程序发布之后无法生成海报问题

小程序产品分销二维码生成不了 开发者工具可以生成海报&#xff0c;但是发布之后无法生成 1.在开发者工具中&#xff0c;将不校验合法域名关闭 2.点击生成海报&#xff0c;查看console 3.将域名填写到微信公众平台小程序的download合法域名中 网址微信公众平台

react18【系列实用教程】memo —— 缓存组件 (2024最新版)

memo 的语法 如上图所示&#xff0c;在react中&#xff0c;当父组件重新渲染时&#xff0c;子组件也会重新渲染&#xff0c;即便子组件无任何变化&#xff0c;通过 memo 可以实现对组件的缓存&#xff0c;即当子组件无变化时&#xff0c;不再重新渲染子组件&#xff0c;核心代码…

【深度学习】Diffusion扩散模型的逆扩散问题

1、前言 上一篇&#xff0c;我们讲了Diffusion这个模型的原理推导。但在推导中&#xff0c;仍然遗留了一些问题。本文将解决那些问题 参考论文&#xff1a; ①Variational Diffusion Models (arxiv.org) ②Tutorial on Diffusion Models for Imaging and Vision (arxiv.org…

迭代的难题:敏捷团队每次都有未完成的工作,如何破解?

各位是否遇到过类似的情况&#xff1a;每次迭代结束后&#xff0c;团队都有未完成的任务&#xff0c;很少有完成迭代全部的工作&#xff0c;相反&#xff0c;总是将上期未完成的任务重新挪到本期计划会中&#xff0c;重新规划。敏捷的核心之一是“快速迭代&#xff0c;及时反馈…

ubuntu20.04 ROS 环境下使用速腾80线激光雷达

1.相关系统环境 系统版本:ubuntu 20.04 ROS版本&#xff1a;ROS1 - noetic 激光雷达型号&#xff1a;RoboSense Ruby &#xff08;更新于2024.5.14&#xff09; 2.网口配置&#xff1a; 将PC/工控机的网口配置为&#xff1a; ipv4&#xff0c;方式设置为手动 ip地址、掩码以…

基于springboot实现社区智慧养老监护管理平台系统项目【项目源码+论文说明】计算机毕业设计

基于SpringBoot实现社区智慧养老监护管理平台系统演示 摘要 如今社会上各行各业&#xff0c;都在用属于自己专用的软件来进行工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。互联网的发展&#xff0c;离不开一些新的技术&#xff0c;而新技术的…

EE-SX670 槽型光电开关 5MM 限位检测感应器 使用案例

EE-SX670是一款槽型光电开关&#xff0c;也被称为U形传感器或限位检测感应器。它是光电传感器中的一种&#xff0c;通过检测物体是否插入其感应槽来触发开关。这种传感器通常用于自动化生产线上的位置检测、对象计数以及安全设备中的运动检测。 EE-SX670作为一款高性能的光电传…

谷歌外贸seo优化怎么做?

一般有两种选择&#xff0c;在大型电商平台开展业务&#xff0c;如亚马逊&#xff0c;阿里巴巴等平台&#xff0c;也可以选择搭建自己的独立站 选择在大型电商平台可以方便迅速建立起自己的商铺&#xff0c;不需要考虑太多交易&#xff0c;支付&#xff0c;物流等方面的问题&am…