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

【数据结构进阶】二叉搜索树

在这里插入图片描述

🔥个人主页: Forcible Bug Maker
🔥专栏: C++ || 数据结构

目录

  • 🌈前言
  • 🔥二叉搜索树
  • 🔥 二叉搜索树的实现
    • ==Insert(插入)==
    • ==find(查找)==
    • ==erase(删除)==
    • ==destroy(析构)==
    • ==InOrder(中序遍历)==
    • ==拷贝构造==
  • 🔥二叉搜索树的应用
  • 🔥二叉搜索树的性能
  • 🌈结语

🌈前言

本篇博客主要内容:二叉搜索树的介绍及自实现

基础的二叉树在前面的C数据结构阶段已经讲过(初阶数据结构之—二叉树链式结构)。之前因为用C语言的话,实现更高级数据结构比较困难,所以并没有往后展开。到了现在,已经有了一定的C++功底,就可以开启我们数据结构进阶部分的内容了。对于二叉搜索树的特性了解,有助于后续更好的理解map和set的特性。本节课作为学习更高阶数据结构的基础,对后续学习来说至关重要。

🔥二叉搜索树

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

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

二叉搜索树的中序遍历是有序的。

在这里插入图片描述

🔥 二叉搜索树的实现

在这里插入图片描述
以下是需要实现的二叉搜索树的头文件内容

#pragma once
#include<iostream>namespace ForcibleBugMaker
{template<class K>struct BSTreeNode{BSTreeNode<K>(const K& k = K()):_key(k), _left(nullptr), _right(nullptr){}K _key;BSTreeNode<K>* _left;BSTreeNode<K>* _right;};template<class K>class BSTree{typedef BSTreeNode<K> Node;public:BSTree() = default;BSTree(const BSTree<K>& t);bool Insert(const K& key);Node* Find(const K& key);bool Erase(const K& key);~BSTree();void InOrder();private:Node* _root = nullptr;};
}

二叉搜索树的结点中有三个成员变量,分别是
_key:存储数据;_left:指向左子树;_right:指向右子树。将其在BSTree中typedef成Node方便后续使用。

Insert(插入)

二叉树的插入,主要考虑两种情况:

  1. 树为空,则直接新增结点,赋给root指针。
  2. 树不为空,按二叉搜索树性质查找插入位置,插入新结点,如key结点值存在,则插入失败。
    在这里插入图片描述
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;}if (parent->_key < key) parent->_right = new Node(key);else parent->_left = new Node(key);return true;
}

find(查找)

二叉搜索树的查找:

  1. 从根开始比较,比跟大则往右边走查找,比根小则往左边走查找。
  2. 最多查找高度次,走到空,还没找到,这个值不存在。
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;
}

erase(删除)

删除的逻辑相比其他的实现来说复杂很多,二叉搜索树的删除:
首先查找元素是否在二叉搜索树中,如果不存在,则返回;否则要删除结点可能分以下三种情况:

  1. 被查找到的结点无孩子(直接删除)
  2. 被查找到的结点有一个孩子(删除结点,将孩子交给父亲)
  3. 被查找到的结点有两个孩子(在其右孩子中找最左边的孩子(如果此孩子不存在,则为该结点右孩子),用它的值填补到被删除结点中,再来处理增补结点的删除。)相当于找一个合适的子节点替代

在这里插入图片描述

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 {if (cur == _root && cur->_left == nullptr) {_root = cur->_right;delete cur;return true;}else if (cur == _root && cur->_right == nullptr) {_root = cur->_left;delete cur;return true;}if (cur->_left == nullptr) {if (parent->_right == cur)parent->_right = cur->_right;elseparent->_left = cur->_right;delete cur;}else if (cur->_right == nullptr) {if (parent->_right == cur)parent->_right = cur->_left;elseparent->_left = cur->_left;delete cur;}else {Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left) {rightMinP = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;cur->_value = rightMin->_value;if (rightMinP == cur)rightMinP->_right = rightMin->_right;elserightMinP->_left = rightMin->_right;delete rightMin;}return true;}}return false;
}

destroy(析构)

二叉树的析构需要传入根结点,通过后序遍历递归实现,但是从外界无法访问对象内部的私有成员_root。所以咱们可以实现一个工具函数,用来帮助完成二叉搜索树的析构。

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

InOrder(中序遍历)

逻辑跟析构一样。中序遍历下来的key是有序的。

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

拷贝构造

本质上就是实现一次二叉树的深拷贝,也是嵌套了一个递归。

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

🔥二叉搜索树的应用

像我们刚刚实现的,只存一个数据,是典型的K模型;如果存两个数据,那就是KV模型。

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

在以上实现K模型的基础上,实现KV模型无非就是让结点多存储一个元素,给模板增添一个类型,具体实现代码如下:

