一、概念
搜索树支持许多动态集合操作,包括SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERT和DELETE等,因此,一棵搜索树既可以作为一个字典又可以作为一个优先队列。
二叉搜索树上的基本操作所费时间与这棵树的高度成正比,对于有N个结点的一棵完全二叉树,这些操作的最坏运行时间为O(logN),但对于N个结点构成的线性链而言,同样的操作要花费O(N)的最坏运行时间,幸运的是一棵随机构造的二叉搜索树的期望高度是O(logN),所以这样一棵树上的动态集合的基本操作平均运行时间为O(logN)。
实际上,我们并不能总是保证随机的构建二叉搜索树,却可以设计它的变体,来保证基本操作具有好的最坏情况性能。红黑树、B树就是这样的例子。其中B树特别适用于二级(磁盘)存储器上的数据库维护。
什么是二叉搜索树?
二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:
(1)、若它的左子树不为空,则左子树上所有结点的值均小于等于它的根结点的值;
(2)、若它的右子树不为空,则右子树上所有结点的值均大于等于它的根结点的值;
(3)、它的左、右子树也分别为二叉查找树。
组织形式为:每个结点就是一个对象,除了key和卫星数据外,每个结点还包括left、right和parent。有木有感觉很熟悉?中序遍历其实就是对数据排序结果!所以二叉搜索树又可以用来排序。
为什么需要二叉搜索树?
目的并非为了排序,而是为了提高查找和删除关键字的速度,不管怎么说,在一个有序的数据集上查找,速度总是要快于无序的数据集。
什么是卫星数据?
在算法导论里,指的是一条纪录(一个对象中)中除了关键字key以外的其他数据。例如在排序算法中,参与排序的数据称做关键字key,而该对象附带的其他数据则称做卫星数据。在排序的过程中,我们只考虑关键字key的大小。形象一点说,其他数据可以看作是关键字key的卫星,反映了其他数据与key的依属关系。
操作与性能
假设数据结构为BSTree(二叉树)中内含BSNode(树节点)数据结构,且有成员treeRoot作为这棵树的根节点。
1.遍历:
对于具有n个结点的子树的根x,从x遍历一次需要O(n)时间,恰好调用自己两次,一次是左孩子,一次是右孩子。
/*
中序遍历 伪代码
*/
void InOrder(BSNode *pRoot)
{
if(pRoot){
InOrder(pRoot->LChild);
cout<<pRoot->data<<endl;
InOrder(pRoot->RChild);
}
}
2.查找:
从树根开始向下寻找,遇到节点x,比较关键字k与x.data,如果k>x.data说明k如果存在的话一定在x的左子树,否则在右子树。这样递归下去直到叶子节点,如果到达叶子节点都没找到这样的关键字为k的节点,说明找不到。运行时间为O(h),h为这棵树的高度。
/*
查找 伪代码
输入一个指向树根的指针pRoot与关键字k,
如果存在返回指向该节点的指针,不存在返回NUll
*/
void TreeSearch(<span style="font-family: Arial, Helvetica, sans-serif;">BSNode </span>*pRoot , int k)//递归版本
{
if(pRoot==NULL && pRoot->data==k)
return pRoot;
if(pRoot->data < k)
return TreeSearch(pRoot->LChild , k);
else
return TreeSearch(pRoot->RChild , k);
}
void TreeSearch(BSNode *pRoot , int k)//遍历版本
{
while(pRoot && pRoot->data!=k){
if(pRoot->data < k)
pRoot = pRoot->LChild;
else
pRoot = pRoot->RChild;
}
return pRoot;
}
3.最大关键字元素和最小关键字元素:时间复杂度O(h)
最大关键字元素:通过树根找最左边的那个元素,如果无左孩子,则为根节点最大;
最小关键字元素:通过树根找最右边的那个元素,如果无右孩子,则为根节点最小;
/*
最大最小关键字元素 伪代码
*/
BSTree* TreeMin(BSNode *pRoot)
{
while(pRoot->LChild)
pRoot = pRoot->LChild;
return pRoot;
}
BSTree* TreeMax(BSNode *pRoot)
{
while(pRoot->RChild)
pRoot = pRoot->RChild;
return pRoot;
}
4.后继和前驱,时间复杂度O(h)。
/*
查找后继 伪代码
*/
BSTree* TreeSUCCESSOR(BSNode *x)
{
if(x->RChild != NULL)//有右子树,则直接定位到右子树最小关键字节点
return TreeMin(x->RChild);
//---没有右子树,沿树向上搜索到一个有左孩子的节点y,y左子树中包含x(这样x后继是y)
BSTree* xParent = x->parent;
while(xParent!=NULL && x==xParent->RChild){
x = xParent;
xParent = xParent->parent;
}
return xParent;
}
5.插入,沿着根节点向下寻找合适的位置,如果欲插入关键值key>pNode.data,沿着右子树向下,key<=pNode.data,沿着左子树向下;直到叶子节点的某个NULL孩子处插入。时间复杂度为O(h)。
/*
插入节点 伪代码
在树T中插入节点x,注意到此时不能只传入pRoot,而需要传入T.root
*/
void TreeInsert(BSTree T , BSNode *x)
{
BSNode *pRoot = T.root;
BSTree *pPre = NULL;//存放pRoot的父节点
while(pRoot!=NULL){
pPre = pRoot;
if(pRoot->data > x->data)
pRoot = pRoot->LChild;
else
pRoot = pRoot->RChild;
}
x->parent = pPre;
if(pPre == NULL)//输入是空树
T.root = x;
else if(x->data < pPre->data)
pPre->LChild = x;
else
pPre->RChild = x;
}
6.删除,从一棵二叉树删除节点分为如下三种情况,时间复杂度O(h)。
设要删除的节点是x,注意到删除x并不会导致x的子树被删掉,且要保护好二叉搜索树的性质。于是:
(1)x是一个叶子节点,没有孩子,直接delete x,并置x.parent相应孩子域为NULL。
(2)x没有左孩子或没有右孩子,直接让唯一存在的那个孩子子树替代x;
(3)x既有左孩子又有右孩子,找到x的后继y,y一定是最小的大于x的那个数,可以知道y一定没有左子树(如果有的话一定能找到一个小于y的节点),这时可以直接让y替代x,并删掉y节点,由于y节点是没有左孩子的,属于(1)情况,可以使用(1)的方法删除。
/*
删除节点 伪代码
在树T中删除节点x,假设x存在于树T中。
不存在的情况可以加一个简单的搜索判定,此处略去
*/
//将u节点用v子树取代,并维护u,parent与v的父子关系
void transplant(BSTree T , BSNode *u , BSNode *v)
{
if(u->parent == NULL)//u是根节点,删掉根节点
T.root = v;
else if(u == u->parent->LChild)//u是父节点的左孩子
u->parent->LChild = v;
else//u是父节点的右孩子
u->parent->RChild = v;
if(v != NULL)
v->parent = u->parent;
}
void TreeDelete(BSTree T , BSNode *x)
{
if(x->LChild == NULL)//左子树为空
transplant(T , x , x->RChild);
else if(x->RChild == NULL)//右子树为空
transplant(T , x , x->LChild);
else{//左右子树均不为空
BSNode *xSucc = TreeMin(x);//找到x的后继
if(xSucc->parent != x){//x后继不是x的孩子
transplant(T , xSucc , xSucc->RChild);//此时xSucc一定没有左孩子,将后继的右孩子替代后继节点
//---置后继为原来x->RChild的双亲
xSucc->RChild = x->RChild;
x->RChild->parent = xSucc;
}
//---将x用后继替换,并置后继为x双亲的孩子和x左孩子的双亲。
transplant(T,x,xSucc);//其内已维护x.parent与x后继父子关系,只需关注x孩子与xSucc孩子关系
xSucc->LChild = x->LChild;
x->LChild->parent = xSucc;
}
delete x;
}
删除节点x一定是取x的后继吗?
不一定,取x的前驱也可以的。考虑下图:可以将47的后继48替换47,自然也能用37来替换它,只要不破坏二叉搜索树的性质就好。
随机的构建二叉树
据上所述,二叉搜索树每个基本操作都能在O(h)时间内完成,h为这棵树的高度。但是随着元素的插入和删除,二叉搜索树的高度是变化的,如果n个关键字按照严格递增次序插入,则这棵树一定是高度为n-1的一条链。幸运的是平均性能接近于最好情形而非最坏情形性能。
一棵有N个不同关键字的随机构建二叉搜索树的期望高度为O(logN)。
虽然如此,我们还是希望能让这棵二叉排序树能平衡就好了,这样引出了新的问题:平衡二叉树(AVL树),不过这是下一篇文章的事儿了。
C++实现二叉查找树
1.注意在类的成员函数内如果删除this指针就不能再访问this的数据成员,分离BSTree与BSNode的实现可以将树的根节点所占内存释放。即BSTree中有一个BSNode类成员root作为本树的根节点。这样不需要删掉BSTree对象也能释放树申请的结点内存。
2.分离了实现后可以做到BSNode析构函数专门删除某个节点(BSTree类的deleteNode函数用到了)用delete Node调用;BSTree中定义一个destory函数专门用来删除以pRoot为根的所有子树并释放堆内存,可以在BSTree析构函数中调用destory函数释放所有BSNode申请的堆内存。
/*
二叉搜索树的实现:建立、插入、删除、查询和遍历
二叉搜索树是这样的:左子树节点<=根节点<=右子树节点
二叉树结构BSTree
二叉树节点结构BSNode
*/
struct BSNode{
BSNode():LChild(NULL),RChild(NULL),pParent(NULL){}
~BSNode(){//默认析构函数只删除一个节点
}
BSNode *LChild;
BSNode *RChild;
BSNode *pParent;
int data;
};
class BSTree{
private:
int printAtLevel(BSNode *pRoot , int k);//打印第k层的层序遍历结果
void printNodeInOrder(BSNode *pRoot);
void transplant(BSNode*u , BSNode *v);//用v子树替换u,维护u的父节点与新节点v的关系
void destory(BSNode **pRoot);//递归销毁以pRoot为根的子树
public:
BSTree():root(NULL){}
~BSTree();
void insertNode(int x);
bool deleteNode(BSNode* pNode);//删除x所在节点,成功返回true
BSNode* searchNode(int x);//查询x所在节点,如果查不到返回NULL
BSNode* MinKeyNode();//查询最小关键字节点
BSNode* MaxKeyNode();//查询最大关键字节点
BSNode* TreeSuccessor(BSNode *x);//得到某个节点的后继
void printInOrder();//中序遍历,从小到大输出排序结果
void printLevelOrder();//整棵树层序遍历,查看建立的二叉树情况
private:
BSNode *root;//保存本树的根节点
};
BSTree::~BSTree()
{
destory(&root);
}
void BSTree::destory(BSNode **pRoot)
{
if(*pRoot){
destory(&((*pRoot)->LChild));
destory(&((*pRoot)->RChild));
delete *pRoot;
*pRoot = NULL;
}
}
void BSTree::insertNode(int x)
{
BSNode *tempNode = new BSNode;
tempNode->data = x;
if(root == NULL){//空子树
root = tempNode;
return;
}
//---寻找插入的位置,x值比根节点值大走右子树,小走左子树,直到走到叶子节点的NULL处插入
//注意需要保存最终NULL的父节点,以便插入成为它的孩子节点
BSNode *pRoot = root;//根节点
BSNode *parent = root->pParent;
while (pRoot != NULL){
parent = pRoot;
if(pRoot->data > tempNode->data)//走左子树
pRoot = pRoot->LChild;
else
pRoot = pRoot->RChild;
}
if(parent->data > tempNode->data)
parent->LChild = tempNode;
else
parent->RChild = tempNode;
tempNode->pParent = parent;
}
/*删除一个节点,分两种情况:
(1)设要删除的结点是x,x有两个孩子的情况
step1:找到结点x的后继y,可知:y是x右子树中最左结点,且y没有左孩子(否则y左孩子为x的后继)
step2:把y的关键字给x,即让结点x成为结点y(移花接木)
step3:删除y,因为y没有左孩子,所以按照(2)的方法删除y
(2)设要删除的结点是y,y只有左孩子或右孩子的情况
直接置y的左孩子(或右孩子)的节点值(无论是否为NULL)为y的父节点相应孩子。
*/
void BSTree::transplant(BSNode*u , BSNode *v)
{
if(u->pParent == NULL){//u是根节点,直接删掉根节点
root = v;
delete u;
}
if(u->pParent->LChild == u)//u是父节点的左孩子
u->pParent->LChild = v;
else if(u->pParent->RChild == u)//u是父节点的右孩子
u->pParent->RChild = v;
if(v != NULL)
v->pParent = u->pParent;
}
bool BSTree::deleteNode(BSNode* pNode)
{
if(pNode == NULL)//删除空节点
return false;
if(pNode == root){//删除根节点
destory(&root);
return true;
}
//---处理两种情况
//没有左孩子或右孩子的情况
if(pNode->LChild == NULL)//只有右孩子
transplant(pNode , pNode->RChild);
else if(pNode->RChild == NULL)//如果只有左孩子
transplant(pNode , pNode->LChild);
else{//如果既有左孩子又有右孩子
BSNode *pNodeSucc = TreeSuccessor(pNode->RChild);//pNode的后继节点
if(pNodeSucc->pParent != pNode){
//则pNodeSucc一定没有左孩子,让pNodeSucc的右孩子替换pNodeSucc
transplant(pNodeSucc , pNodeSucc->RChild);
pNodeSucc->RChild = pNode->RChild;//维护pNode后继与pNode右孩子的父子关系
pNode->RChild->pParent = pNodeSucc;
}
//---维护pNode后继和pNode左孩子
transplant(pNode,pNodeSucc);
pNodeSucc->LChild = pNode->LChild;
pNode->LChild->pParent = pNodeSucc;
}
delete pNode;
pNode = NULL;
return true;
}
BSNode* BSTree::searchNode(int x)
{
BSNode *pRoot = root;
while(pRoot && x!=pRoot->data){
if(x > pRoot->data)
pRoot = pRoot->RChild;
else
pRoot = pRoot->LChild;
}
if(pRoot == NULL)//空树
return NULL;
else
return pRoot;
}
BSNode* BSTree::MinKeyNode()
{
BSNode *pRoot = root;
while(pRoot->LChild != NULL)
pRoot = pRoot->LChild;
//如果没有左孩子,则返回根节点
return pRoot;
}
BSNode* BSTree::MaxKeyNode()
{
BSNode* pRoot = root;
while(pRoot->RChild != NULL)
pRoot = pRoot->RChild;
//如果没有右孩子,则返回根节点
return pRoot;
}
BSNode* BSTree::TreeSuccessor(BSNode *x)
{
BSNode* pRoot = root;
if(pRoot == NULL)//空树
return NULL;
//---如果x节点有右孩子,后继就是右孩子
if(x->RChild)
return x->RChild;
//---如果x节点没有右孩子,则由x向上查找,直到找到第一个左子树含有x的节点
BSNode *parent = x->pParent;
while(parent!=NULL && parent->RChild==x){
x = parent;
parent = parent->pParent;
}
return parent;//返回NULL或找到的后继(最后一个元素--根节点没有后继)
}
void BSTree::printInOrder()
{
BSNode *pRoot = root;
printNodeInOrder(pRoot);
}
void BSTree::printNodeInOrder(BSNode *pRoot)
{
if(pRoot){
printNodeInOrder(pRoot->LChild);
cout<<pRoot->data<<" ";
printNodeInOrder(pRoot->RChild);
}
}
//思想:打印从根节点开始的第k层相当于打印根节点左右孩子开始的k-1层
int BSTree::printAtLevel(BSNode *pRoot , int k)
{
if(pRoot == NULL || k<0)
return 0;//未打印节点
if(k == 0){
cout<<pRoot->data<<" ";
return 1;//打印一个节点
}
//先打印左子树,返回左子树打印的节点数
int LeftNodeNum = printAtLevel(pRoot->LChild , k-1);
//再打印右子树,返回右子树打印的节点数
int RightNodeNum = printAtLevel(pRoot->RChild , k-1);
return LeftNodeNum+RightNodeNum;
}
void BSTree::printLevelOrder()
{
BSNode *pRoot = root;
if(pRoot == NULL)
cout<<"空树!"<<endl;
for(int k=0 ; ;k++){
if(0 == printAtLevel(pRoot , k))//整层都未打印节点,返回
break;
cout<<endl;
}
}