【定义】
二叉排序树 (Binary Sort Tree或Binary Search Tree) 的定义为:二叉排序树或者是空树,或者是满足下列性质的二叉树。
- 若左子树不为空,则左子树上所有结点的值(关键字)都小于根结点的值;
- 若右子树不为空,则右子树上所有结点的值(关键字)都大于根结点的值;
- 左、右子树都分别是二叉排序树。
如此定义的二叉排序树中,各结点关键字是唯一的。但在实际应用中,不能保证被查找的数据集中各元素的关键字互不相同,所以可将定义1中的“小于”改为“小于等于”,或将定义2中的“大于”改为“大于等于”。
结论:若按中序遍历一棵二叉排序树,所得到的结点序列是一个递增序列。
BST仍然可以用二叉链表来存储,如下所示:
typedef int KeyType; //关键字类型
/* 二叉排序树存储表示 */
typedef struct BSTNode
{
KeyType key; //关键字域
//......... //其他数据域
struct BSTNode* lchild;
struct BSTNode* rchild;
}BSTNode; //二叉排序树结点
typedef BSTNode* BSTree; //指向二叉排序树结点的指针
【查找】
首先将给定的K
值与二叉排序树的根结点的关键字进行比较:
- 若相等: 则查找成功;
- 若给定的
K
值小于BST的根结点的关键字:继续在该结点的左子树上进行查找; - 若给定的
K
值大于BST的根结点的关键字:继续在该结点的右子树上进行查找。
递归+非递归算法实现,见最后代码SearchBST。
在随机情况下,二叉排序树的平均查找长度ASL和树的深度log(n)
是等数量级的。
【插入】
在BST树中插入一个新结点,要保证插入后仍满足BST的性质。
在BST树中插入一个新结点x
时,若BST树为空,则令新结点x
为插入后BST树的根结点;否则,将结点x
的关键字与根结点T
的关键字进行比较:
- 若相等: 不需要插入;
- 若
x.key < T->key
:结点x
插入到T
的左子树中; - 若
x.key > T->key
:结点x
插入到T
的右子树中。
递归+非递归算法实现,见最后代码InsertBST。
由上述算法(尤其是非递归算法)可知,每次插入的新结点都是BST树的叶子结点,即在插入时不必移动其它结点,仅需修改某个结点的指针。
【建立】
利用BST树的插入操作,可以从空树开始逐个插入每个结点,从而建立一棵BST树。
见最后代码实现CreateBSTree
需要注意的是,即便是一组相同的数字,如果插入它们的顺序不同,最后生成的二叉查找树也可能不同。如下图所示,先后插入 {5, 3, 7, 4, 2, 8, 6}
与 {7, 4, 5, 8, 2, 6, 3}
之后可以得到两棵不同的二叉查找树。
【删除】
从BST树上删除一个结点,仍然要保证删除后满足BST的性质。
设被删除结点为p
,其父结点为f
,删除情况如下:
-
若
p
是叶子结点: 直接删除p
,如下图b所示。 -
若
p
只有一棵子树(左子树或右子树):直接用p
的左子树(或右子树)取代p
的位置而成为f
的一棵子树。即原来p
是f
的左子树,则p
的子树成为f
的左子树;原来p
是f
的右子树,则p
的子树成为f
的右子树,如下图c、d所示。 -
若
p
既有左子树又有右子树 :处理方法有以下两种,可以任选其中一种。- 用
p
的直接前驱结点代替p
。即从p
的左子树中选择值最大的结点s
放在p
的位置 (用结点s
的内容替换结点p
内容),然后删除结点s
。其中s
是p
的左子树中的最右边的结点且没有右子树,对s
的删除同②,如下图e所示。 - 用
p
的直接后继结点代替p
。即从p
的右子树中选择值最小的结点s
放在p
的位置 (用结点s
的内容替换结点p
内容),然后删除结点s
。其中s
是p
的右子树中的最左边的结点且没有左子树,对s
的删除同②。
- 用
见最后代码实现DeleteBST
【算法实现】
(代码实现用的二级指针传参。如果二级指针不熟,使用指针的引用更方便;如果指针和引用都不会,emmmmm)
二叉排序树的完整代码如下:
#include <stdio.h>
#include <stdlib.h>
#define EQ(a,b) ((a)==(b))
#define LT(a,b) ((a) < (b))
typedef int KeyType; //关键字类型
/* 二叉排序树存储表示 */
typedef struct BSTNode
{
KeyType key; //关键字域
//......... //其他数据域
struct BSTNode* lchild;
struct BSTNode* rchild;
}BSTNode; //二叉排序树结点
typedef BSTNode* BSTree; //指向二叉排序树结点的指针
//1.递归查找,课本228页算法9.5(a),返回指向key的指针,若不存在返回MULL。
BSTNode* SearchBST1(BSTree T, KeyType key)
{
if(!T || EQ(T->key, key))
return T;
else if(LT(key, T->key))
return SearchBST1(T->lchild, key);
else
return SearchBST1(T->rchild, key);
}
//2.非递归查找,返回指向key的指针,若不存在返回MULL。
BSTNode* SearchBST2(BSTree T, KeyType key)
{
BSTNode *p = T;
while(p!=NULL && !EQ(p->key, key))
{
if(LT(key, p->key)) p = p->lchild;
else p = p->rchild;
}
if(EQ(p->key, key)) return p;
else return NULL;
}
//3.递归插入
void InsertBST1(BSTree *T, KeyType key)
{
if((*T)==NULL) //空树,也即插入位置
{
BSTNode *x = (BSTNode *)malloc(sizeof(BSTNode)); //新建结点x
x->key = key;
x->lchild = x->rchild = NULL;
(*T) = x;
return;
}
if(EQ((*T)->key, key)) //说明结点已存在,直接返回
return;
else if(LT(key, (*T)->key))
InsertBST1(&((*T)->lchild), key);
else
InsertBST1(&((*T)->rchild), key);
}
//4.非递归插入
void InsertBST2(BSTree *T, KeyType key)
{
BSTNode *x = (BSTNode *)malloc(sizeof(BSTNode)); //新建结点x
x->key = key;
x->lchild = x->rchild = NULL;
if((*T)==NULL) //空树,也即插入位置
{
(*T) = x;
return;
}
BSTNode *p = (*T), *q;
while(p!=NULL)
{
if(EQ(p->key, x->key)) return; //说明结点已存在,直接返回
q = p; //q记录p的父结点
if(LT(x->key, p->key)) p = p->lchild;
else p = p->rchild;
}
if(LT(x->key, q->key)) q->lchild = x;
else q->rchild = x;
}
//5.创建二叉排序树
void CreateBSTree(BSTree *T)
{
(*T) = NULL; //初始化根结点为空
int n;
printf("请输入数据元素的个数:");
scanf("%d", &n);
KeyType key;
printf("请依次输入数据元素的关键字:");
for(int i = 1; i <= n; i++)
{
scanf("%d", &key);
InsertBST1(T, key);
//InsertBST2(T, key);
}
}
//6.删除,在以T为根结点的BST树中删除关键字为key的结点
void DeleteBST(BSTree *T, KeyType key)
{
BSTNode *p = (*T), *f = NULL, *q, *s;
while(p!=NULL && !EQ(p->key, key))
{
f = p; //f记录p的父结点
if(LT(key, p->key)) p = p->lchild; //搜索左子树
else p = p->rchild; //搜索右子树
}
if(p==NULL) return; //没有要删除的结点
/* 找到了要删除的结点为p */
s = p;
/* 将第3种情况,左、右子树都不空,找左子树中最右边的结点s */
if(p->lchild!=NULL && p->rchild!=NULL)
{
f = p;
s = p->lchild; //从左子树开始找
while(s->rchild!=NULL)
{
f = s;
s = s->rchild;
}
p->key = s->key; //用结点s的内容替换结点p内容
}
/* 第2种情况 */
if(s->lchild!=NULL) q = s->lchild; //若s有左子树,右子树为空
else q = s->rchild;
if(f==NULL) (*T) = q;
else if(f->lchild==s) f->lchild = q;
else f->rchild = q;
free(s);
}
//7.中序遍历二叉排序树,生成关键字序列。
void InOrderTraverse(BSTree T)
{
if(T)
{
InOrderTraverse(T->lchild);
printf("%d ", T->key);
InOrderTraverse(T->rchild);
}
}
int main()
{
BSTree BST;
CreateBSTree(&BST);
printf("中序序列为:");
InOrderTraverse(BST);
printf("\n\n");
KeyType key;
printf("请输入待查找数据元素的关键字:");
scanf("%d", &key);
SearchBST1(BST, key)==NULL ? printf("不在!\n\n") : printf("在!\n\n");
DeleteBST(&BST, key);
printf("中序序列为:");
InOrderTraverse(BST);
return 0;
}
测试结果如下:
【算法分析】
和折半查找的判定树类似,二叉排序树中的结点作为内部结点,可以添加相应的外部结点。具有 个内部结点的二叉排序树,其外部结点的个数为 。
然而,用折半查找法查找长度为 的有序表,其判定树是唯一的,而含有 个元素的二叉排序树却不唯一。
以关键字序列{5,2,1,6,7,4,8,3,9}
和{1,2,3,4,5,6,7,8,9}
生成的二叉排序树如下:
其等概率查找成功的平均查找长度分别为:
由此可见,在二叉排序树上进行查找时的平均查找长度和二叉排序树的形态有关。上图b为最坏情况,退化为单支树,其平均查找长度和单链表上的顺序查找相同,即 。