C++进阶:二叉搜索树介绍、模拟实现(递归迭代两版本)及其应用

上次介绍完多态后:C++进阶:详解多态(多态、虚函数、抽象类以及虚函数原理详解)

也是要开始继续学习了


文章目录

  • 1.二叉搜索树
    • 1.1概念
    • 1.2二叉搜索树特性
    • 1.3 二叉搜索树的操作
  • 2.模拟实现
    • 2.1项目文件规划
    • 2.2基本结构
    • 2.3各种接口、功能、以及基本结构的补充
      • 2.3.1 Find() 查找 (迭代/循环版本)
      • 2.3.2 Insert() 插入(迭代/循环版本)
        • 写出中序遍历来进行验证(中序遍历有序)
      • 2.3.3 Erase() 删除(迭代/循环版本)
        • 验证
      • 2.3.4FindR() 查找 (递归版本)
      • 2.3.5Insert() 插入 (递归版本)
      • 2.3.6 EraseR() 删除 (迭代版本)
      • 2.3.7 补全拷贝构造函数
      • 2.3.8 补全析构函数
  • 3.二叉搜索树应用
  • 4.性能分析


1.二叉搜索树

1.1概念

二叉搜索树(Binary Search Tree,BST)是一种二叉树,其中每个节点都具有以下性质:

  • 节点的左子树中的所有节点的值都小于该节点的值。
  • 节点的右子树中的所有节点的值都大于该节点的值。
  • 左右子树也分别为二叉搜索树。

假设我们插入以下元素:5, 3, 7, 1, 4, 6, 8,可以构建如下的二叉搜索树(BST):

      5/ \3   7/ \ / \1  4 6  8

1.2二叉搜索树特性

  • 中序遍历二叉搜索树得到的序列是有序的。
  • 查找、插入和删除操作的平均时间复杂度为O(log N),其中N为树中节点的数量

1.3 二叉搜索树的操作

  1. 插入操作

    • 树为空,则直接新增节点,赋值给root指针

    • 树不空,按二叉搜索树性质查找插入位置,插入新节点

  2. 删除操作

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

    • 如果要删除的节点没有孩子结点,那么可以直接删除它。

    • 如果要删除的节点只有左孩子结点,可以直接删除该节点,并使其父节点指向其左孩子。

    • 如果要删除的节点只有右孩子结点,同样可以直接删除该节点,并使其父节点指向其右孩子。

    • 如果要删除的节点有左、右孩子结点,可以在其右子树中找到中序遍历下的第一个结点(右子树里最小),将其值替换到要删除的节点中,再递归删除右子树中的那个中序遍历下的第一个结点。(这个可以直接删)


2.模拟实现

2.1项目文件规划

在这里插入图片描述

头文件BSTree.h:进行模拟的编写

源文件test.cpp:进行测试,检查代码逻辑是否满足期望

2.2基本结构

namespace key
{template<class K>struct BSTreeNode{BSTreeNode* _left;//左指针BSTreeNode* _right;//右指针K _key;//存数据的BSTreeNode(const K& key)//构造函数:_left(nullptr), _right(nullptr), _key(key){}};template<class K>class BSTree{typedef BSTreeNode<K> Node;public:BSTree() = default;//强制生成默认构造函数BSTree(const BSTree<K>& t)//拷贝构造函数{//....}~BSTree()//析构函数{//...}private:Node* _root = nullptr;//头结点};
}

2.3各种接口、功能、以及基本结构的补充

2.3.1 Find() 查找 (迭代/循环版本)

		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;//没找到才会出循环}

这里的思路很简单:该key< cur的_key,就进入到左子树;反之进入右子树

2.3.2 Insert() 插入(迭代/循环版本)

bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);return true;}else{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{return false;//搜索二叉树里不能有相同 }}//出来后cur是nullptr,parent是叶子节点cur = new Node(key);//重新利用if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;}}
  1. 如果二叉搜索树为空(即 _root == nullptr),则创建一个新节点 _root,其键值为 key,并将其作为根节点。
  2. 如果二叉搜索树不为空,则从根节点开始,沿着树的路径向下搜索应该插入新节点的位置。
  3. 在搜索过程中,如果发现要插入的键值 key 小于当前节点的键值,则继续在当前节点的左子树中搜索;如果大于当前节点的键值,则继续在右子树中搜索。
  4. 如果搜索到某个节点的键值与要插入的键值相等,则说明二叉搜索树中不能有相同的节点,直接返回 false
  5. 当搜索到空节点时,表示找到了要插入新节点的位置,此时创建一个新节点 cur,其键值为 key
  6. 最后,将新节点 cur 插入到父节点 parent 的左子树或右子树上,具体取决于新节点的键值与父节点的键值的大小关系
写出中序遍历来进行验证(中序遍历有序)
void InOrder(){_InOrder(_root);cout << endl;}private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}

这里因为要传入根节点,所以正常无法在外部调用。我们采用子函数的方式来解决这个问题。

顺便把子函数放在private里,这样更安全

#include<iostream>
using namespace std;#include"BSTree.h"int main()
{key::BSTree<int> t;int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };for (auto e : a){t.Insert(e);}t.InOrder();
}

