真c++ 从二叉树到红黑树(3)之二叉搜索树BST
此文章为从二叉树到红黑树系列文章的第三节,主要介绍介绍二叉搜索树BST,为AVL和RedBlack打下基础
文章目录
- 一、前面文章链接~(点击右边波浪线可以返回目录)
- 二、二叉搜索树BST的定义~
- 三、二叉搜索树类~
- (一)定义变量和接口~
- 1.需要的变量~
- 2.需要的接口~
- 3.重要辅助函数~
- 4.BST.h~
- (二)查找search~
- 1._hot节点的作用~
- 2.查找递归版~
- 3.查找迭代版~
- 4.search复杂度分析~
- (三)插入insert~
- 1.插入代码~
- 2.插入示例(动图演示!)~
- 3.insert复杂度分析~
- (四)删除remove~
- 1.最重要的删除函数(动图演示!)~
- (1)该节点没有子树~
- (2)该节点只有一颗子树~
- a.只有左子树~
- b.只有右子树~
- (3)该节点有两颗子树~
- a.右孩子就是该节点的直接后继~
- b.该节点的直接后继在右孩子的左子树中~
- (4)删除静态函数代码~
- 2.BST的删除代码~
- 3.remove复杂度分析~
- (五)完整BST.h~
- 四、BST测试代码~
- 五、BST的缺点~
- 五、后序文章链接~
一、前面文章链接~(点击右边波浪线可以返回目录)
在阅读本文前,强烈建议你看下前面的文章的目录、前言以及基本介绍,否则你无法理解后面的内容。链接如下:
二、二叉搜索树BST的定义~
确认一颗树的中序遍历序列十分容易。即将树中每一个元素都投影到底部就可以了。
当一颗树的中序遍历序列是单调非减的时候,那么这颗树就必为一颗BST,即二叉搜索树。
更准确一点的定义为:根节点的值大于其左子树中任意一个节点的值,小于其右节点中任意一节点的值。其左子树和右子树也都满足这个条件。
二叉搜索树,又称二叉排序树,二叉查找树。
如下图所示
三、二叉搜索树类~
既然二叉搜索树属于二叉树的特例,故自然可以基于BinTree模板类(见此系列文章第二部分)
由于BST继承于普通二叉树。而普通二叉树的大部分函数和变量都是属于public或者protected的。自然BST可以沿用这些函数和变量。
并由于BST本身特殊的性质(单调非减),因此需要对某些函数进行重写(override)。以及扩增一些新的功能。
在此,为了方便性起见,我们默认BST中没有重复的数(读者可以自行更改search接口的判断条件来进行扩充)。
(一)定义变量和接口~
1.需要的变量~
_hot节点,此节点的设定至关重要,有了这个节点,就能简化很多步骤,方便BST包括后序的AVL,RedBlack的插入和删除。读者看到后面的插入和删除算法的实现的时候,自然能够体会到这个节点的作用。
BinNodePtr _hot;//"命中节点"的"父亲"2.需要的接口~
由于BST是继承于BinTree的,因此,很多接口都可以沿用BinTree的接口,比如遍历,判空,获取当前高度等等。但依然有些接口是需要进行扩充和重写的。
比如:
查找树中有没有这个节点search 在树中插入一个节点insert 在树中删除一个节点remove3.重要辅助函数~
对于BST本身,有了上面的查找,插入和删除,已经可以满足BST本身的要求了,但是对于后序的AVL和RedBlack而言,还远远不够,因此在此处会引入两个重要的辅助函数,并且我会在介绍AVL的时候,详细说明这两个辅助函数的作用。在介绍RedBlack的时候,也会引入一个指向AVL的链接。
关于下面两个函数的实现,我不会在本节进行解析,我会在AVL中再进行解析。
3+4重构 connect34 对该节点及其父亲、祖父做统一旋转调整rotateAt4.BST.h~
由于BST属于二叉树的一种,因此,可以将BST继承自我们在第二部分定义的BinTree。
template<typename T=int> class BST :public BinTree<T> { protected:using BinNodePtr = BinNode<T>*;protected:BinNodePtr _hot;//"命中节点"的"父亲"public:BST():_hot(nullptr){}//调用构造函数时,先调用子类的,再调用父类,析构则相反virtual~ BST() = default;//由于删除全部交给b树模板类,所以这里不需要去额外删除protected:BinNode<T>* connect34(//3+4重构,可以用于AVL,RedBlackBinNode<T>*, BinNode<T>*, BinNode<T>*,BinNode<T>*, BinNode<T>*, BinNode<T>*, BinNode<T>*);BinNode<T>* rotateAt(BinNode<T>* v);//对v及其父亲、祖父做统一旋转调整private:BinNode<T>*& search_R(const T& data);//查找递归版public:BinNode<T>*& search(const T& data);//查找//返回引用,便于插入删除//接受者一般要加个引用接收virtual BinNode<T>* insert(const T& data)override;//插入节点virtual bool remove(const T& data);//删除节点};//class BST(二)查找search~
从树根出发,逐步地缩小查找范围,直到发现目标(成功)或缩小至空树(失败)。由于BST的中序遍历是有序的,所以可以用二分查找法。
比如下方查找22和23
找22时,先与根节点比较大小,其比根节点16大,则进入右子树查找。下一次与25相比,22比25小,所以进入左子树查找。然后与19对比,自然进入右分支,最后正好命中22这个节点。即查找成功。
找23时,也会经过这样一条道路,由于22的右子树为空,所以查找失败。
1._hot节点的作用~
若查找成功,则search()的返回值都将如下图(a)所示,指向一个数值为e且真实存在的节点;
若查找失败,则返回值的数值虽然为NULL,但是它作为引用将如下图(b)所示,指向最后一次试图转向的空节点。
由上图可以得知,无论查找成功或者失败,查找的返回值都是等效地指向“命中节点”,而命中节点的父亲节点就是_hot节点。有了_hot节点,对于插入和删除,都会变得相对容易一点。
2.查找递归版~
众所周知,二分查找可以写成递归或者迭代形式。并且幸运的是,对于二叉树而言,其二分查找的算法,与一般数组的查找算法相比,本质上没有任何区别,甚至可以说比一般的二分查找算法更加简单一点。
由于递归算法需要不断地调用自身,并且为了保持与非递归算法的接口传入参数的一致性,所以不妨将递归的部分与不需要递归的部分分离开来。
下面是主要查找函数的接口。当需要查找一个数据时,就从根节点开始,进行二分递归查找。并同时更新_hot节点的值,方便后续的插入和删除。
并且返回值注意为引用类型。这点对于删除算法而言至关重要。
template<typename T> BinNode<T>*& BST<T>::search_R(const T& data) {using mybst::search_In;return search_In(this->_root, data, _hot = nullptr); }下方为二分查找的递归算法
//=================查找================// namespace mybst {template<typename T,typename BinNodePtr>static BinNode<T>*& search_In(BinNodePtr& current, const T& data, BinNodePtr& hot) {//必须加引用,不然地址改变的话,原地址不能改变if (!current || data == current->_data)return current;//递归基,当当前节点为空或者找到了对应的节点,就退出hot = current;//hot 记录父亲节点的位置return search_In((data < current->_data ? current->_lchild : current->_rchild), data, hot);//二分递归查找} }//namespace mybst3.查找迭代版~
比起分开写的递归算法,笔者认为迭代版的查找算法的可读性相对更高一点,并且效率也更高一点。同样,也要注意返回的是引用。
后面在插入和查找过程中,都默认用的是迭代版算法。
4.search复杂度分析~
在二叉搜索树的每一层,查找算法至多访问一个节点,且只需常数时间,故总体所需时间应线性正比于查找路径的长度,或最终返回节点的深度,在最坏情况下不超过全树的高度。
(三)插入insert~
对于BST而言,无论是插入还是删除而言,先前都必将要经过查找操作。对于在此系列定义的BST而言,里面存放的都是不可重复的数据。因此,对于插入而言,如果这个元素存在BST中,则不插入,如果在BST中不存在这个元素,就在适合的位置插入这个元素。
在此,_hot节点的作用就体现出来了,如果不存在这个元素,_hot节点也必将是假设这个不存在的元素存在时的父亲节点。
1.插入代码~
//=================插入================// template<typename T> BinNode<T>* BST<T>::insert(const T& data) {BinNodePtr& x = search(data);//查找有无这个节点//必须用引用,这样其才必然是_hot的孩子if (x)//若有,则返回这个节点,插入失败return x;x = new BinNode<T>(data, _hot);//若无,则新建一个节点,并将_hot节点作为其父节点连接到树上this->_size++;//更新规模this->updateHeightAbove(x);//更新高度。return x; } 请读者好好体会这段简短但不简单的代码所表达的意思。特别是引用的用处。
并且插入后也要更新相应的节点高度,以及其祖先节点的高度。关于高度更新的算法实现,请见本系列文章第二部分。
2.插入示例(动图演示!)~
在下图中插入213.insert复杂度分析~
节点插入操作所需的时间,主要消耗于对算法search()及updateHeightAbove()的调用。后者与前者一样,在每一层次至多涉及一个节点,仅消耗O(1)时间,故其时间复杂度也同样取决于新节点的深度,在最坏情况下不超过全树的高度。
(四)删除remove~
理解BST的删除算法,可以说是BST最重要的一点,弄懂了BST的删除,那么你就懂了BST,接下来介绍一个最重要的删除静态函数,这个函数不仅对于BST有用,对于AVL,RedBlack都有用。
1.最重要的删除函数(动图演示!)~
删除一颗BST的节点,并且保持BST的性质,需要分以下3种大情况来考虑。
(1)该节点没有子树~
直接删除该节点既可,并且修改对应指针地址 如下图删除节点21(2)该节点只有一颗子树~
a.只有左子树~
则其左子树接替它的位置,并且修改对应指针地址 如下图删除节点29b.只有右子树~
则其右子树接替它的位置,并且修改对应指针地址 如下图删除节点29(3)该节点有两颗子树~
在此,需要借用在本系列文章第一部分提到的关于求当前节点的直接后继算法。建议看完这个算法的实现,再来理解这种情况,否则你会感到迷惑。
首先要在心里明确一个事实:即直接后继,必然没有左孩子。
a.右孩子就是该节点的直接后继~
在这种情况下,可以得知,该右孩子必然没有左孩子(可能有右孩子),不然其右孩子不可能作为其直接后继。此时,就将该节点与其后继(也就是右孩子)的值进行交换(看下面代码你就能明白交换比直接删这个节点更好)。再重新连接指针即可。
下面图示为了简单起见,没有体现交换的过程 如下图删除节点20b.该节点的直接后继在右孩子的左子树中~
在这种情况下,可以得知,该节点的直接后继必然是左孩子。此时,就将该节点与其后继(也就是右孩子)的值进行交换。再重新连接指针即可。
下面图示为了简单起见,没有体现交换的过程 如下图删除节点65(4)删除静态函数代码~
此代码为删除算法的难点,重点,理解好此代码,你才能理解进一步的AVL,RedBlack!
其中HasLChid等全局函数,位于第一部分定义的BInNode_Macro.h中,succ()算法位于第一部分求直接后继的算法中,release删除函数位于第二部分定义的release.h中。
//=================删除================// template<typename T>//适用于AVL Splay,RedBlack等,必须这么设计,才能做到完美删除,且保持BST的性质 static BinNode<T>* removeAt(BinNode<T>*& x, BinNode<T>*& hot) {//这里x必须用引用,才不会使指针乱指using BinNodePtr = BinNode<T>*; //记录x的地址里面保存的值,若删除temp里面的值,即删除x里面的值,但x的本身地址不会影响temp,反之亦然。BinNodePtr temp = x;//替代被删除节点的接替者,一般为被删除节点的左孩子或者右孩子,而不是x的左孩子或者右孩子BinNodePtr replacer = nullptr;if (!HasLChild(x)) {//如果x没有左孩子,或者x左右孩子均无,则将x的右孩子作为x,并将接替者设为x的右孩子x = x->_rchild;replacer = x;}else if (!HasRChild(x)) {//如果x没有右孩子,则将x的左孩子作为x,并将接替者设为x的左孩子x = x->_lchild;replacer = x;}else {temp = temp->succ();//取得中序遍历的后继//这个后继必将没有左孩子std::swap(x->_data, temp->_data);//交换对应的值if (temp->_parent == x) {//如果后继的父亲是原来的x,后继必然为x的右孩子replacer = temp->_rchild; //就将后继的右孩子作为父亲的右孩子temp->_parent->_rchild = replacer;}else {//如果后继的父亲不是原来的x,后继必然为某一节点的左孩子replacer = temp->_rchild;temp->_parent->_lchild=replacer;//就将后继的右孩子作为这个节点左孩子}}//hot即被删除节点的父亲。而temp正是要删除的节点。hot = temp->_parent;if (replacer)//若replacer存在,则必须将其父指针指向hot。不然如同x->_rchild的父亲指向的还是原来的replacer->_parent = hot;//释放原来x所指的堆区的数据,或者x的后继的堆区的数据release(temp->_data);release(temp);return replacer; }2.BST的删除代码~
理解了上面的代码,就能很容易理解BST的删除。
同样先前都必将要经过查找操作。对于在此系列定义的BST而言,里面存放的都是不可重复的数据。因此,对于删除而言,如果这个元素存在BST中,则进行删除,如果在BST中不存在这个元素,就返回false。
在此,_hot节点的作用就体现出来了,如果存在这个元素,在经过removeAt算法后,_hot节点也必将是被删除节点的父亲节点,此时,更新高度,只需要更新_hot节点的高度既可。
template<typename T> bool BST<T>::remove(const T& data) {BinNodePtr& x = search(data);//接收返回的值,这里用引用if (x == nullptr)//若没找到return false;//返回falseremoveAt(x, _hot);//删除找到的节点this->_size--;//更新规模this->updateHeightAbove(_hot);//更新高度return true; }3.remove复杂度分析~
删除操作所需的时间,主要消耗于对search()、succ()和updateHeightAbove()的调用。在树中的任一高度,它们至多消耗O(1)时间。故总体的渐进时间复杂度,亦不超过全树的高度。
(五)完整BST.h~
注意,connect34算法和rotateAt算法,我会在AVL中再进行介绍
#pragma once #include "BinTree.h"namespace mytree {using namespace mytree_marcro;template<typename T=int>class BST :public BinTree<T> {protected:using BinNodePtr = BinNode<T>*;protected:BinNodePtr _hot;//"命中节点"的"父亲"public:BST():_hot(nullptr){}//调用构造函数时,先调用子类的,再调用父类,析构则相反virtual~ BST() = default;//由于删除全部交给b树模板类,所以这里不需要去额外删除protected:BinNode<T>* connect34(//3+4重构,可以用于AVL,RedBlackBinNode<T>*, BinNode<T>*, BinNode<T>*,BinNode<T>*, BinNode<T>*, BinNode<T>*, BinNode<T>*);BinNode<T>* rotateAt(BinNode<T>* v);//对v及其父亲、祖父做统一旋转调整private:BinNode<T>*& search_R(const T& data);//查找递归版public:BinNode<T>*& search(const T& data);//查找//返回引用,便于插入删除//接受者一般要加个引用接收virtual BinNode<T>* insert(const T& data)override;//插入节点virtual bool remove(const T& data);//删除节点};//class BSTtemplate<typename T>BinNode<T>* BST<T>::connect34(BinNode<T>* a, BinNode<T>* b, BinNode<T>* c,BinNode<T>* T1, BinNode<T>* T2, BinNode<T>* T3, BinNode<T>* T4){a->_lchild = T1; if (T1)T1->_parent = a;a->_rchild = T2; if (T2)T2->_parent = a; this->updateHeight(a);c->_lchild = T3; if (T3)T3->_parent = c;c->_rchild = T4; if (T4)T4->_parent = c; this->updateHeight(c);b->_lchild = a; a->_parent = b;b->_rchild = c; c->_parent = b; this->updateHeight(b);return b;}template<typename T>BinNode<T>* BST<T>::rotateAt(BinNode<T>* v) //返回调整后局部子树根节点的位置{if (v == nullptr) {printf("Error!"); exit(0);}BinNode<T>* p = v->_parent; BinNode<T>* g = p->_parent;//设定v的父亲与祖父//视v、p和g相对位置分四种情况if (IsLChild(p)) {//Lif (IsLChild(v)) {//LLp->_parent = g->_parent;//向上连接return connect34(v, p, g, v->_lchild, v->_rchild, p->_rchild, g->_rchild);}else {//LRv->_parent = g->_parent;//向上连接return connect34(p, v, g, p->_lchild, v->_lchild, v->_rchild, g->_rchild);}}else {//Rif (IsRChild(v)) {//RRp->_parent = g->_parent;//向上连接return connect34(g, p, v, g->_lchild, p->_lchild, v->_lchild, v->_rchild);}else {//RLv->_parent = g->_parent;//向上连接return connect34(g, v, p, g->_lchild, v->_lchild, v->_rchild, p->_rchild);}}}//=================查找================//namespace mybst {template<typename T,typename BinNodePtr>static BinNode<T>*& search_In(BinNodePtr& current, const T& data, BinNodePtr& hot) {//必须加引用,不然地址改变的话,原地址不能改变if (!current || data == current->_data)return current;//递归基,当当前节点为空或者找到了对应的节点,就退出hot = current;//hot 记录父亲节点的位置return search_In((data < current->_data ? current->_lchild : current->_rchild), data, hot);//二分递归查找}}//namespace mybsttemplate<typename T>BinNode<T>*& BST<T>::search_R(const T& data) {using mybst::search_In;return search_In(this->_root, data, _hot = nullptr);}template<typename T>BinNode<T>*& BST<T>::search(const T& data) {if (!this->_root || data == this->_root->_data) {//如果根节点不为空,或者正好根节点就是要找的节点_hot = nullptr;return this->_root;}_hot = this->_root;//更新_hotwhile (true){//current的地址与_hot的孩子绑定,不能与_hot绑定。BinNodePtr& current = ((data < _hot->_data) ? _hot->_lchild : _hot->_rchild);if (!current || data == current->_data)return current;_hot = current;//更新_hot位置}}//=================插入================//template<typename T>BinNode<T>* BST<T>::insert(const T& data) {BinNodePtr& x = search(data);//查找有无这个节点//必须用引用,这样其才必然是_hot的孩子if (x)//若有,则返回这个节点,插入失败return x;x = new BinNode<T>(data, _hot);//若无,则新建一个节点,并将_hot节点作为其父节点连接到树上this->_size++;//更新规模this->updateHeightAbove(x);//更新高度。return x;}//=================删除================//template<typename T>//适用于AVL Splay,RedBlack等,必须这么设计,才能做到完美删除,且保持BST的性质static BinNode<T>* removeAt(BinNode<T>*& x, BinNode<T>*& hot) {//这里x必须用引用,才不会使指针乱指using BinNodePtr = BinNode<T>*; //记录x的地址里面保存的值,若删除temp里面的值,即删除x里面的值,但x的本身地址不会影响temp,反之亦然。BinNodePtr temp = x;//替代被删除节点的接替者,一般为被删除节点的左孩子或者右孩子,而不是x的左孩子或者右孩子BinNodePtr replacer = nullptr;if (!HasLChild(x)) {//如果x没有左孩子,或者x左右孩子均无,则将x的右孩子作为x,并将接替者设为x的右孩子x = x->_rchild;replacer = x;}else if (!HasRChild(x)) {//如果x没有右孩子,则将x的左孩子作为x,并将接替者设为x的左孩子x = x->_lchild;replacer = x;}else {temp = temp->succ();//取得中序遍历的后继//这个后继必将没有左孩子std::swap(x->_data, temp->_data);//交换对应的值if (temp->_parent == x) {//如果后继的父亲是原来的x,后继必然为x的右孩子replacer = temp->_rchild; //就将后继的右孩子作为父亲的右孩子temp->_parent->_rchild = replacer;}else {//如果后继的父亲不是原来的x,后继必然为某一节点的左孩子replacer = temp->_rchild;temp->_parent->_lchild=replacer;//就将后继的右孩子作为这个节点左孩子}}//hot即被删除节点的父亲。而temp正是要删除的节点。hot = temp->_parent;if (replacer)//若replacer存在,则必须将其父指针指向hot。不然如同x->_rchild的父亲指向的还是原来的replacer->_parent = hot;//释放原来x所指的堆区的数据,或者x的后继的堆区的数据release(temp->_data);release(temp);return replacer;}template<typename T>bool BST<T>::remove(const T& data) {BinNodePtr& x = search(data);//接收返回的值,这里用引用if (x == nullptr)//若没找到return false;//返回falseremoveAt(x, _hot);//删除找到的节点this->_size--;//更新规模this->updateHeightAbove(_hot);//更新高度return true;} }//namespace mytree四、BST测试代码~
在此处,就会利用到再本系列文章第一部分中的遍历函数,以及利用仿函数来实现遍历。
#include<iostream> #include "BinNode.h" #include "BST.h" using namespace std; using namespace mytree;template<typename BinNodePtr> void visitprint(BinNodePtr x) {cout << x->_data; }int main() {BST<int>* BT = new BST<int>;//cout << BT.size() << endl;//cout << BT.empty() << endl;BT->insert(3);BT->insert(6);BT->insert(2);BT->insert(4);BT->insert(5);BT->insert(1);BT->insert(7);BT->insert(8);BT->remove(7);BT->remove(6);BT->remove(3);cout << endl;BT->travLevel(visitprint<BinNode<int>*>);cout << endl;BT->travPre(visitprint<BinNode<int>*>);cout << endl;BT->travIn(visitprint<BinNode<int>*>);cout << endl;BT->travPost(visitprint<BinNode<int>*>);cout << endl;delete BT;return 0; } 结果为 42815 42185 12458 12584
动图演示为
五、BST的缺点~
平均BST树的高度为log2n,即平均复杂度为log2n。而BST在高度上,并没有限制,因此可能出现这种情况。
这样,要找到6这个节点,就需要把整棵树都遍历一遍,因此最坏复杂度为O(n)。这样BST相对有序的vector而言,就没有任何的优势。
而这,也恰恰是我们要引入AVL和RedBlack的原因!
五、后序文章链接~
学一个东西,不知道其道理,不高明!
总结
以上是生活随笔为你收集整理的真c++ 从二叉树到红黑树(3)之二叉搜索树BST的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 关闭360WIFI登录认证
- 下一篇: 基于C++的绘图板