文章目录
- 前言 🦕
- 二叉搜索树的概念 🦕
- 搜索二叉树的初始化 🦕
- Insert( )插入函数 🦕
- 👾 InsertR( ) 插入函数(递归)
- InOrder( ) 中序遍历打印 🦕
- Find( ) 查找函数 🦕
- 👾 Find( ) 查找函数(递归)
- Erase( ) 删除函数 🦕
- 👾 Erase( ) 函数(递归)
- 默认成员函数 🦕
- 👾 构造函数
- 👾 拷贝构造函数
- 👾 赋值运算符重载
- 👾 析构函数
- 总代码 🦕
前言 🦕
二叉树顾名思义即为一个树中节点的度不大于2的有序树,是树中结构中较为简单的一种;
其特点为一个节点中分为两个子树;
通常可以以二叉树进行分治的思路对问题进行解决(递归或非递归);
但在实际中普通二叉树由于在数据存储方面中并没有明显的特性,所以在实际中普通二叉树的应用场景并不多;
BinarySearchTree二叉搜索树
,顾名思义这种类型的二叉树对搜索有着比较大的优势;
二叉搜索树的概念 🦕
二叉搜索树又称搜索二叉树,也可以称为排序树,顾名思义这棵树有着对数据进行搜索的优势;
该二叉树的性质为:
- 如果这棵树的左子树不为空,则左子树上的节点值都小于根节点的值;
- 如果这棵树的右子树不为空,则右子树上的节点值都小于根节点的值;
二叉搜索树的左右子树也同时满足该条件;
该二叉树的节点若是使用中序遍历的方式进行打印则将会以顺序的方式进行打印,所以这棵树也被称为排序树;
同时搜索二叉树中的数据还具有唯一性,即同一种的数据只能进行一次插入;
搜索二叉树的初始化 🦕
在实现当中搜索二叉树也应该使用类模板确保可以进行泛型编程;
搜索二叉树的初始化与普通二叉树的初始化相当,都需要两个类:
struct BSTNode
该类用来对搜索二叉树的各个节点进行封装;template<class K>//一般情况下二叉树内的模板参数习惯以K代替(key); struct BSTNode{//构造函数BSTNode(const K& key = K())// 给定缺省参数确保可以正常进行初始化:_pleft(nullptr),//初始化列表初始化参数_pright(nullptr),_key(key){}BSTNode<K> *_pleft;//左子树BSTNode<K> *_pright;//右子树K _key;//数据 };
class BinarySearchTree
该类主要用于实现二叉树的整体(包括各个成员函数)template<class K> class BinarySearchTree{ typedef BSTNode<K> Node;//对节点进行typedef重命名 public:BinarySearchTree(){}//构造函数~BinarySearchTree(){}//析构函数BinarySearchTree(const BinarySearchtree<K> &node){}//拷贝构造函数BinarySearchTree<K>& operator=(BinarySearchTree<K> node){}//赋值运算符重载bool Insert(const K& key){}//数据插入void InOrder(){}//中序遍历打印数据bool Find(const K&key){}//查找数据bool Erase(const K&key){}//删除数据protected: private:Node* _proot = nullptr;//头节点 };
Insert( )插入函数 🦕
插入函数的实现在搜索二叉树中一般为顺照搜索二叉树的特性进行遍历;
当遍历节点至空时则表示在该处进行插入;
- 若是需要插入的数据大于当前节点的数据则向当前节点的右子树进行遍历;
- 若是需要插入的数据小于当前节点的数据则向当前节点的左子树进行遍历;
- 若是需要插入的数据等于当前节点的数据则返回
false
(唯一性); - 遍历至
nullptr
处则表示当前不存在节点,可进行插入,插入结束后返回true
;
bool Insert(const K& key){if(_proot == nullptr){//如果root节点为空说明为空树,空树进行第一次插入_proot = new Node(key);return true;//插入成功进行返回}Node* cur = _proot;Node* tmp;while(cur!=nullptr){tmp = cur;if(key>cur->_key){tmp = cur;cur = cur->_pright;}else if(key<cur->_key){tmp = cur;cur = cur->_pleft;}else{//相同 返回falsereturn false;}}if(key>tmp->_key){//链接在右边cur = new Node(key);tmp->_pright = cur;}else{//链接在左边cur = new Node(key);tmp->_pleft = cur;}return true;}
该函数的插入还存在一个链接的问题;
链接的问题采用类似双指针的方式,一个指针进行遍历确认是否进行插入,另一个指针用来保存该节点的父节点位置方便两个节点进行链接;
👾 InsertR( ) 插入函数(递归)
一般来说在类中成员函数的递归比较麻烦的是对于数据的传参问题;
所以为了解决这个问题建议在自定义类型中定义递归函数时尽量写一个子函数用于递归,并使用成员函数来调用此子函数;
该函数的递归思路与非递归的思路相当,唯一不同的是在递归中除了要思考节点的插入问题同时还需要注意节点与节点之间的链接问题,在递归当中子函数的节点参数可以使用Node*& root
来代替;
由于引用算是一个变量的别名,意义即为将一个指针的别名传入下一层的递归当中,将新节点插入该节点即完成插入的节点链接问题;
bool InsertR(const K&key){//主函数return _InsertR(key,_proot);//调用函数进行递归}//----------------------------------bool _InsertR(const K&key,Node*& root){//子函数if(root == nullptr) {root = new Node(key);return true;}if(root->_key < key){return _InsertR(key,root->_pright);}else if(root->_key>key){return _InsertR(key,root->_pleft);}else return false;}
InOrder( ) 中序遍历打印 🦕
该函数以递归的方式进行中序遍历,使用子函数递归,主函数调用的方式即可;
void InOrder(){//主函数_InOrder(_proot);cout<<endl;}//----------------------------------void _InOrder(Node *root){//子函数if(root == nullptr){return;}_InOrder(root->_pleft);cout<<root->_key<<" ";_InOrder(root->_pright);}
Find( ) 查找函数 🦕
该函数使用非递归以搜索二叉树的规则进行遍历即可;
找到数据则返回true
否则返回false
;
bool Find(const K& key){Node*pcur = _proot;while(pcur){if(key>pcur->_key) pcur = pcur->_pright;else if(key<pcur->_key) pcur = pcur->_pleft;else return true;}return false;}
👾 Find( ) 查找函数(递归)
该函数建立子函数进行递归即可;
bool FindR(const K& key){//主函数return _FindR(key,_proot);}//----------------------------------bool _FindR(const K&key,Node*root){//子函数if(root == nullptr) return false; if(root->_key == key) return true;if(root->_key>key) return _FindR(key,root->_pleft);else return _FindR(key,root->_pright); }
Erase( ) 删除函数 🦕
删除函数在搜索二叉树中是一个比较复杂的函数;
复杂的问题在于:
- 该如何删除节点;
- 删除节点后如何对剩余节点进行链接;
同时从一张图的分析中可以分析出搜索二叉树删除节点的几种情况:
- 删除的节点不存在子树;
当所删除的节点不存在左右子树时直接将节点进行删除即可;
- 删除的节点左子树为空或者右子树为空;
当所删除的节点的左子树或者右子树为空时,可采用托孤的方式对节点进行链接;
即所删除节点的父节点与所删除的子节点进行链接;
- 删除的节点左右子树都不为空;
当所删除的节点其左右子树都不为空时,由于每个节点只能管理两个子树(左右子树),而所删除的节点存在左右两棵子树,故不能采用托孤的方式对所删除的节点的子树进行管理;
在这种情况下即可采用伪删除法的方式对节点进行伪删除;
伪删除即为选择一个符合接管该节点的左右子树规则的节点,将其值赋值给当前节点,并将选择的节点进行删除 (符合该种条件的节点一般为该节点左子树的最大值或该节点右子树的最小值);
以该图为例若是删除节点8
,则可以使用节点7
进行代替接管其左右子树,并将原节点7
进行删除从而达到伪删除的目的;
实现:
bool Erase(const K& key){if(nullptr == _proot) return false;//找到这个key值Node* pcur = _proot;Node* parent = _proot;while(pcur){//进行遍历找到对应的key值if(key>pcur->_key){parent = pcur;pcur = pcur->_pright;}else if(key<pcur->_key){parent = pcur;pcur = pcur->_pleft;}else{//已经找到了对应的key值,需要进行删除//左为空的情况if(pcur->_pleft == nullptr){//判断极端情况(左为空或者右为空的情况下所删节点为根)if(pcur == _proot){_proot = pcur->_pright;delete pcur;return true;}if(parent->_pleft == pcur){parent->_pleft = pcur->_pright;}else{parent->_pright = pcur->_pright;}delete pcur;return true;}//右为空的情况else if(pcur->_pright == nullptr){//判断极端情况(左为空或者右为空的情况下所删节点为根)if(pcur == _proot){_proot = pcur->_pleft;delete pcur;return true;}if(parent->_pleft == pcur){parent->_pleft = pcur->_pleft;}else {parent->_pright = pcur->_pleft;}delete pcur;return true;}//左右都不为空else{/** 找到左子树的最右子树或右子树的最左子树*/Node*tmp = pcur->_pright; //右子树的最左子树(最小值)Node*tmp_parent = pcur;while(tmp->_pleft){tmp_parent = tmp;tmp = tmp->_pleft; }/** 找到符合条件的值,进行伪删除法;*/pcur->_key = tmp->_key;//说明该子树存在孩子,需要对该子树进行托孤//由于所找的数据为该节点右子树的最小值,说明找到的这个最小值不存在左子树,只需要对右子树进行托孤;if(tmp_parent->_pleft == tmp){tmp_parent->_pleft = tmp->_pright;delete tmp;}else{tmp_parent->_pright = tmp->_pright;delete tmp;}return true;}}}//未找到key值,pcur指针已经为空,返回false;return false;}
👾 Erase( ) 函数(递归)
对于Erase()函数的递归方式与其非递归的方式都有异曲同工之处;
唯一不同的是为了节点链接的方便在用于递归的子函数当中使用指针引用的方式完成节点的链接;
bool EraseR(const K&key){//主函数return _EraseR(key,_proot);}//----------------------------------bool _EraseR(const K& key, Node*& root){//子函数if(root == nullptr) return false;if(key>root->_key) return _EraseR(key,root->_pright);else if(key<root->_key) return _EraseR(key,root->_pleft);else{Node *cur = root;//左为空if(root->_pleft == nullptr){root = root->_pright;delete cur;return true;} //右为空 else if(root->_pright == nullptr){root = root->_pleft;delete cur;return true;}//左右都不为空else{cur = root->_pleft; while(cur->_pright) cur = cur->_pright;root->_key = cur->_key;_EraseR(cur->_key,root->_pleft);return true; } }
}
默认成员函数 🦕
👾 构造函数
搜索二叉树的构造函数即对节点进行初始化即可,在不存在其他的构造函数的情况下可以使用默认生成的构造函数,因为由于在定义成员变量时给定了成员变量了缺省参数_proot = nullptr
;
若是存在其他构造函数 (拷贝构造函数也为构造函数的一种) 时则需要手写这个构造函数;
或是使用default
关键字:
BinarySearchTree() = default;
👾 拷贝构造函数
拷贝构造函数则可以使用递归的方式逐个对节点进行拷贝从而达到深拷贝的效果;
BinarySearchTree(const BinarySearchTree<K> &node){//主函数_proot = _CopyNode(node._proot);}//----------------------------------Node* _CopyNode(const Node* node){//子函数if(node == nullptr) {return nullptr;}Node*newnode = new Node(node->_key);newnode->_pleft = _CopyNode(node->_pleft);newnode->_pright = _CopyNode(node->_pright);return newnode;}
👾 赋值运算符重载
赋值运算符重载一般采用现代的写法:
即为利用自定义类型在进行传值传参时会进行拷贝构造的特性完成对该自定义类型的拷贝,再进行交换与返回;
BinarySearchTree<K>& operator=( BinarySearchTree<K> node){//在传值传参过程中调用拷贝构造生成了临时拷贝(深拷贝)swap(_proot,node._proot);return *this;}
👾 析构函数
析构函数采用父子函数的方式,利用子函数作递归,主函数调用该子函数即可;
void _Destroy(Node*& root){if(root == nullptr) return;_Destroy(root->_pleft);_Destroy(root->_pright);delete root;root = nullptr;
}//----------------------------------~BinarySearchTree(){_Destroy(_proot);
}
总代码 🦕
#pragma once #include<iostream>using namespace std;template<class K>
struct BSTNode{BSTNode(const K& key = K()):_pleft(nullptr),_pright(nullptr),_key(key){}BSTNode<K> *_pleft;BSTNode<K> *_pright;K _key;
};template<class K>
class BinarySearchTree{typedef BSTNode<K> Node;public:/** 默认成员函数*/ //--------------------------------//构造函数BinarySearchTree()=default;/*强制生成默认构造函数*///--------------------------------//拷贝构造BinarySearchTree(const BinarySearchTree<K> &node){_proot = _CopyNode(node._proot);}//--------------------------------//赋值重载操作符BinarySearchTree<K>& operator=( BinarySearchTree<K> node){swap(_proot,node._proot);return *this;}//--------------------------------//析构函数 - 使用递归方法 后序遍历进行Destory~BinarySearchTree(){_Destroy(_proot);}//--------------------------------bool Insert(const K& key){if(_proot == nullptr){//如果root节点为空说明为空树,空树进行第一次插入_proot = new Node(key);return true;//插入成功进行返回}Node* cur = _proot;Node* tmp;while(cur!=nullptr){tmp = cur;if(key>cur->_key){tmp = cur;cur = cur->_pright;}else if(key<cur->_key){tmp = cur;cur = cur->_pleft;}else{//相同 返回falsereturn false;}}if(key>tmp->_key){//链接在右边cur = new Node(key);tmp->_pright = cur;}else{//链接在左边cur = new Node(key);tmp->_pleft = cur;}return true;}//-----------------------void InOrder(){/** 自定义类型成员函数若是需要进行递归时* 尽量设置一个子函数来方便递归并使用原成员函数进行调用* * 在该自定义类型当中,由于传的参数为private私有成员变量* 私有成员变量不方便直接进行传参,所以为了方便传参_proot* 在private访问限定符的域当中设置一个子函数用来递归* 通过原函数去调用这个私有成员函数*/_InOrder(_proot);cout<<endl;}//-----------------------bool Find(const K& key){Node*pcur = _proot;while(pcur){if(key>pcur->_key) pcur = pcur->_pright;else if(key<pcur->_key) pcur = pcur->_pleft;else return true;}return false;}//-----------------------bool Erase(const K& key){if(nullptr == _proot) return false;//找到这个key值Node* pcur = _proot;Node* parent = _proot;while(pcur){//进行遍历找到对应的key值if(key>pcur->_key){parent = pcur;pcur = pcur->_pright;}else if(key<pcur->_key){parent = pcur;pcur = pcur->_pleft;}else{//已经找到了对应的key值,需要进行删除//左为空的情况if(pcur->_pleft == nullptr){if(pcur == _proot){_proot = pcur->_pright;delete pcur;return true;}if(parent->_pleft == pcur){parent->_pleft = pcur->_pright;}else{parent->_pright = pcur->_pright;}delete pcur;return true;}//右为空的情况else if(pcur->_pright == nullptr){//判断极端情况(左为空或者右为空的情况下所删节点为根)if(pcur == _proot){_proot = pcur->_pleft;delete pcur;return true;}if(parent->_pleft == pcur){parent->_pleft = pcur->_pleft;}else {parent->_pright = pcur->_pleft;}delete pcur;return true;}//左右都不为空else{/** 找到左子树的最右子树或右子树的最左子树*/Node*tmp = pcur->_pright; //右子树的最左子树(最小值)Node*tmp_parent = pcur;while(tmp->_pleft){tmp_parent = tmp;tmp = tmp->_pleft; }/** 找到符合条件的值,进行伪删除法;*/pcur->_key = tmp->_key;//说明该子树存在孩子,需要对该子树进行托孤//由于所找的数据为该节点右子树的最小值,说明找到的这个最小值不存在左子树,只需要对右子树进行托孤;if(tmp_parent->_pleft == tmp){tmp_parent->_pleft = tmp->_pright;delete tmp;}else{tmp_parent->_pright = tmp->_pright;delete tmp;}return true;}}}//未找到key值,pcur指针已经为空,返回false;return false;}//-----------------------bool FindR(const K& key){return _FindR(key,_proot);}//-----------------------bool InsertR(const K&key){return _InsertR(key,_proot);}//-----------------------bool EraseR(const K&key){return _EraseR(key,_proot);}//-----------------------private:/** 私有成员包括成员函数以及成员变量*/Node *_proot = nullptr; //-----------------------protected:void _InOrder(Node *root){if(root == nullptr){return;}_InOrder(root->_pleft);cout<<root->_key<<" ";_InOrder(root->_pright);}//-----------------------bool _FindR(const K&key,Node*root){if(root == nullptr) return false;if(root->_key == key) return true;if(root->_key>key) return _FindR(key,root->_pleft);else return _FindR(key,root->_pright);} //-----------------------bool _InsertR(const K&key,Node*& root){//纯高效法/** 该方法重点在于使用了一个引用来引用指针,* 指针既可以为判断条件(为nullptr),* 也不缺乏其为上一个节点的左\右子树;* */if(root == nullptr) {root = new Node(key);return true;}if(root->_key < key){return _InsertR(key,root->_pright);}else if(root->_key>key){return _InsertR(key,root->_pleft);}else return false;}//-----------------------bool _EraseR(const K& key, Node*& root){if(root == nullptr) return false;if(key>root->_key) return _EraseR(key,root->_pright);else if(key<root->_key) return _EraseR(key,root->_pleft);else{/** 在该递归删除中同样利用到了指针引用的方式 // 具体能够指针引用是因为递归每次都会开辟出新的栈帧* 解决了大部分的判断问题(具体表现在左为空或右为空)* 在判断两个子树都不为空的条件下,具体的思路是利用了循环找到可以进行伪删除法的数,再重新调用该子函数完成删除* 其想法可以理解为将问题划分为子问题* (将左右均不为空的方式在进行一系列操作后将其最终以左为空或者右为空的问题进行解决);* */Node *cur = root;//左为空if(root->_pleft == nullptr){root = root->_pright;delete cur;return true;} //右为空 else if(root->_pright == nullptr){root = root->_pleft;delete cur;return true;}//左右都不为空else{cur = root->_pleft; while(cur->_pright) cur = cur->_pright;root->_key = cur->_key;_EraseR(cur->_key,root->_pleft);return true; } }}//-----------------------Node* _CopyNode(const Node* node){if(node == nullptr) {return nullptr;}Node*newnode = new Node(node->_key);newnode->_pleft = _CopyNode(node->_pleft);newnode->_pright = _CopyNode(node->_pright);return newnode;}//------------------------void _Destroy(Node*& root){if(root == nullptr) return;_Destroy(root->_pleft);_Destroy(root->_pright);delete root;root = nullptr;}};