在这里插入图片描述


2.3.3 Erase() 删除(迭代/循环版本)

		bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;while (cur)//先找到在哪里删除{if (key < cur->_key){parent = cur;cur = cur->_left;}else if (key > cur->_key){parent = cur;cur = cur->_right;}else//进来就说明找到啦,就是现在的cur{if (cur->_left == nullptr)//左为空,就把右给父亲节点(为空也无妨){if (cur == _root){_root = cur->_right;}else{if (cur == parent->_right)//cur在parent右,那就接到右侧{parent->_right = cur->_right;}else{parent->_left = cur->_right;}}delete cur;return true;}else if (cur->_right == nullptr){if (cur == _root){_root = cur->_left;}else{if (cur == parent->_right)//cur在parent右,那就接到右侧{parent->_right = cur->_left;}else{parent->_left = cur->_left;}}delete cur;return true;}else//左右都不是空,使用替换法,这里用右子树最小来换{Node* rightMinParent = cur;//右子树最小的父亲节点Node* rightMin = cur->_right;//右子树最小的节点while (rightMin->_left)//开始找{rightMinParent = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;//把值一给,现在删rightMin就行了if (rightMin == rightMinParent->_left)rightMinParent->_left = rightMin->_right;elserightMinParent->_right = rightMin->_right;delete rightMin;return true;}}}}
  1. 首先,定义了两个指针 parentcur,分别用来记录当前节点的父节点和当前节点,初始时 cur 指向根节点 _root
  2. while 循环中,不断遍历二叉搜索树,直到找到要删除的节点或遍历完整棵树
  3. 在循环中,通过比较要删除的键值 key 与当前节点的键值 cur->_key 的大小关系,来确定是向左子树还是右子树继续遍历。
  4. 如果找到了要删除的节点,根据不同情况进行处理:
  • 如果要删除的节点没有左子树,直接将其右子树接到父节点的相应位置,然后删除该节点。
  • 如果要删除的节点没有右子树,类似地将其左子树接到父节点的相应位置,然后删除该节点。
  • 如果要删除的节点既有左子树又有右子树,采用替换法,找到右子树中最小的节点(即右子树中最左边的节点),将其键值替换到要删除的节点上,然后删除右子树中最小的节点。
  1. 删除节点后,返回 true 表示删除成功
验证
int main()
{key::BSTree<int> t;int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };for (auto e : a){t.Insert(e);}t.InOrder();t.Erase(8);t.InOrder();t.Erase(14);t.InOrder();t.Erase(4);t.InOrder();t.Erase(6);t.InOrder();return 0;
}

在这里插入图片描述


2.3.4FindR() 查找 (递归版本)

		bool FindR(const K& key){return _FindR(_root, key);}private:bool _FindR(Node* root, const K& 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;}}
  1. FindR 函数是对外提供的接口函数,用于从根节点开始递归查找指定键值的节点。
  2. _FindR 函数中,首先判断当前节点是否为空,如果为空则表示在当前路径上没有找到指定键值的节点,返回 false
  3. 如果当前节点的键值小于要查找的键值 key,则递归调用 _FindR 函数查找右子树。
  4. 如果当前节点的键值大于要查找的键值 key,则递归调用 _FindR 函数查找左子树。
  5. 如果当前节点的键值等于要查找的键值 key,则表示找到了目标节点,返回 true
  6. 通过递归的方式不断在左右子树中查找,直到找到目标节点或者遍历完整棵树

2.3.5Insert() 插入 (递归版本)

