二叉搜索树
首先回顾一下:
查找问题:
- 静态查找与动态查找;
- 针对动态查找,数据如何组织;
- 静态查找:要查找的元素是不动的,就是在一个集合中,主要做的是find操作,没有delete 和 insert 操作的发生;
- 动态查找:集合中,除了有find操作,还经常伴随着delete 和 insert 操作。
对于静态查找,我们会经常使用一个非常高效的查找算法----二分查找。它将查找的时间复杂度从 O(n) 直接降为 O(log n)。
二分查找要求集合要有序
为什么二分查找的效率这么好?
因为二分查找事先将要查找的数据进行了有序化操作。这样就把一个线性操作转化为一个类似于树的操作,查找的效率就是树的高度。从这里我们可以得到启发,能不能将数据直接存到树上,而不是将数据存入数组中,然后再采用类似于树的操作,即:减少了中间过程。PS: 树的动态性好,方便进行insert 和 delete 。
将数据直接放在树上面肯定是可行的,但是我们应该如何组织这棵树呢?我们可以尝试,让一个结点满足下列要求:
- 所有的左子树都小于该结点;
- 所有的右子树都大于该节点;
这样我们的查找过程就变成了一个对当前结点的一个判断。这样就大大缩小我们的查找范围。
我们上面期望的那种树在数据结构中早有定论:二叉搜索树
二叉搜索树的概念
二叉搜索树(BST,Binary Search Tree),亦称为:二叉排序树 or 二叉查找树。
二叉搜索树可以为空。如果不为空,则应满足:
- 非空左子树的所有键值 小于 其根节点的键值;
- 非空右子树的所有键值 大于 其根节点的键值;
- 左、右子树都是二叉搜索树;
二叉搜索树的常用操作:
- 查找操作:
- position Find(ElementType X, BinTree BST):从二叉搜索树BST中查找元素X,返回其所在结点的地址;
查找原理:查找从根节点开始,如果树为空,返回NULL; 若搜索树非空,则根节点和X进行比较,并进行不同的处理:1.若X < 根节点键值,只需左子树中继续搜索;2.如果X大于根节点的键值,在右子树中进行继续搜索;3.若两者比较结果是相等,搜索完成,返回指向此节点的指针。代码如下:
BinTree Find(ElementType X,BinTree BST){
if(!BST) return NULL ;
if(X > BST ->Data){
// 继续查找右子树
return Find(X,BST->Right);
}else(X < BST -> Left){
// 继续查找左子树
return Find(X,BST-Left);
}else{
// 查找成功
return BST;
}
}
我们可以发现上面的代码实现方式是递归实现,且都是尾递归实现;
尾递归:在程序即将返回的时候才进行递归;且尾递归都可以采用循环来实现
由于非递归函数的执行效率高,可将"尾递归"改为"迭代"函数
BinTree iterFind(ElementType X ,BinTree BST){
while(BST){
if(X > BST ->Data){
BST = BST -> Right;
}else if(X < BST -> Data){
BST = BST -> Left;
}else{
return BST;
}
}
return NULL ;
}
查找的效率决定于树的高度,如果都在左边 or 都在右边形成一条链,那么查找的效率就是O(n-1),我们的目标是O(log n),这就需要树的结构尽量左右对称,也就是以后要谈的平衡二叉树
- position FindMin(BinTree BST): 从二叉搜索树BST中查找并返回最小元素所在结点的地址;
最小元素一定是在树的最左分支的端节点上;(min 即最左叶子结点)
/*尾递归 查找最小元素*/
BinTree FindMin(BinTree BST){
if(BST == NULL) return NULL;
if(BST->Left != NULL){
return FindMin(BST -> Left);
} else{
return BST;
}
}
改为:迭代函数
BinTree FindMin(BinTree BST){
if(BST != NULL){
while(BST -> Left != NULL){
BST = BST -> Left;
}
return BST;
}
}
- position FindMax(BinTree BST): 从二叉搜索树BST中查找并返回最大元素所在结点的地址;
最大元素一定是在树的最右分支的端结点上;(max 即最右叶子结点)
/*尾递归 查找最大元素*/
BinTree FindMax(BinTree BST){
if(BST == NULL) return NULL;
if(BST->Right != NULL){
return FindMax(BST -> Right);
} else{
return BST;
}
}
改为:迭代函数
BinTree FindMax(BinTree BST){
if(BST != NULL){
while(BST -> Right != NULL){
BST = BST -> Right;
}
return BST;
}
}
- 插入操作:
- BinTree Insert(ElementType X,BinTree BST):
二叉搜索树的插入:关键是要找到元素应该插入的位置,可以采用与Find类似的方法
/*二叉搜索树的插入算法*/
BinTree Insert(ElementType X,BinTree BST){
if(BST == NULL){
/*若原树为空,生成并返回一个结点的二叉搜索树*/
BST = malloc(sizeof(struct TreeNode));
BST -> Data = X ;
BStT -> Left = BST -> Right = NULL;
}else{
if(X < BST -> Data){
// 递归 插入左子树
BST -> Left = Insert(X,BST -> Left);
}else if(X > BST -> Data){
// 递归 插入右子树
BST -> Right = Insert(X,BST -> Right);
}
}
return BST;
}
- 删除操作:
- BinTree Delete(ElementType X,BinTree BST):
二叉树的删除分为多种情况:
- 要删除的是叶子结点
直接删除,并再修改其父结点指针---置为NULL
- 要删除的结点只有一个孩子结点:
将其父结点的指针指向要删除结点的孩子结点
- 要删除的结点有左右两颗子树:
用另一结点替代被删除结点:右子树的最小元素 or 左子树的最大元素
复杂的问题简单化,将此种情况转化为上述两种情况之一
BinTree Delete(ElementType X,BinTree BST){
Position Tmp;
if(BST == NULL) printf("要删除的元素未找到");
if(X < BST -> Data){
// 左子树递归删除
BST -> Left = Delete(X, BST -> Left);
}else if(X > BST -> Data){
// 右子树递归删除
BST -> Right = Delete(X,BST -> Right);
}else{
// 找到要删除的结点
if(BST -> Left != NULL && BST -> Right != NULL){
//被删除结点有左右两个子结点
Tmp = FindMin(BST -> Right);
// 策略1:在右子树中找最小的元素替代被删除结点
BST -> Data = Tmp -> Data;
// 在删除结点的右子树中删除最小元素
BST -> Right = Delete(BST -> Data,BST -> Right);
}else{
// 被删除结点有一个or无子结点
Tmp = BST;
if(BST -> Left == NULL){
// 有右孩子or 无子结点
BST = BST -> Right;
}else if(BST -> Right == Right){
// 有左孩子or 无子结点
BST = BST -> Left;
}
free(Tmp);
}
}
return BST;
}