#pragma once
#include<iostream>namespace ForcibleBugMaker
{template<class K, class V>struct BSTreeNode{BSTreeNode<K, V>(const K& k = K(), const V& v = V()):_key(k), _value(v), _left(nullptr), _right(nullptr){}K _key;V _value;BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;};template<class K, class V>class BSTree{typedef BSTreeNode<K, V> Node;public:BSTree() = default;BSTree(const BSTree<K, V>& t){_root = _Copy(t._root);}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;}if (parent->_key < key) parent->_right = new Node(key, value);else parent->_left = new Node(key, value);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 {if (cur == _root && cur->_left == nullptr) {_root = cur->_right;delete cur;return true;}else if (cur == _root && cur->_right == nullptr) {_root = cur->_left;delete cur;return true;}if (cur->_left == nullptr) {if (parent->_right == cur)parent->_right = cur->_right;elseparent->_left = cur->_right;delete cur;}else if (cur->_right == nullptr) {if (parent->_right == cur)parent->_right = cur->_left;elseparent->_left = cur->_left;delete cur;}else {Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left) {rightMinP = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;cur->_value = rightMin->_value;if (rightMinP == cur)rightMinP->_right = rightMin->_right;elserightMinP->_left = rightMin->_right;delete rightMin;}return true;}}return false;}~BSTree(){Destroy(_root);_root = nullptr;}void InOrder(){_InOrder(_root);std::cout << std::endl;}private: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;}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);std::cout << root->_key << ":" << root->_value << " ";_InOrder(root->_right);}Node* _root = nullptr;};
}

🔥二叉搜索树的性能

二叉搜索树(Binary Search Tree, BST)的性能主要取决于其结构。理想情况下,二叉搜索树是一个平衡树,其中每个节点的左子树只包含小于节点值的元素,右子树只包含大于节点值的元素,且左、右子树的高度大致相等。然而,在实际应用中,由于插入和删除操作的随机性,二叉搜索树可能会退化为链表状结构(即所有节点都偏向一侧),这会导致其性能急剧下降
在这里插入图片描述
时间复杂度:

  • 搜索(Search):在平衡的二叉搜索树中,搜索操作的时间复杂度为O(log n),其中n是树中节点的数量。这是因为每次递归或迭代都排除了一半的搜索空间。但在最坏的情况下(树退化为链表),时间复杂度会退化为O(n)
  • 插入(Insert)和删除(Delete):同样,在平衡的二叉搜索树中,插入和删除操作的时间复杂度也是O(log n)。但在最坏的情况下,时间复杂度会退化为O(n)

空间复杂度:

  • 二叉搜索树的空间复杂度主要由树中存储的节点数量决定,为O(n)

优化:
为了保持二叉搜索树的平衡,避免性能退化,可以使用各种自平衡二叉搜索树的数据结构,如:

  • AVL树:任何节点的两个子树高度最大差的绝对值小于二,通过旋转操作来维持平衡。
  • 红黑树:一种自平衡二叉查找树,通过特定的操作和性质(如节点着色和树的高度限制)来保持树的平衡。
  • B树和B+树:这些树不仅用于保持数据的排序,还优化了磁盘读写操作,常用于数据库和文件系统中。

这些平衡二叉树也将会是我们未来在数据结构进阶中主要展开的内容。

🌈结语

本篇博客主要讲了二叉搜索树及其实现,K模型和KV模型,最后分析了二叉搜索树的性能,同时介绍了一些维持树平衡的解决方案。感谢大家的支持♥

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • DC-1靶场打靶第一次!!!!冲冲冲!
  • 算法日记day 16(二叉树的广度优先遍历|反转、对称二叉树)
  • Android APP 基于RecyclerView框架工程(知识体系积累)
  • 在虚拟机 CentOS7 环境下安装 MySQL5.7 数据库
  • 深入理解Linux网络(三):TCP对象创建
  • [HTML]一文掌握
  • MySQL中EXPLAIN关键字详解
  • Python入门基础教程(非常详细)
  • C++ | Leetcode C++题解之第264题丑数II
  • 轨道相互作用和带隙
  • 为什么要从C语言开始编程
  • Python 热门面试题(七)
  • 十五、公开课
  • 基于SSM的网上选课系统
  • 【ACM独立出版|EI检索稳定】2024年智能感知与模式识别国际学术会议(ISPC 2024,9月6日-8)
  • 10个最佳ES6特性 ES7与ES8的特性
  • Angular Elements 及其运作原理
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • MobX
  • Netty 框架总结「ChannelHandler 及 EventLoop」
  • Python连接Oracle
  • React组件设计模式(一)
  • Webpack4 学习笔记 - 01:webpack的安装和简单配置
  • 笨办法学C 练习34:动态数组
  • 高程读书笔记 第六章 面向对象程序设计
  • 关于List、List?、ListObject的区别
  • 基于OpenResty的Lua Web框架lor0.0.2预览版发布
  • 深入浅出webpack学习(1)--核心概念
  • 一、python与pycharm的安装
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • #define,static,const,三种常量的区别
  • #Linux(make工具和makefile文件以及makefile语法)
  • #考研#计算机文化知识1(局域网及网络互联)
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (delphi11最新学习资料) Object Pascal 学习笔记---第7章第3节(封装和窗体)
  • (二十九)STL map容器(映射)与STL pair容器(值对)
  • (附源码)计算机毕业设计ssm电影分享网站
  • (佳作)两轮平衡小车(原理图、PCB、程序源码、BOM等)
  • (三) diretfbrc详解
  • (转)memcache、redis缓存
  • ****** 二十三 ******、软设笔记【数据库】-数据操作-常用关系操作、关系运算
  • .bat批处理(十):从路径字符串中截取盘符、文件名、后缀名等信息
  • .Net CF下精确的计时器
  • .Net MVC + EF搭建学生管理系统
  • .NET WPF 抖动动画
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .NetCore项目nginx发布
  • .NET单元测试
  • .NET轻量级ORM组件Dapper葵花宝典
  • @Autowired注解的实现原理
  • @Data注解的作用
  • [ vulhub漏洞复现篇 ] Hadoop-yarn-RPC 未授权访问漏洞复现
  • []我的函数库
  • [20181219]script使用小技巧.txt
  • [AutoSAR 存储] 汽车智能座舱的存储需求