bool InsertR(const K& key)//难点在于,如何与父亲节点进行链接{return _InsertR(_root, key);}private:bool _InsertR(Node*& root,const K& 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;}}
  1. InsertR 函数是对外提供的接口函数,用于从根节点开始递归插入新节点。
  2. _InsertR 函数中,参数 root 是一个指向节点指针的引用,这样可以在递归过程中改变节点指针的指向,从而实现与父节点的链接
  3. 首先判断当前节点是否为空,如果为空则表示可以在该位置插入新节点,创建一个新节点并将其指针赋给 root,然后返回 true
  4. 如果当前节点的键值小于要插入的键值 key,则递归调用 _InsertR 函数插入到右子树。
  5. 如果当前节点的键值大于要插入的键值 key,则递归调用 _InsertR 函数插入到左子树。
  6. 如果当前节点的键值等于要插入的键值 key,则表示树中已经存在相同键值的节点,返回 false
  7. 通过递归的方式在左右子树中寻找合适的插入位置,并不断更新父节点的指针,直到插入新节点或者确认新节点已存在

2.3.6 EraseR() 删除 (迭代版本)

		bool EraseR(const K& key){return _EraseR(_root, key);}private:bool _EraseR(Node*& root, const K& 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//找到啦{Node* del = root;//保存一下,过会要delete的//这里便是引用开始发挥的作用,可以直接进行链接if (root->_right == nullptr){root = root->_left;}else if (root->_left == nullptr){root = root->_right;}else//还是使用替换法,不过这次,直接交换后,再去右子树里递归{Node* rightMin = root->_right;while (rightMin->_left){rightMin = rightMin->_left;}std::swap(root->_key, rightMin->_key);return _EraseR(root->_right, key);}delete del;return true;}}
  1. EraseR 函数是对外提供的接口函数,用于从根节点开始递归删除指定键值的节点。
  2. _EraseR 函数中,参数 root 是一个指向节点指针的引用,这样可以在递归过程中改变节点指针的指向,从而实现删除操作
  3. 首先判断当前节点是否为空,如果为空则表示在当前路径上没有找到指定键值的节点,返回 false
  4. 如果当前节点的键值小于要删除的键值 key,则递归调用 _EraseR 函数在右子树中删除。
  5. 如果当前节点的键值大于要删除的键值 key,则递归调用 _EraseR 函数在左子树中删除。
  6. 如果当前节点的键值等于要删除的键值 key,则表示找到了目标节点,进行删除操作。
  7. 如果目标节点只有一个子节点或者没有子节点,直接用子节点替换目标节点即可。
  8. 如果目标节点有两个子节点,找到右子树中最小的节点(即右子树中最左边的节点),将其键值与目标节点键值交换,然后在右子树中递归删除这个最小节点。
  9. 最后删除目标节点,并返回 true 表示删除成功

2.3.7 补全拷贝构造函数

		BSTree(const BSTree<K>& t)//拷贝构造函数{_root = copy(_root);}Node* copy(Node* root){if (root == nullptr)return nullptr;Node* newnode = new Node(root->_key);if (root->_left){newnode->_left = copy(root->_left);}if (root->_right){newnode->_right = copy(root->_right);}return newnode;}
  1. 在拷贝构造函数 BSTree(const BSTree<K>& t) 中,调用了 copy 函数来复制传入二叉搜索树 t 的根节点及其所有子节点。
  2. copy 函数接收一个节点指针 root,用于递归复制以该节点为根的子树。
  3. 首先判断当前节点是否为空,如果为空则返回 nullptr
  4. 创建一个新节点 newnode,键值为当前节点 root 的键值,表示复制当前节点。
  5. 如果当前节点有左子节点,则递归复制左子树,并将复制得到的左子树根节点赋值给新节点的左指针 _left
  6. 如果当前节点有右子节点,则递归复制右子树,并将复制得到的右子树根节点赋值给新节点的右指针 _right
  7. 返回新节点 newnode,表示复制当前节点及其所有子节点。(也是起到链接的作用)
  8. 在拷贝构造函数中,调用 copy 函数复制传入二叉搜索树的根节点,从而完成整棵树的复制。

2.3.8 补全析构函数

		~BSTree()//析构函数{Destory(_root);}void Destory(Node* root)//走个后序{if (root == nullptr)return;Destory(root->_left);Destory(root->_right);delete root;}

3.二叉搜索树应用

  1. K模型

    • 结构:在K模型中,每个节点只包含一个关键码(key),不包含值。节点的左子树中的所有节点都小于当前节点的关键码,右子树中的所有节点都大于当前节点的关键码。
    • 操作
      • 插入:将新的关键码插入到二叉搜索树中的合适位置,保持树的有序性。
      • 搜索:通过比较关键码,可以快速判断给定的值是否在树中存在。
    • 应用场景:适用于需要快速判断特定值是否存在的场景,如拼写检查、查找特定单词等。
  2. KV模型

    • 结构:在KV模型中,每个节点包含一个键值对(<Key, Value>),其中Key为关键码,Value为对应的值。节点的左子树中的所有节点的关键码小于当前节点的关键码,右子树中的所有节点的关键码大于当前节点的关键码。
    • 操作
      • 插入:插入新的键值对到二叉搜索树中,保持树的有序性。
      • 搜索:通过比较关键码,可以快速查找对应的值。
    • 应用场景
      • 英汉词典:以英文单词为关键码,对应的中文翻译为值,可以通过英文单词快速查找对应的中文翻译。
      • 统计单词次数:以单词为关键码,出现次数为值,可以方便地查找给定单词的出现次数。

4.性能分析

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

在这里插入图片描述

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

为了改进这种情况,可以使用自平衡二叉搜索树,如AVL树和红黑树。这些树在插入和删除操作时会通过旋转和重新平衡操作来保持树的平衡,从而确保树的高度较低,提高了搜索效率。

我后面也会继续学习,分享给大家!!!

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

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

相关文章

kafka集群介绍及搭建

介绍 kafka是一个高性能、低延迟、分布式的消息传递系统&#xff0c;特点在于实时处理数据。集群由多个成员节点broker组成&#xff0c;每个节点都可以独立处理消息传递和存储任务。 路由策略 发布消息由key、value组成&#xff0c;真正的消息是value&#xff0c;key是标识路…

【prometheus-operator】k8s监控集群外redis

1、部署exporter GitHub - oliver006/redis_exporter: Prometheus Exporter for Redis Metrics. Supports Redis 2.x, 3.x, 4.x, 5.x, 6.x, and 7.x redis_exporter-v1.57.0.linux-386.tar.gz # 解压 tar -zxvf redis_exporter-v1.57.0.linux-386.tar.gz # 启动 nohup ./redi…

Java基础 学习笔记九

for循环 for循环语句的语法结构 for(初始化表达式;条件表达式;更新表达式){循环体;}初始化表达式最先被执行&#xff0c;而且只执行一次条件表达式的执行结果必须是一个布尔类型的值更新表达式一般是负责更新某个变量值的&#xff08;只有更新了某个变量值&#xff0c;条件表达…

Visual Studio 2013 - 重置窗口布局

Visual Studio 2013 - 重置窗口布局 1. Microsoft Visual Studio 2013 - 重置窗口布局References 1. Microsoft Visual Studio 2013 - 重置窗口布局 窗口 -> 重置窗口布局 References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/

API接口采集淘宝商品详情数据获取属性价格详情图等

什么是电商APIAPI全称应用程序编程接口&#xff08;Application Programming Interface&#xff09;&#xff0c;是一组用于访问某个软件或硬件的协议、规则和工具集合。电商API就是各大电商平台提供给开发者访问平台数据的接口。目前&#xff0c;主流电商平台如淘宝、天猫、京…

软件工程导论画图题汇总:期末+复试

文章目录 一、数据模型&#xff1a;实体联系图&#xff08;E-R图&#xff09;二、行为模型&#xff1a;状态转换图三、功能模型&#xff1a;数据流图四、数据字典五、系统流程图六、层次图七、HIPO图八、结构图九、程序流程图十、盒图十一、PAD图十二、判定表、判定树 一、数据…

EF数据持久化(三层架构,客户增删)

效果图 点击新增按钮 点击添加 添加成功展示新增数据 点击删除&#xff0c;出现删除选项&#xff0c;点击确定根据id删除成功 成功删除 实现过程 Model设置具体流程在下面链接中 https://blog.csdn.net/Mr_wangzu/article/details/136805824?spm1001.2014.3001.5501 DAL …

函数栈帧的创建和销毁 - 局部变量|函数传参|函数调用|函数返回|图文详解

目录 1.寄存器EBP和ESP 2.函数栈帧的创建 3.函数的调用 4. 函数栈帧的销毁 函数栈帧&#xff08;function stack frame&#xff09;是在函数调用期间在栈上分配的内存区域&#xff0c;用于存储函数的局部变量、参数、以及用于函数调用和返回的相关信息。每当函数被调用时&a…

ros小问题之差速轮式机器人轮子不显示(rviz gazebo)

在rviz及gazebo练习差速轮式机器人时&#xff0c;很奇怪&#xff0c;只有个机器人的底板及底部的两个万向轮&#xff0c;如下图&#xff0c; 后来查看相关.xacro文件&#xff0c;里面是引用包含了轮子的xacro文件&#xff0c;只需传入不同的参数即可调用生成不同位置的轮子&…

1058:求一元二次方程

【题目描述】 利用公式 求一元二次方程axbxc0的根&#xff0c;其中a不等于0。结果要求精确到小数点后5位。 【输入】 输入一行&#xff0c;包含三个浮点数a,b,c&#xff08;它们之间以一个空格分开&#xff09;&#xff0c;分别表示方程axbxc0的系数。 【输出】 输出一行&…

航顺车规级SoC全新亮相,助推汽车智能化发展

受益于汽车电动化、智能化和网联化的推进&#xff0c;汽车车身域和座舱域MCU市场规模持续扩大。据统计&#xff0c;2021年中国车载芯片MCU市场规模达30.01亿美元&#xff0c;同比增长13.59%&#xff0c;预计2025年市场规模将达42.74亿美元。 在技术要求方面&#xff0c;对…

MyBatisPlus 之四:MP 的乐观锁和逻辑删除、分组、排序、链式的实现步骤

乐观锁 乐观锁是相对悲观锁而言的&#xff0c;乐观锁假设数据一般情况不会造成冲突&#xff0c;所以在数据进行提交更新的时候&#xff0c;才会正式对数据的冲突与否进行检测&#xff0c;如果冲突&#xff0c;则返回给用户异常信息&#xff0c;让用户决定如何去做。 乐观锁适用…

[Qt学习笔记]QT下获取Halcon图形窗口鼠标事件并执行相应操作

目录 1、背景2、参考信息3、目标4、步骤4.1 Halcon库的配置4.2 读取图像&#xff0c;并实现图像自适应窗体控件大小4.3 主要的图形绘制和贴图操作见如下代码&#xff0c;其中重点为全局函数的创建来实现选择Select、拖拽Drag和尺寸Resize事件响应。 5、总结 1、背景 在视觉项目…

3.19作业

1、思维导图 2、模拟面试题 1&#xff09;TCP通信中的三次握手和四次挥手 答&#xff1a;三次握手 客户端向服务器发送连接请求 服务器向客户端回复应答并向客户端发送连接请求 客户端回复服务端&#xff0c;并建立联系 四次挥手 进程a向进程b发送断开连接请求…

3.20作业

1、思维导图 2、 1> 创建一个工人信息库&#xff0c;包含工号&#xff08;主键&#xff09;、姓名、年龄、薪资。 2> 添加三条工人信息&#xff08;可以完整信息&#xff0c;也可以非完整信息&#xff09; 3> 修改某一个工人的薪资&#xff08;确定的一个&am…

踏“时间”与“空间”前来探寻复杂度的奥妙(Java篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

校园综合能效平台建设的意义

彭姝麟 Acrelpsl 一 高校用能分析 当前高校用能普遍存在以下点问题&#xff1a; 一是用能需求日益增加&#xff1a;随着高校的快速发展&#xff0c;校园用能人数、用能设备、建筑面积等逐年增加&#xff0c;用能需求也相应攀升。日益增长的能耗需求与节能降耗任务之间的客观矛…

一文读懂什么是序列 (sequence)

sequence 序列 sequence(序列)是一组有顺序的元素的集合 (严格的说&#xff0c;是对象的集合&#xff0c;但鉴于我们还没有引入“对象”概念&#xff0c;暂时说元素) 序列可以包含一个或多个元素&#xff0c;也可以没有任何元素。 我们之前所说的基本数据类型&#xff0c;都…

蓝桥杯练习03个人博客

个人博客 介绍 很多人都有自己的博客&#xff0c;在博客上面用自己的方式去书写文章&#xff0c;用来记录生活&#xff0c;分享技术等。下面是蓝桥云课的博客&#xff0c;但是上面还缺少一些样式&#xff0c;需要大家去完善。 准备 开始答题前&#xff0c;需要先打开本题的…

物业社区人行通道闸如何选择,这6点一定要考虑!

社区是居民的共同家园&#xff0c;一个安全、便捷且和谐的社区环境对于提升居民的生活质量至关重要。人行通道闸不仅仅是一道简单的进出关卡&#xff0c;它是守护社区人员通行安全的坚实屏障&#xff0c;是提升社区管理效率的智能工具&#xff0c;更是增强业主满意度的关键因素…