9.4 二叉查找树(BST)
9.4.1 二叉查找树的定义
二叉查找树是一种特殊的二叉树,又称为排序二叉树、二叉搜索树、二叉排序树。二叉查找树的递归定义:
- 要么二叉查找树是一颗空树
- 要么二叉查找树由根结点、左子树和右子树组成,其中左子树和右子树都是二叉查找树,且左子树上所有结点的数据域均小于根结点数据域,右子树上所有节点数据域均大于根结点数据域。
9.4.2 二叉查找树基本操作
1.查找
查找将会是从树根到查找结点的一条路径,故最坏复杂度是O(h),其中h是二叉查找树的高度。
- 如果当前根节点root为NULL , 说明查找失败,返回
- 如果需要查找的值x等于当前节点数据域root->data,说明查找成功,访问之。
- 如果需要查找的值x小于当前结点数据域root->data,说明应该往左子树查找,因此向root->lchild递归。
- 如果需要查找的值x大于当前结点数据域root->data ,说明应该往右子树查找,因此向root->rchild递归。
void search( Node* root , int x ){
if( root == NULL ){ //空树查找失败
return ;
}
if( root->data == x ){ //查找成功,访问之
cout << root->data << endl ;
}
else if( root->data > x ){ //x比当前节点小
search( root->lchild , x ) ; //往左子树递归
}
else{
search( root->rchild , x ) ;
}
}
2. 插入
对于一棵二叉查找树来说,查找某个数据域的结点一定是沿着确定的路径进行的。因此当对某个需要查找的值在二叉查找树中查找成功,说明结点已经存在;反之,如果这个需要查找的值在二叉查找树中查找失败,那么说明查找失败的地方一定是结点需要插入的地方。因此可以在上面查找的基础上,在root==NULL时新建需要插入的结点。显然插入的时间复杂度也是O(h),其中h为二叉查找树的高度。
//insert函数将在二叉树中插入一个数据域为x的新节点(注意参数root要加引用&)
void insert( Node* &root , int x ){
if( root == NULL ){
root = newNode(x) ; //新建结点,权值为x(见9.1.3)
return ;
}
if( root->data == x ){ //查找成功,说明结点已经存在
return ;
}
else if( root->data > x ){ //x比当前节点小
insert( root->lchild , x ) ; //往左子树递归
}
else{
insert( root->rchild , x ) ;
}
}
3. 二叉查找树的建立
建立一颗二叉查找树,就是先后插入n个结点的过程,这和一般二叉树的建立完全是一样的。
//二叉查找树的建立
Node* Create( int data[] , int n ){
Node* root = new Node ;
for( int i = 0 ; i < n ; i++ ){
insert(root , data[i]) ;
}
return root ;
}
4. 二叉查找树的删除
二叉查找树的删除操作一般有两种常见做法,复杂度都是O(h),h是二叉树的高度。
如果需要删掉根结点5,应该怎么做呢?为了保证删除操作之后仍然是一棵二叉查找树,一种办法是以树中比5小的最大结点(也就是结点4)覆盖结点5,然后删除原来的结点4;另一种办法是把树中比5大的最小结点(也就是结点6)覆盖结点5,然后删除原来的结点6。这两种做法都能保证删除操作之后仍然是一棵二叉查找树。
把以二叉查找树中比结点权值小的最大结点称为该结点的前驱,而把比结点权值大的最小结点称为该结点的后继。显然,结点的前驱是该结点左子树中的最右结点(也就是从左子树根结点开始不断沿着rchild往下直到rchild为NULL时的结点),而结点的后继则是该结点右子树中的最左结点(也就是从右子树根结点开始不断沿着lchild往下直到Ichild为NULL时的结点)。下面两个函数用来寻找以root为根的树中最大或最小权值的结点,用以辅助寻找结点的前驱和后继:
//寻找root为根节点的树中的最大权值结点
node* findMax( node* root ){
while( root->rchild != NULL ){
root = root->rchild ;
}
return root ;
}
//寻找root为根节点的树中的最小权值结点
node* findMin( node* root){
while( root->lchild != NULL ){
root = root->lchild ;
}
return root ;
}
假设决定用结点N的前驱P来替换N,于是就把问题转换为在N的左子树中删除结点P,就可以递归下去了,直到递归到一个叶子结点,就可以直接把它删除了。
因此删除操作的基本思路如下:
- 如果当前结点root为空,说明不存在权值为给定权值x的结点,直接返回。
- 如果当前结点root的权值恰为给定的权值x,说明找到了想要删除的结点,此时进入删除处理。
a)如果当前结点root不存在左右孩子,说明是叶子结点,直接删除。
b)如果当前结点root存在左孩子,那么在左子树中寻找结点前驱pre,然后让pre的数据覆盖root,接着在左子树中删除结点pre.
c)如果当前结点root存在右孩子,那么在右子树中寻找结点后继next,然后让next的数据覆盖root,接着在右子树中删除结点next。 - 如果当前结点root的权值大于给定的权值x,则在左子树中递归删除权值为x的结点。
- 如果当前结点root的权值小于给定的权值x,则在右子树中递归删除权值为x的结点。
//删除以root为根结点的树中权值为x的结点
void deleteNode( node* &root , int x ){
if( root == NULL ){ //不存在x结点,直接返回
return ;
}
if( root->data == x ){
if( root->lchild == NULL && root->rchild == NULL ){ //当前节点为叶子结点,直接删除
root = NULL ; //把root地址改为NULL,父节点引用不到了
}
else if( root->lchild != NULL ){
node* pre = findMax( root->lchild ) ; //找root前驱
root->data = pre->data ;
deleteNode( root->lchild , pre->data ) ; //在左子树删除pre
}
else{
node* next = findMin( root->rchild ) ;
root->data = next->data ;
deletNode( root->rchild , next->data ) ;
}
}
else if( root->data > x ){
deleteNode( root->lchid , x ) ;
}
else{
deleteNode( root->rchild , x ) ;
}
}
但是也要注意,总是优先删除前驱(或者后继)容易导致树的左右子树高度极度不平衡,使得二叉查找树退化成一条链。解决这一问题的办法有两种:一种是每次交替删除前驱或后继;另一种是记录子树高度,总是优先在高度较高的一棵子树里删除结点。
9.4.3 二叉查找树的性质
二叉查找树的中序遍历序列是有序的。