二叉搜索树
上一讲提到的查找问题 , 分两类 : 静态查找 ( 查字典 )和动态查找 ( 经常发生插入删除 )
二分查找效率高 , 不用数组直接把元素放在树上 , 这样的树就是二叉查找树 ( 二叉搜索树 )
又称 BST , Binary Search Tree
性质
一颗二叉搜索树如果不空的话 , 满足以下性质 :
- 非空左子树的所有键值小于其根结点的键值
- 非空右子树的所有键值大于其根结点的键值
- 左、右子树都是二叉搜索树
二叉搜索树操作的特别函数
Position Find( ElementType X, BinTree BST)
: 从二叉搜索树BST中查找元素 X , 返回其所在结点的地址Position FindMin( BinTree BST)
: 从二叉搜索树BST中查找并返回最小元素所在的结点的地址Position FindMax( BinTree BST)
: 从二叉搜索树BST中查找并返回最大元素所在的结点的地址BinTree Insert( ElementType X, BinTree BST)
: 插入BinTree Delete( ElementType X, BinTree BST)
: 删除
查找操作 Find函数
Position Find(ElementType X, BinTree BST){
if( !BST ) return NULL; /*如果不是BST类型则查找失败*/
if( X > BST->Data)
return Find(X, BST->Right); /*在右子树中继续查找*/ /*尾递归*/
Else if( X < BST->Data)
return Find(X, BST->Left); /*在左子树中继续查找*/ /*尾递归*/
else /* X == BST->Data */
return BST;
}
都是尾递归 : 在程序返回的时候才出现递归 (尾递归一般可以用循环实现)
- 由于非递归函数的执行效率高 , 可将尾递归函数改为迭代函数
由此可知 : 查找的效率取决于树的高度Position IterFind(ElementType X, BinTree BST){ while( !BST ) { if(X > BST->Data) BST = BST->Right; /*向右子树中移动,继续查找*/ else if( X < BST->Data) BST = BST->Left; /*向左子树中移动,继续查找*/ else /* X == BST->Data*/ return BST; /*查找成功,返回结点的找到结点的地址*/ } return NULL; /*查找失败*/ }
**
查找最大和最小元素
- 最大元素一定是在树的最右分支的端结点上
- 最小元素一定是在树的最左分支的端结点上
例如 :
Position FindMin(BinTree BST){
if( !BST ) return NULL; /*空的二叉搜索树,返回NULL*/
else if (!BST->Left)
return BST; /*找到最左叶结点并返回*/
else
return FindMin(BST->Left); /*沿左分支继续查找*/
}
查找最小元素的递归函数
Position FindMax( BinTree BST){
if ( BST )
while(BST->Right) BST = BST->Right; /*当右孩子不空时指针向右移动*/
/*沿右分支继续查找,直到最右叶结点*/
return BST;
}
查找最大元素的迭代函数
二叉搜索树的插入
分析 : 关键是要找到元素应该插入的位置 , 可以采用与Find类似的方法
需要注意的地方 : 上例中需要记住33的位置 , 需要递归函数返回到根结点的地址
BinTree Insert(ElementType X, BinTree BST){
if(!BST){
/*若原树为空,生成并返回一个结点的二叉搜索树*/
BST = malloc(sizeof(struct TreeNode));
BST->Data = X;
BST->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);
/*递归插入右子树*/
/*else X已经存在,什么都不做*/
return BST;
}
二叉搜索树的插入算法
例
以一年十二个月的英文缩写为键值 , 按从一月到十二月顺序输入 , 即输入序列为 (Jan Feb…), 按字典顺序排列
二叉搜索树的删除
先找到再删除 , 找到之后考虑三种情况 :
- 要删除的是叶子结点 : 直接删除 , 并再修改其父节点指针 —- 置为NULL
- 要删除的是只有一个孩子的结点 : 将其父结点的指针指向要删除结点的孩子结点 (删除Aug : 将Apr指向Dec)
- 要删除的是有左右两颗子树的结点 : 用另一个结点代替被删除的结点 , 右子树的最小元素或左子树的最小元素
取右子树的最小元素代替 : =>
取左子树的最小元素替代 : =>
BinTree Delete(ElementType X, BinTree BST){
Position Tmp;
if(!BST) printf("要删除的元素未找到");
else 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 && BST->Right){ /*被删除结点有左右两个子结点*/
Tmp = FindMin(BST->Right); /*在右子树中找最小的元素填充删除结点*/
BST->Data = Tmp->Data;
BST->Right = Delete(BST->Data,BST->Right); /*在删除结点的右子树中删除最小元素*/
} else { /*被删除结点有一个或无子结点*/
Tmp = BST;
if(!BST->Left) /*有右孩子或无子结点*/
BST = BST->Right;
else if(!BST->Right) /*有左孩子或无子结点*/
BST = BST->Left;
free(Tmp);
}
return BST;
}
平衡二叉树
定义
搜索树的结点插入次序不同的话 , 会导致树的深度和平均查找长度ASL的不同
(平均查找长度ASL : 衡量查找效率的指标)
例 :
上图中(a)为自然月份序列 , (b)按 72538146 10 9 11 12 , (c)按月份字符串大小顺序(英文单词的字典顺序)
其中 :
- ASL(a) = (1+2╳2+3╳3+4╳3+5╳2+6╳1)/12 = 3.5
- ASL(b) = 3.0
- ASL(c) = 6.5
树(b)效率最高 , 因为相比之下最**平衡**
平衡因子
左右两边怎样才能维持平衡 ?
两个指标 : 左右两边的高度差不多 ; 左右两边的结点数差不多
因此定义了 平衡因子 (Balance Factor, BF) :
( h和h分别为T的左右子树的高度 )
平衡二叉树 (Balaced Binary Tree) : ( AVL树 )
- 空树 , 或者任一结点左右子树高度差的绝对值不超过 1 , 即
高度
问题 : 平衡二叉树的高度能达到 log**n 吗 ?**
(完全二叉树的高度为 log**n **)
讨论 : 设 n 是高度为 h 的平衡二叉树的最小结点数。
结点数最少时 :
h = 3时 , 至少需要7个结点
( 与斐波那契数列一致 )
得到查找效率 :
平衡二叉树的调整
调整方式的选择取决于 破坏者与被破坏者的关系
右单旋
问题 : 插入时导致原本平衡的二叉树不平衡了 , 此时需要**调整
抽象成 :
- “不平衡”的发现者是 Mar (-2) , “麻烦结点” Nov在发现者右子树的右边
- 因此叫 RR插入 , 需要 RR旋转(右单旋)
B 上的所有结点都比A大比B小 , 所以挂在A的右子树上
**
具体例
如下二叉树。5 是被破坏者 , 15 是破坏者
插入 13 作为破坏者 :
左单旋
非根节点的左单旋**
抽象成 :
- “发现者” 是 Mar (-2) , “麻烦结点” Apr 在发现者左子树的左边
- 因此叫 LL插入 , 需要 LL旋转(左单旋)
具体例
LR旋转
抽象成 :
- “发现者”是 May, “麻烦结点” Jan 在左子树的右边
- 因此叫 LR插入 , 需要 LR旋转
RL旋转
例 : 是否是同一颗二叉搜索树的判别
题意理解
- 给定一个插入序列就可以唯一确定一颗二叉搜索树。
- 然而 , 一颗给定的二叉搜索树却可以由多种不同的插入序列得到句号
问题
对于输入的各种插入序列 , 判断他们是否能生成一样的二叉搜索树
输入输出样例
求解思路
两个序列是否对应相同搜索树的判别
方法 :
- 分别建两颗搜索树的判别方法 : 根据两个序列分别建树 , 再判别树是否一样
- 不建树的判别方法
- 建一棵树 , 再判别其他序列是否与该树一致 (选用这种方法)
不建树的判别方法
第一个数相同表示根结点相同 , 后面的序列可以分为比根结点大的 (右子树) 和比根结点小的 (左子树) ,
然后比较两个序列的左右子树是否完全相同 (按上述方法递归)
建一棵树 , 再判别其他序列是否与该树一致
思路 :
- 搜索树表示
- 建搜索树T
- 判别另一序列是否与搜索树T一致
1. 搜索树表示
经典结构 :
typedef struct TreeNode *Tree;
struct TreeNode {
int v;
Tree Left, Right; /*左右指针表示链表*/
int flag; /*用来判别另一序列是否与该树一致 - 如果某结点没被访问则flag为0,被访问过为1*/
};
2. 程序框架搭建
int main()
{ 对每组数据
◉ 读入N和L /*N为结点个数,L为序列个数*/
◉ 根据第一行序列建树T
◉ 依据树T分别判别后面的L个序列是否能与T形成同一搜索树并输出结果
return 0;
}
根据上述基本框架 , 可以得到需要设计的主要函数有 :
- 读数据建搜索树T
- 判别某一序列是否与T构成一样的搜索树
具体实现 :
int main()
{
int N,L,i;
Tree T;
scanf("%d",&N);
while(N){
scanf("%d",&L);
T = MakeTree(N);
for(i=0;i<L;i++){ /*后面有L个序列*/
if(Judge(T,N)) printf("Yes\n");
else printf("No\n");
ResetT(T); /*清除T中的标记flag*/
}
FreeTree(T);
scanf("%d",&N);
}
return 0;
}
3. 如何建搜索树
Tree MakeTree( int N ){ /*N为结点数*/
Tree T;
int i,V;
scanf("%d",&V);
T = NewNode(V);
for(i=1;i<N;i++){
scanf("%d",&V);
T = Insert(T,V); /*把当前读入的结点插到树里*/
}
return T;
}
Tree NewNode( int V ){
Tree T = (Tree)malloc(sizeof(struct TreeNode));
T->V = V;
T->Left = T->Right = NULL;
T->flag = 0;
return T;
}
Tree Insert( Tree T, int V){
if(!T) T = NewNode(V);
else{
if(V>T->v)
T->Right = Insert(T->Right,V);
else
T->Left = Insert(T->Left,V);
}
return T;
}
4. 如何判别
如何判别序列 3 2 4 1 是否与树 T 一致 ?
方法 : 在树 T 中按顺序搜索序列 3 2 4 1 中的每个数
- 如果每次搜索所经过的结点在前面均出现过 , 则一致
- 否则 ( 某次搜索中遇到前面未出现的 ) , 则不一致
```c
int check(Tree T, int V){ /在树T中查找整数V/
if( T->flag){
} else{if(V<T->v) return check(T->Left,V); else if(V>T->v) return check(T->Right, V); else return 0;
} }if(V==T->v){ T->flag = 1; return 1; } else return 0;
int Judge(Tree T, int N){ /有bug: 当发现序列中的某个数与T不一致时,必须把序列后面的数都读完/ int i,V;
scanf("%d",&V);
if(V!=T->v) return 0;
else T->flag = 1;
for(i=1;i<N;i++){
scanf("%d",&V);
if(!check(T,V)) return 0;
}
return 1;
}
int Judge(Tree T, int N){ /改写/ int i,V,flag = 0; /flag:0 代表目前还一致,1代表一定不一致/
scanf("%d",&V);
if(V!=T->v) flag=1;
else T->flag = 1;
for(i=1;i<N;i++){
scanf("%d",&V);
if((!flag)&&!check(T,V)) flag=1;
}
if(flag) return 0;
else return 1;
}
<a name="LgYHs"></a>
### 5. 小程序
```c
void ResetT( Tree T){ /*清除T中各结点的flag标记*/
if(T->Left) ResetT(T->Left);
if(T->Right) ResetT(T->Right);
T->flag = 0;
}
void FreeTree(Tree T){ /*释放T的空间*/
if(T->Left) FreeTree(T->Left);
if(T->Right) FreeTree(T->Right);
free(T);
}
课件
小测验1: 二叉搜索树
课件1: 二叉搜索树
C语言实现: 二叉搜索树的插入与删除
BinTree Insert( BinTree BST, ElementType X )
{
if( !BST ){ /* 若原树为空,生成并返回一个结点的二叉搜索树 */
BST = (BinTree)malloc(sizeof(struct TNode));
BST->Data = X;
BST->Left = BST->Right = NULL;
}
else { /* 开始找要插入元素的位置 */
if( X < BST->Data )
BST->Left = Insert( BST->Left, X ); /*递归插入左子树*/
else if( X > BST->Data )
BST->Right = Insert( BST->Right, X ); /*递归插入右子树*/
/* else X已经存在,什么都不做 */
}
return BST;
}
BinTree Delete( BinTree BST, ElementType X )
{
Position Tmp;
if( !BST )
printf("要删除的元素未找到");
else {
if( X < BST->Data )
BST->Left = Delete( BST->Left, X ); /* 从左子树递归删除 */
else if( X > BST->Data )
BST->Right = Delete( BST->Right, X ); /* 从右子树递归删除 */
else { /* BST就是要删除的结点 */
/* 如果被删除结点有左右两个子结点 */
if( BST->Left && BST->Right ) {
/* 从右子树中找最小的元素填充删除结点 */
Tmp = FindMin( BST->Right );
BST->Data = Tmp->Data;
/* 从右子树中删除最小元素 */
BST->Right = Delete( BST->Right, BST->Data );
}
else { /* 被删除结点有一个或无子结点 */
Tmp = BST;
if( !BST->Left ) /* 只有右孩子或无子结点 */
BST = BST->Right;
else /* 只有左孩子 */
BST = BST->Left;
free( Tmp );
}
}
}
return BST;
}