此文章为从二叉树到红黑树系列文章的第三节,主要介绍介绍二叉搜索树BST,为AVL和RedBlack打下基础
在阅读本文前,强烈建议你看下前面的文章的目录、前言以及基本介绍,否则你无法理解后面的内容。链接如下:
确认一颗树的中序遍历序列十分容易。即将树中每一个元素都投影到底部就可以了。
当一颗树的中序遍历序列是单调非减的时候,那么这颗树就必为一颗BST,即二叉搜索树。
更准确一点的定义为:根节点的值大于其左子树中任意一个节点的值,小于其右节点中任意一节点的值。其左子树和右子树也都满足这个条件。
二叉搜索树,又称二叉排序树,二叉查找树。
既然二叉搜索树属于二叉树的特例,故自然可以基于BinTree模板类(见此系列文章第二部分)
由于BST继承于普通二叉树。而普通二叉树的大部分函数和变量都是属于public或者protected的。自然BST可以沿用这些函数和变量。
并由于BST本身特殊的性质(单调非减),因此需要对某些函数进行重写(override)。以及扩增一些新的功能。
在此,为了方便性起见,我们默认BST中没有重复的数(读者可以自行更改search接口的判断条件来进行扩充)。
_hot节点,此节点的设定至关重要,有了这个节点,就能简化很多步骤,方便BST包括后序的AVL,RedBlack的插入和删除。读者看到后面的插入和删除算法的实现的时候,自然能够体会到这个节点的作用。
BinNodePtr _hot;//"命中节点"的"父亲"
由于BST是继承于BinTree的,因此,很多接口都可以沿用BinTree的接口,比如遍历,判空,获取当前高度等等。但依然有些接口是需要进行扩充和重写的。
比如:
查找树中有没有这个节点search
在树中插入一个节点insert
在树中删除一个节点remove
对于BST本身,有了上面的查找,插入和删除,已经可以满足BST本身的要求了,但是对于后序的AVL和RedBlack而言,还远远不够,因此在此处会引入两个重要的辅助函数,并且我会在介绍AVL的时候,详细说明这两个辅助函数的作用。在介绍RedBlack的时候,也会引入一个指向AVL的链接。
关于下面两个函数的实现,我不会在本节进行解析,我会在AVL中再进行解析。
3+4重构 connect34
对该节点及其父亲、祖父做统一旋转调整rotateAt
由于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,RedBlack
BinNode<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
从树根出发,逐步地缩小查找范围,直到发现目标(成功)或缩小至空树(失败)。由于BST的中序遍历是有序的,所以可以用二分查找法。
找22时,先与根节点比较大小,其比根节点16大,则进入右子树查找。下一次与25相比,22比25小,所以进入左子树查找。然后与19对比,自然进入右分支,最后正好命中22这个节点。即查找成功。
找23时,也会经过这样一条道路,由于22的右子树为空,所以查找失败。
若查找成功,则search()的返回值都将如下图(a)所示,指向一个数值为e且真实存在的节点;
若查找失败,则返回值的数值虽然为NULL,但是它作为引用将如下图(b)所示,指向最后一次试图转向的空节点。
由上图可以得知,无论查找成功或者失败,查找的返回值都是等效地指向“命中节点”,而命中节点的父亲节点就是_hot节点。有了_hot节点,对于插入和删除,都会变得相对容易一点。
众所周知,二分查找可以写成递归或者迭代形式。并且幸运的是,对于二叉树而言,其二分查找的算法,与一般数组的查找算法相比,本质上没有任何区别,甚至可以说比一般的二分查找算法更加简单一点。
由于递归算法需要不断地调用自身,并且为了保持与非递归算法的接口传入参数的一致性,所以不妨将递归的部分与不需要递归的部分分离开来。
下面是主要查找函数的接口。当需要查找一个数据时,就从根节点开始,进行二分递归查找。并同时更新_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 mybst
比起分开写的递归算法,笔者认为迭代版的查找算法的可读性相对更高一点,并且效率也更高一点。同样,也要注意返回的是引用。
后面在插入和查找过程中,都默认用的是迭代版算法。
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;//更新_hot
while (true){
//current的地址与_hot的孩子绑定,不能与_hot绑定。
BinNodePtr& current = ((data < _hot->_data) ? _hot->_lchild : _hot->_rchild);
if (!current || data == current->_data)
return current;
_hot = current;//更新_hot位置
}
}
在二叉搜索树的每一层,查找算法至多访问一个节点,且只需常数时间,故总体所需时间应线性正比于查找路径的长度,或最终返回节点的深度,在最坏情况下不超过全树的高度。
对于BST而言,无论是插入还是删除而言,先前都必将要经过查找操作。对于在此系列定义的BST而言,里面存放的都是不可重复的数据。因此,对于插入而言,如果这个元素存在BST中,则不插入,如果在BST中不存在这个元素,就在适合的位置插入这个元素。
在此,_hot节点的作用就体现出来了,如果不存在这个元素,_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;
}
请读者好好体会这段简短但不简单的代码所表达的意思。特别是引用的用处。
并且插入后也要更新相应的节点高度,以及其祖先节点的高度。关于高度更新的算法实现,请见本系列文章第二部分。
节点插入操作所需的时间,主要消耗于对算法search()及updateHeightAbove()的调用。后者与前者一样,在每一层次至多涉及一个节点,仅消耗O(1)时间,故其时间复杂度也同样取决于新节点的深度,在最坏情况下不超过全树的高度。
理解BST的删除算法,可以说是BST最重要的一点,弄懂了BST的删除,那么你就懂了BST,接下来介绍一个最重要的删除静态函数,这个函数不仅对于BST有用,对于AVL,RedBlack都有用。
删除一颗BST的节点,并且保持BST的性质,需要分以下3种大情况来考虑。
在此,需要借用在本系列文章第一部分提到的关于求当前节点的直接后继算法。建议看完这个算法的实现,再来理解这种情况,否则你会感到迷惑。
首先要在心里明确一个事实:即直接后继,必然没有左孩子。
在这种情况下,可以得知,该右孩子必然没有左孩子(可能有右孩子),不然其右孩子不可能作为其直接后继。此时,就将该节点与其后继(也就是右孩子)的值进行交换(看下面代码你就能明白交换比直接删这个节点更好)。再重新连接指针即可。
在这种情况下,可以得知,该节点的直接后继必然是左孩子。此时,就将该节点与其后继(也就是右孩子)的值进行交换。再重新连接指针即可。
此代码为删除算法的难点,重点,理解好此代码,你才能理解进一步的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;
}
理解了上面的代码,就能很容易理解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;//返回false
removeAt(x, _hot);//删除找到的节点
this->_size--;//更新规模
this->updateHeightAbove(_hot);//更新高度
return true;
}
删除操作所需的时间,主要消耗于对search()、succ()和updateHeightAbove()的调用。在树中的任一高度,它们至多消耗O(1)时间。故总体的渐进时间复杂度,亦不超过全树的高度。
注意,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,RedBlack
BinNode<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
template<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)) {//L
if (IsLChild(v)) {//LL
p->_parent = g->_parent;//向上连接
return connect34(v, p, g, v->_lchild, v->_rchild, p->_rchild, g->_rchild);
}
else {//LR
v->_parent = g->_parent;//向上连接
return connect34(p, v, g, p->_lchild, v->_lchild, v->_rchild, g->_rchild);
}
}
else {//R
if (IsRChild(v)) {//RR
p->_parent = g->_parent;//向上连接
return connect34(g, p, v, g->_lchild, p->_lchild, v->_lchild, v->_rchild);
}
else {//RL
v->_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 mybst
template<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;//更新_hot
while (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;//返回false
removeAt(x, _hot);//删除找到的节点
this->_size--;//更新规模
this->updateHeightAbove(_hot);//更新高度
return true;
}
}//namespace mytree
在此处,就会利用到再本系列文章第一部分中的遍历函数,以及利用仿函数来实现遍历。
#include
#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树的高度为log2n,即平均复杂度为log2n。而BST在高度上,并没有限制,因此可能出现这种情况。
这样,要找到6这个节点,就需要把整棵树都遍历一遍,因此最坏复杂度为O(n)。这样BST相对有序的vector而言,就没有任何的优势。
而这,也恰恰是我们要引入AVL和RedBlack的原因!
学一个东西,不知道其道理,不高明!