二叉排序树(BST)
定义
前面学过的查找方法中,我们发现折半查找的效率比较高,结合折半查找的思想,我们定义了符合如下的条件的二叉树结构:
①可以为空树;
②如果根结点有左子树,其左子树上的任何结点都要比根结点的值小;
③如果根结点有右子树,其右子树上的任何结点都要比根结点的值大;
④根结点的左右子树也要满足以上三个条件。
如图111所示,此二叉树为一棵二叉排序树。二叉排序树又称为二叉搜索树,或二叉查找树。
图111
根据构造条件可以发现,如果我们对一棵二叉排序树进行中序遍历,可以得到一个所有结点按照增序的序列,将上述这棵二叉排序树进行中序遍历,得到的序列为 2,3,4,5,6,8,9。
二叉排序树的操作
查找
根据树的结构可知,不为空的二叉排序树的查找首先从根结点比较:
①若给定值比根结点关键字值小,则将根结点的左孩子结点与给定值比较,
②若给定值比根结点关键字值大,则将根结点的右孩子结点与给定值比较,
③若结点关键字值与其相等,则查找成功,继续递归下去,直到查找成功,或到达空指针,即查找失败。
比如在图111所示的二叉排序树中,查找结点值为“8”的结点,首先跟根结点比较,比根结点大,继续比较右子树,8<9继续跟值为“9”的结点的左孩子结点“6”比较,8>6则跟结点“6”的右孩子结点比较,给定值“8”与结点的值相等,查找成功,比较了4次。
如果查找值为”10“的结点,则先跟根结点比较,10>5 则与根结点的右子树”9“比较,10>9跟值为”9“的结点的右孩子结点比较,右孩子为空,则可知这棵树中没有比”9“更大的结点,即查找失败,此次查找比较了两次。
使用递归实现:
BiTree Search_BST(BiTree T,KeyType key){
//递归的出口,递归的过程T为null,则查找失败,返回空;
//若查找到,则返回指向改关键字的指针
if(T==null){
return null;
}else if(key==T->data){
return T;
}else if(key<T->data){
//key值小于当前结点,遍历左子树
Search_BST(T->lchild,key);
else if(key>T->data){
//key值大于当前结点,遍历右子树
Search_BST(T->rchild,key)
}
}
使用非递归方式实现:
BiTree Search_BST(BiTree T,KeyType key){
// 如果树为空,则结束循环;如果查找到,也结束循环
while(T!=null && T->data!=key){
// key值小于当前结点值,则在左子树上查找;
if(key<T->data) T=T->lchild;
// key值大于当前结点值,则在右子树上查找;
else if(key>T->data) T=T->rchild;
}
return T;
}
查找效率分析
从图111中查找“6”和“10”的过程我们可以发现,分别查找了3次和2次,可知查找的次数与结点所在树的深度有关系,比如结点“8”在深度3,则需要比较4次,结点“10”若存在,则位于深度2,需要比较3次,即深度加一;所以查找所有结点来看,树的深度(高度)越低,比较的次数越少。
在结点数目给定的情况下,则有两种极端情况,一是树的高度最低,二是最高; 树的高度最低的情况,如图222-1,类似于完全二叉树,并且结点排列符合排序二叉树的要求,此时平均查找长度 O ( l o g 2 n ) O(log_2 n) O(log2n),此时的平均查找长度推导可以参考平衡二叉树的内容;另外一种极端情况是每层只有一个结点的二叉排序树,如图222-2,其平均查找长度与单链表类似,为 O ( n ) O(n) O(n) 。
所以,二叉排序树的查询复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)~ O ( n ) O(n) O(n)
在每个结点被查找概率相同的情况下,图222-1所示,其平均查找长度为:
A S L a = ( 1 + 2 × 2 + 3 × 4 + 4 × 4 ) / 11 = 3 ASL_a= (1+2\times2+3\times4+4\times4)/11=3 ASLa=(1+2×2+3×4+4×4)/11=3
图222-2所示的树,其平均查找长度为:
A S L a = ( 1 + 2 + . . . + 11 ) / 11 = 6 ASL_a=(1+2+...+11)/11=6 ASLa=(1+2+...+11)/11=6
结点插入
二叉排序树其实是动态查找表的一种表现形式,在构造树或是在查找对象不在树中的时候,我们需要将我们需要将对象放入到树中。
插入的过程与动态查找表类似,若树是一棵空树,则直接插入,若插入的对象关键字比根结点小,则插入到左子树,否则插入到右子树;依次递归进行,直到插入成功;插入的对象一定是树中不存在的,即在树中为一新的结点,并且此节点一定是叶子结点。
其插入过程与查找过程类似,所以插入结点的位置是在查找路径最后一个结点的左右孩子结点处。如图444所示,在树中插入关键字“28”,查找的路径为21—>34—>27,查找失败;插入的路径为21—>34—>27,最后插入27的右孩子结点。
二叉排序树插入算法描述:
bool Insert_BST(BiTree T,KeyType key){
//如果原树为空,则直接插入
if(T==null){
//分配内存空间,赋值,并将孩子结点置空
T=(BiTree)malloc(sizeof(BiTree));
T->data=key;
T->lchild=null;
T->rchild=null;
//递归出口,插入成功
return true;
}else if(T->data==key){
//插入的关键字在树中存在,则插入失败
return false;
}else if(T->data<key){
//插入关键字比当前结点大,则插入右子树
return Insert_BST(T->rchild,key);
}else if(T->data>key){
//插入关键字比当前结点小,则插入左子树
return Insert_BST(T->lchild,key);
}
二叉排序树的构造
按照输入元素的顺序,按照二叉排序树的要求将元素插入到树中合适的位置,便能构造出一棵二叉排序树;现在输入的元素序列为{34,45,65,76,23,67,15,79},则构造二叉排序树的步骤如图333所示。
构造排序算法的描述:
void build_BST(BiTree &T,KeyType key[],int n){
//构造前,T应为空树
T=null;
//按照输入顺序插入树中;
for(int i =0;i<n;i++){
Insert_BST(T,key[i]);
}
}
结点删除
二叉排序树的增、删、改、查中,已经将增和查说明清楚了,改的方法就是先查找到关键字,再对关键字修改即可,现在来介绍二叉排序树的删除。根据树的性质,如果要删除一个结点,势必会影响到树的结构,要保证删除结点后这棵树还是一棵二叉排序树,则需要考虑其父亲结点和孩子结点的连接问题。我们根据结点的位置,将删除结点的操作分为了三种情况(参考图555):
①删除的结点位于叶子结点处,删除不会影响二叉排序树的性质,因此可以直接删除,如图555-1,删除68的结点,则直接删除;
②删除的结点有一棵子树,则可以让其子树代替其位置,如图555-2,删除58的结点,可以直接将其孩子结点68代替其位置;
③删除的结点有两颗子树,这里介绍两种删除操作方法:
(1)如图555-3中,删除77结点;直接将结点的左孩子结点代替要删除的结点,图中直接将58结点替换77结点;然后将中序遍历中所要删除结点的直接前驱结点作为替换后的结点右孩子结点,图中77的直接前驱结点为68,所以将68作为58的右孩子结点;最后将要删除结点的右孩子结点加入树中,图中将93结点加入68的右子树中。
(2)或者我们直接用删除结点的直接前驱(直接后继)来替代,并删除此结点,如图555-4所示,删除77结点,直接使用其前驱结点68代替,并将68删除,删除的结点一定是满足①或②的条件,删除68结点,其符合②情况,直接让其子树代替其位置。
二叉排序树删除操作的算法描述如下,对考生不做要求,理解即可:
bool delete_Element_BST(BiTree *T){
//T不为空
//如果T结点为叶子结点,则可直接删除
BiTree q;
if((*T)->lchild==null&&(*T)->rchild==null){
*T=null;
}else if((*T)->rchild==null){
//右子树为空,左子树不为空,则直接将左孩子结点代替T结点
q=*T;
*T=(*T)->lchild;
free(q);
}else if((*T)->lchild==null){
//左子树为空,右子树不为空,则直接将右孩子结点代替T结点
q=*T;
*T=(*T)->rchild;
free(q);
}else{
//左右子树都不为空的时候,选择第二种处理方法
q=*T;
BiTree s=(*T)->lchild;
//找到T的前驱结点s,q指向s的父亲结点
while(s->rchild!=null){
q=s;
s=s->rchild;
}
//找到前驱结点后更改T中的值
(*T)->data=s->data;
//判断是否需要对前驱结点s的子树进行修改
if(q==*T){
//T结左孩子结点s没有右子树的情况,则直接将s的左子树连接到T的左子树上,即删除s
(*T)->lchild=s->lchild;
}else{
//前驱结点s删除的同时,处理s的子树
q->rchild=s->lchild;
}
return true;
}
另外,注意我们对一棵二叉排序树先进行删除某一节点操作后,再将这结点插入二叉树中,二叉树的结构是可能会发生变化的。比如图555-4所示,删除结点77后,再次将77插入,最后得到如图所示二叉排序树,其结构发生了变化。