家具网站设计网,主页网址,好看的响应式网站,微信公众号定位开发✅1主页#xff1a;我的代码爱吃辣#x1f4c3;2知识讲解#xff1a;数据结构——二叉搜索树☂️3开发环境 #xff1a;Visual Studio 2022#x1f4ac;4前言#xff1a;在之前的我们已经学过了普通二叉树#xff0c;了解了基本的二叉树… ✅1主页我的代码爱吃辣2知识讲解数据结构——二叉搜索树☂️3开发环境 Visual Studio 20224前言在之前的我们已经学过了普通二叉树了解了基本的二叉树的结构和基本操作了不过我们之前的二叉树结构都是使用C语言实现的我们这次来介绍二叉树中更加复杂的树结构C语言在实现这些结构已经有些吃力了更适合我们使用C来实现。 目录
一.前言
二.二叉搜索树
三. 二叉搜索树操作
1.结点与整体结构
2.insert 3.find
4.erase 5.构造与析构
四.二叉搜索树的应用 五. 二叉搜索树的性能分析
六.整体代码 一.前言
map和set特性需要先铺垫二叉搜索树而二叉搜索树也是一种树形结构二叉搜索树的特性了解有助于更好的理解map和set的特性二叉树中部分面试题稍微有点难度在前面讲解大家不容易接受且时间长容易忘。本节借二叉树搜索树对二叉树部分进行收尾总结。
二.二叉搜索树
二叉搜索树又称二叉排序树它或者是一棵空树或者是具有以下性质的二叉树:
若它的左子树不为空则左子树上所有节点的值都小于根节点的值若它的右子树不为空则右子树上所有节点的值都大于根节点的值它的左右子树也分别为二叉搜索树 三. 二叉搜索树操作
1.结点与整体结构
templateclass K
struct BSTreeNode
{BSTreeNode(K key):_key(key),_right(nullptr),_left(nullptr){}K _key; //数值BSTreeNodeK* _right; //右孩子BSTreeNodeK* _left; //左孩子
};templateclass K
class BSTree
{typedef BSTreeNodeK Node;
public://...
private:Node* _root nullptr;
};
2.insert
我们主要针对两种情况
二叉树中一个数据都没有二叉树已经有数据了
如果二叉树中还没有数据我们需要将插入的第一个数据作为二叉搜索树的根节点。
如果二叉搜索树中已经有了数据我们根据搜索二叉树的特性如果插入的值比根小我们就往根的左子树去插入如果插入的值比根大我们就往根的右子树去插入如果遇到相同的值就算是插入失败了循环上面得动作直到找到一个空位置。 循环版本 bool insert(const K key){//如果BSTree还没有结点if (_root nullptr){_root new Node(key);return true;}//找到插入的合适位置和其父亲结点//父亲结点得作用是我们新插入得结点要和父亲结点连接//简单来说就是父亲结点要孩子指针要指向我们新的结点。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 new Node(key);//判断新插入得结点是父亲得左孩子还是右孩子if (key parent-_key){parent-_right cur;}else{parent-_left cur;}return true;}递归版本 bool Rinsert(const K key){return _Rinsert(_root, key);}bool _Rinsert(Node* root, const K key){//如果BSTree还没有结点、或者已经找到得合适的空位置if (root nullptr){root new Node(key);return true;}//BSTree已经有结点if (key root-_key){//key比当前结点小往左树插入return _Rinsert(root-_left, key);}else if (key root-_key){//key比当前结点大往右树插入return _Rinsert(root-_right, key);}else{return false;}}//中序遍历void InOrder(){_InOrder(_root);}void _InOrder(Node* root){if (root nullptr){return;}_InOrder(root-_left);cout root-_key ;_InOrder(root-_right);}
测试
二叉搜索树中序遍历得结果就是排序结果我们可以通过这个特性判断我们插入得是否正确。
int main()
{int a[] { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTreeint b;BSTreeint Rb;for (auto e : a){b.insert(e);Rb.Rinsert(e);}b.InOrder();cout endl;Rb.InOrder();return 0;
} 3.find
二叉搜索树的查找
从根开始比较查找比根大则往右边走查找比根小则往左边走查找。最多查找高度次走到到空还没找到这个值不存在。
找到以后返回目标结点得指针。 循环版本 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;}
递归版本 Node* Rfind(const K key){return _Rfind(_root, key);}Node* _Rfind(Node* root, const K key){if (root nullptr){return nullptr;}if (key root-_key){return _Rfind(root-_left, key);}else if (key root-_key){return _Rfind(root-_right, key);}else{return root;}}
4.erase
二叉搜索树的删除
首先查找元素是否在二叉搜索树中如果不存在则返回, 否要删除则的结点可能分下面四种情 况
a. 要删除的结点无孩子结点b. 要删除的结点只有左孩子结点c. 要删除的结点只有右孩子结点d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况实际情况a可以与情况b或者c合并起来因此真正的删除过程 如下
情况1要删除的结点只有左孩子结点删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除情况2要删除的结点只有右孩子结点删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除情况3要删除的结点有左、右孩子结点在它的右子树中寻找中序下的第一个结点(数值最小)用它的值填补到被删除节点中再来处理该结点的删除问题--替换法删除 情况二与情况一处理方法相同: 循环版本
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 (cur _root){_root _root-_right;}else{//判断待删除结点是父亲的左孩子还是右孩子if (cur parent-_left){parent-_left cur-_right;}else{parent-_right cur-_right;}}delete cur;return true;}//待删除结点左节点为空将其左边结点交给父亲else if (cur-_right nullptr){//此时如果删除的是根节点需要改变根节点指向if (cur _root){_root _root-_left;}else{//判断待删除结点是父亲的左孩子还是右孩子if (cur parent-_left){parent-_left cur-_left;}else{parent-_right cur-_left;}}delete cur;return true;} else{//由于删除的结点左右都有孩子//需要找一个能代替删除结点的位置结点即比左子树大比右子树小//最适合的结点就是左子树的最右结点(最大节点)右子树的最左节点(最小结点)Node* MinNode cur-_right;Node* MinParent cur;while (MinNode-_left){MinParent MinNode;MinNode MinNode-_left;}//先将MinNode结点的孩子交给他的父亲//注意:不能因为是找的最左边结点就因为MinNode结点一定是MinParent的左孩子if (MinParent-_left MinNode){MinParent-_left MinNode-_right;}else{MinParent-_right MinNode-_right;}//将MinNode结点的值赋值给curcur-_key MinNode-_key;delete MinNode;return true;}}}return false;}
递归版本
bool _Rerase(Node* root, const K key){//空树、没有找到删除的结点if (root nullptr){return false;}if (key root-_key){//key比当前结点小往左树删除return _Rerase(root-_left, key);}else if(key root-_key){//key比当前结点小往左树删除return _Rerase(root-_right, key);}else{//找到开始删除Node* cur root;if (root-_left nullptr){//1.待删除结点左孩子为空root root-_right;}else if (root-_right nullptr){//2.待删除结点右孩子为空root root-_left;}else//待删除结点左右孩子都不为空{//找到左树的最大结点Node* maxleft root-_left;while (maxleft-_right){maxleft maxleft-_right;}//交换maxleft和待删除结点的Key值//并再次转换成左树删除一个单孩子结点复用上述情况一二的代码swap(maxleft-_key, root-_key);return _Rerase(root-_left, key);}delete cur;}}测试
int main()
{int a[] { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTreeint b;BSTreeint Rb;for (auto e : a){b.insert(e);Rb.Rinsert(e);}for (auto e : a){b.erase(e);b.InOrder();cout endl;Rb.Rerase(e);Rb.InOrder();cout endl;}return 0;
} 5.构造与析构
拷贝构造
前序创建结点后续连接指向。 BSTree(const BSTreeK root){_root _copy(root._root);}Node* _copy(const Node* root){if (root nullptr)return nullptr;Node* newnode new Node(root-_key);newnode-_left _copy(root-_left);newnode-_right _copy(root-_right);return newnode;}
析构函数
后续销毁结点 ~BSTree(){Destroy(_root);}void Destroy(Node* root){if (root nullptr)return;Destroy(root-_left);Destroy(root-_right);delete root;}
默认构造
如果我们写了拷贝构造编译器就不会自己生成默认构造函数了我们可以自己写一个默认构造函数也可以强制编译器生成一个但是默认构造只能有一个。 //告诉编译器强制生成BSTree() default;//自己写BSTree(){}
四.二叉搜索树的应用
1. K模型K模型即只有key作为关键码结构中只需要存储Key即可关键码即为需要搜索到 的值。比如给一个单词word判断该单词是否拼写正确具体方式如下以词库中所有单词集合中的每个单词作为key构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在存在则拼写正确不存在则拼写错误。 2. KV模型每一个关键码key都有与之对应的值Value即Key, Value的键值对。该种方 式在现实生活中非常常见比如英汉词典就是英文与中文的对应关系通过英文可以快速找到与其对应的中文英文单词与其对应的中文word, chinese就构成一种键值对再比如统计单词次数统计成功后给定单词就可快速找到其出现的次数单词与其出现次数就是word, count就构成一种键值对。
例如我们将上述的代码改造成K/V结构
templateclass K,class V
struct BSTreeNode
{BSTreeNode(const K key, const V val):_key(key),_val(val),_right(nullptr),_left(nullptr){}K _key;V _val;BSTreeNodeK,V* _right;BSTreeNodeK,V* _left;
};templateclass K, class V
class KV_BSTree
{typedef BSTreeNodeK,V Node;
public:KV_BSTree() default;KV_BSTree(const KV_BSTreeK,V root){_root _copy(root._root);}~KV_BSTree(){//...}bool insert(const K key,const V val){//如果BSTree还没有结点if (_root nullptr){_root new Node(key,val);return true;}//找到插入的合适位置和其父亲结点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 new Node(key,val);if (key parent-_key){parent-_right cur;}else{parent-_left cur;}return true;}Node* find(const K key){//...}bool erase(const K key){//...}void InOrder(){_InOrder(_root);}private:void Destroy(Node* root){//...}void _InOrder(Node* root){//...}Node* _root nullptr;
};int main()
{string arr[] { 苹果, 西瓜, 苹果, 西瓜, 苹果, 苹果, 西瓜,
苹果, 香蕉, 苹果, 香蕉 };KV_BSTreestring,int b;for (auto e : arr){auto cur b.find(e);if (cur nullptr){b.insert(e, 1);}else{cur-_val;}}b.InOrder();return 0;
} 统计水果出现的次数 五. 二叉搜索树的性能分析
插入和删除操作都必须先查找查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树若每个元素查找的概率相等则二叉搜索树平均查找长度是结点在二 叉搜索树的深度的函数即结点越深则比较次数越多。 但对于同一个关键码集合如果各关键码插入的次序不同可能得到不同结构的二叉搜索树 最优情况下二叉搜索树为完全二叉树(或者接近完全二叉树)其平均比较次数为$log_2 N$ 最差情况下二叉搜索树退化为单支树(或者类似单支)其平均比较次数为$\frac{N}{2}$ 问题如果退化成单支树二叉搜索树的性能就失去了。那能否进行改进不论按照什么次序插 入关键码二叉搜索树的性能都能达到最优那么我们后续章节学习的AVL树和红黑树就可以上 场了。
六.整体代码
BSTree.hpp #pragma once
#includeiostream
using namespace std;templateclass K
struct BSTreeNode
{BSTreeNode(const K key):_key(key),_right(nullptr),_left(nullptr){}K _key;BSTreeNodeK* _right;BSTreeNodeK* _left;
};templateclass K
class BSTree
{typedef BSTreeNodeK Node;
public://告诉编译器强制生成BSTree() default;//自己写//BSTree()//{//}BSTree(const BSTreeK root){_root _copy(root._root);}~BSTree(){Destroy(_root);}bool insert(const K key){//如果BSTree还没有结点if (_root nullptr){_root new Node(key);return true;}//找到插入的合适位置和其父亲结点//父亲结点得作用是我们新插入得结点要和父亲结点连接//简单来说就是父亲结点要孩子指针要指向我们新的结点。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 new Node(key);//判断新插入得结点是父亲得左孩子还是右孩子if (key parent-_key){parent-_right cur;}else{parent-_left cur;}return true;}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 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 (cur _root){_root _root-_right;}else{//判断待删除结点是父亲的左孩子还是右孩子if (cur parent-_left){parent-_left cur-_right;}else{parent-_right cur-_right;}}delete cur;return true;}//待删除结点左节点为空将其左边结点交给父亲else if (cur-_right nullptr){//此时如果删除的是根节点需要改变根节点指向if (cur _root){_root _root-_left;}else{//判断待删除结点是父亲的左孩子还是右孩子if (cur parent-_left){parent-_left cur-_left;}else{parent-_right cur-_left;}}delete cur;return true;} else{//由于删除的结点左右都有孩子//需要找一个能代替删除结点的位置结点即比左子树大比右子树小//最适合的结点就是左子树的最右结点(最大节点)右子树的最左节点(最小结点)Node* MinNode cur-_right;Node* MinParent cur;while (MinNode-_left){MinParent MinNode;MinNode MinNode-_left;}//先将MinNode结点的孩子交给他的父亲//注意:不能因为是找的最左边结点就因为MinNode结点一定是MinParent的左孩子if (MinParent-_left MinNode){MinParent-_left MinNode-_right;}else{MinParent-_right MinNode-_right;}//将MinNode结点的值赋值给curcur-_key MinNode-_key;delete MinNode;return true;}}}return false;}bool Rerase(const K key){return _Rerase(_root,key);}Node* Rfind(const K key){return _Rfind(_root, key);}bool Rinsert(const K key){return _Rinsert(_root, key);}void InOrder(){_InOrder(_root);}private:Node* _copy(const Node* root){if (root nullptr)return nullptr;Node* newnode new Node(root-_key);newnode-_left _copy(root-_left);newnode-_right _copy(root-_right);return newnode;}void Destroy(Node* root){if (root nullptr)return;Destroy(root-_left);Destroy(root-_right);delete root;}bool _Rerase(Node* root, const K key){//空树、没有找到删除的结点if (root nullptr){return false;}if (key root-_key){//key比当前结点小往左树删除return _Rerase(root-_left, key);}else if(key root-_key){//key比当前结点小往左树删除return _Rerase(root-_right, key);}else{//找到开始删除Node* cur root;if (root-_left nullptr){//1.待删除结点左孩子为空root root-_right;}else if (root-_right nullptr){//2.待删除结点右孩子为空root root-_left;}else//待删除结点左右孩子都不为空{//找到左树的最大结点Node* maxleft root-_left;while (maxleft-_right){maxleft maxleft-_right;}//交换maxleft和待删除结点的Key值//并再次转换成左树删除一个单孩子结点复用上述情况一二的代码swap(maxleft-_key, root-_key);return _Rerase(root-_left, key);}delete cur;}}Node* _Rfind(Node* root, const K key){if (root nullptr){return nullptr;}if (key root-_key){return _Rfind(root-_left, key);}else if (key root-_key){return _Rfind(root-_right, key);}else{return root;}}bool _Rinsert(Node* root, const K key){//如果BSTree还没有结点if (root nullptr){root new Node(key);return true;}//BSTree已经有结点if (key root-_key){//key比当前结点小往左树插入return _Rinsert(root-_left, key);}else if (key root-_key){//key比当前结点大往右树插入return _Rinsert(root-_right, key);}else{return false;}}void _InOrder(Node* root){if (root nullptr){return;}_InOrder(root-_left);cout root-_key ;_InOrder(root-_right);}Node* _root nullptr;
};