二叉排序树
又称二叉查找树。它或是一棵空树,或者是具有以下性质的二叉树:
1.若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;
2.若它的右子树不空,则右子树上所有结点的值均大于它的根结构的值;
3.它的左、右子树也是二叉排序树
二叉树的结构定义
//二叉树的二叉链表结点结构定义
typedef struct BiTNode
{
int data; //数据
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
二叉排序树查找
//递归查找二叉排序树T中是否存在key
//指针f指向T的双亲,初值调用值为NULL
//若查找成功,则指针p指向该数据元素的结点,并返回true
//否则,指针p指向查找路径上访问的最后一个结点并返回false
Status SearchBST(BiTree T,int key,BiTree f,BiTree *p)
{
if(!T) //查找失败
{
*p = f;
return False; //结束递归
}
else if(key == T->data) //查找成功
{
*p = T;
return True;
}
else if(key < T->data)
return SearchBST(T->lchild, key, T, p); //在左子树继续查找
else
return SearchBST(T->rchild, key, T, p); //在右子树继续查找
}
二叉排序树插入
//当二叉排序树T中不存在关键字key时
//插入key并返回True,否则返回false
Status InsertBST(BiTree *T,int key)
{
BiTree p,s;
if(!SearchBST(*T, key, NULL, &p)) //查找不成功,p为访问的最后一个结点
{
s = (BiTree)malloc(sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL; //构建s结点
if(!p) //p为空,即树为空树
*T = s; //新的结点s作为根结点
else if(p->data > key)
p->lchild = s; //插入s为左孩子
else
p->rchild = s; //插入s为右孩子
}
}
}
二叉排序树构建
int i;
int a[10] = {62,88,58,47,35,73,51,99,37,93};
BiTree T = NULL;
for(i=0;i<10;i++)
{
InsertBST(&T,a[i]); //依次插入各个数据
}
二叉树删除
分三种情况:
1.删除叶子结点
2.删除仅有左或右子树的结点
3.删除左右子树都有的结点
分两步:
1.查找到关键字key所在的结点
2.删除该节点
//若树T中存在关键字key的结点时,删除该节点返回true,否则返回false
//这段只是将查找操作换成了删除操作
Status DeleteBST(BiTree *T,int key)
{
if(!T) //不存在关键字
{
return False; //结束递归
}
else
{
if(key == T->data) //找到结点
return Delete(T);
else if(key < T->data)
return DeleteBST(&(*T)->lchild, key); //在左子树继续查找
else
return DeleteBST(&(*T)->rchild, key); //在右子树继续查找
}
}
//从二叉排序树中删除结点p,并重新拼接左或右子树
Status Delete(BiTree *p)
{
BiTree q,s;
if((*p)->rchild == NULL) //右子树空则只需重接他的左子树
{
q = *p; //记录结点p为q
*p = (*p)->lchild; //p更新为其左子树
free(q); //释放q结点
}
else if((*p)->lchild == NULL) //只需重接右子树
{
q = *p; //记录结点p为q
*p = (*p)->rchild; //p更新为其左子树
free(q); //释放q结点
}
else //左右子树均不为空
{
q = *p; //用q记录p的位置
s = (*p) ->lchild; //转左再向右到尽头直到找到待删除结点的前驱
while(s->rchild) //s指向p的前驱结点 q指向s的父结点
{
q = s;
s = s->rchild;
}
(*p)-data = s ->data; //将s结点的数据放到p
if(q != *p) //p不等于q,即s为q的右孩子时
q->rchild = s ->lchild; //重接q的右子树
else //p等于q,即s为q的左孩子没有更右的叶子
q->lchild = s ->lchild; //重接q的左子树
free(s); //释放s结点
}
return True;
}
二叉排序树的总结:
采用链式方式存储,保持了链式存储结构在插入和删除时不需要移动元素的优点,对于查找,从根节点到要查找的结点的路径,其比较次数等于给定的结点在二叉排序树的层数,即最少为1次,最多为树的深度。但是由于树的结构不确定,可能存在极端的(左)右斜树,其查找就如同顺序查找,因此引入问题:如何让二叉树平衡?!
平衡二叉树(AVL树)
AVL树是一种高度平衡的二叉排序树,满足二叉排序树的要求
其为一种二叉排序树,且其中每一个结点的左子树和右子树的高度差至多等于1。
平衡因子BF:该结点的左子树的深度减去右子树的深度
平衡二叉树上所有结点的平衡因子只可能是:-1 、0 、1
最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树
平衡树的实现思想:
在构建二叉排序树时,每插入一个结点,先检查是否因为插入而破坏了书的平衡性,如果是,则找出最小不平衡树进行调整,使之成为新的平衡树。
情况:
BF为正,将最小不平衡树进行右旋
BF为负,将最小不平衡树进行左旋
根节点与子节点的BF值符号不统一,先调整最小不平衡树的子树,使符号统一后,再进行旋转调整
//二叉平衡树的结点结构定义
typedef struct BiTNode
{
int data; //数据
int bf; //平衡因子
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
//右旋操作
//对以p为根的二叉排序树进行右旋处理
//处理之后p指向新的树的根节点
void R_Rotate(BiTree *p)
{
BiTree L;
L = (*p)->lchild; //L指向P的左子树的根节点
(*p) ->lchild = L ->rchild; //L的右子树挂接为p的左子树
L -> rchild = (*p);
*p = L; //p指向新的根节点
}
//左旋操作
//对以p为根的二叉排序树进行左旋处理
//处理之后p指向新的树的根节点
void L_Rotate(BiTree *p)
{
BiTree R;
R = (*p)->rchild; //R指向P的右子树的根节点
(*p) ->rchild = R ->lchild; //R的左子树挂接为p的右子树
R -> lchild = (*p);
*p = R; //p指向新的根节点
}
除了左右旋之外,我们还需要判断什么时候进行调整以及bf的修改,所以对于左平衡旋转的处理代码如下
#define LH +1 //左高
#define EH 0 //等高
#define RH -1 //右高
//对于左子树高于右子树的情况,即T的根节点的bf大于1
//对以指针T所指结点为根的二叉树作为作平衡旋转处理
//当新节点插入到树的左子树后
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
L = (*T) ->lchild; //L指向T的左子树根节点
switch(L->bf) //检查L的bf情况
{
case LH: //新节点插入在T的左孩子的左子树上 即L与根节点的符号一致
(*T) ->bf = L -> bf = EH; //调整bf
R_Rotate(T); //右旋操作
case RH; //新节点插入在T的左孩子的右子树上,L与根节点的符号不一致,需要做双旋处理
Lr =L -> rchild; //Lr指向T的左孩子的右子树根
switch(Lr->bf) //修改T以及其左孩子的平衡因子
{
case LH: //Lr左高,调整后根节点的bf为-1
(*T)->bf = RH;
L -> bf = EH;
break;
case EH: //Lr平衡
(*T)->bf = L->bf = EH;
break;
case RH: //Lr右高,调整后L->bf为左高
(*T)->bf = EH;
L->bf = LH;
break;
}
Lr -> bf = EH;
L_Rotate(&(*T)->lchild); //对T的左子树做左旋平衡处理
R_Rotate(T); //对T作右旋平衡处理
}
}
右平衡旋转的处理基本与上述相似
主函数如下:
//若在平衡二叉树排序树T中不存在和e相同的关键字的结点,则插入一个数据为e的新节点
//并返回1,否则返回0
//用布尔变量taller反映T长高与否
Status InsertAVL(BiTree *T,int e,Status *taller)
{
if(!*T) //不存在相同数据的结点,插入新节点,taller选择为真
{
*T = (BiTree)malloc (sizeof(BiTNode));
(*T)->data = e;
(*T)->lchild = (*T)->rchild = NULL;
(*T)->bf = EH;
*taller = True;
}
else
{
if(e == (*T)->data) //已经存在数据相同结点,不在插入
{
*taller = False;
return False;
}
if((*T)->data > e) //应该在T的左子树中查找
{
if(!InsertAVL(&(*T)->lchild ,e,taller)) //未插入、插入失败
return False;
if(*taller) //已插入且左子树长高
{
switch((*T)->bf) //检查T的平衡树
{
case LH: //原本左子树高于右子树,现在左树增高,需要左平衡处理
LeftBalance(T);
*taller = False; //重置标志位
break;
case EH: //原本左子树等于右子树,现在左树增高,导致根的左子树高
(*T)->bf = LH; //根的左子树高
*taller = True;
break;
case RH: //原右子树比左子树高,现在左右等高
(*T)->bf = EH;
*taller = True;
break;
}
}
}
else //右子树中进行搜索
{
if(!InsertAVL(&(*T)->rchild ,e,taller)) //未插入、插入失败
return False;
if(*taller) //已插入且左子树长高
{
switch((*T)->bf) //检查T的平衡树
{
case LH: //原本左子树高于右子树,现在左右等高
(*T)->bf = EH;
*taller = False;
break;
case EH: //原本左子树等于右子树,现在右树增高,导致根的右子树高
(*T)->bf = RH; //根的右子树高
*taller = True;
break;
case RH: //原右子树比左子树高,现在作右平衡处理
RightBalance(T);
*taller = False; //重置标志位
break;
}
}
}
}
return True;
}
//构建平衡二叉树
int i;
int a[10] = {3,2,1,4,5,6,7,10,9,8};
BiTree T = NULL;
Status taller;
for(i=0;i<10;i++)
{
InsertAVL(&T,a[i],&taller);
}
平衡二叉排序树的查找、删除、插入的时间复杂度均为O(logn)
多路查找树
为了降低对外存设备的访问次数,需要新的数据结构。
多路查找树,其每一个结点的孩子树可以多于两个,且每一个结点处可以存储多个元素。
这里介绍其中四个特殊的形式:2-3树 2-3-4树 B树 B+树
2-3树
2-3树是一颗多路查找树:其中每一个结点都具有两个孩子(2结点)或三个孩子(3结点),一个2结点包含一个元素和两个孩子(或者没有孩子),一个3结点包含一大一小两个元素和三个孩子(或者没有孩子)。注意2-3树的所有叶子均在同一层次上。
插入操作
(1)对于空树,插入一个2结点即可
(2)插入到一个2结点的叶子上,则将2结点升级为3结点即可
(3)插入到一个3结点时,需要拆分原有的结构,又分为三种情况:
情况一:插入的叶子为3结点,但是其双亲为2结点,考虑将其双亲升级为3结点
情况二:当插入的叶子结点以及其双亲结点均为3结点时,若其双亲的双亲为2结点,考虑将其升为3结点
情况三:如果一直到根节点,均为3结点,那么考虑增加树的高度,同时让这些3结点全部拆分成2结点
删除操作
(1)删除的元素位于一个3叶子结点上,删除该节点,让其变成一个2结点即可
(2)删除的元素位于一个2结点上,删除结点分为4种情况:
情形一:此节点的双亲也是2结点,且拥有一个3结点的孩子,则删除元素结点,将剩下的进行旋转调整即可
情形二:如果该节点的双亲为2结点,其右孩子也是2结点。则需要对整棵树进行调整,如下即是将根节点与其前驱7组成一个3结点,其后继成为新的根结点,然后再对3结点进行旋转操作调整。
情形三:其节点的双亲为3结点,则删除该结点,同时调整使其双亲成为2结点
情形四:如果整棵树是一个满二叉树,那么删除任何一个结点,只能考虑将结点合并、减小层数
(3)如果删除的元素位于非叶子结点的分支结点。此时我们考虑将树中按中序遍历的顺序得到其前驱或者后继元素,考虑让他们补位
2-3-4树
其为2-3 树的概念扩展,包括了4结点的使用。一个 4 结点包含小中大三个元素和四个孩子 (或没有孩子) ,一个 4结点要么没有孩子,要么具有 4 个孩子。如果某个 4 结点有孩子的话,左子树包含小于最小元素的元素 ; 第二子树包含大于最小元素, /J、于第二元素的元素;第三子树包含大于第二元素,小于最大元素的元素 i 右子树包含大于最大元素的元素。
其插入操作与删除操作与2-3树相似,例子如下
B树
是一种平衡的多路查找树,2-3树和2-3-4树均是B树的特例。结点最大的孩子数目为B树的阶,2-3树的阶为3,2-3-4树的阶为4
一个m阶的B树具有以下属性:
1.如果根节点不是叶结点,则其至少有两棵子树
2.每一个非根的分支结点都有k-1个元素和k个孩子,其中[m/2]<=k<=m。每一个叶子结点n都k-1个元素,其中[m/2]<=k<=m
3.所有的叶子结点均位于同一层次
4.所有分支结点包含以下信息数据(n,A0,K1,A1,K2,A2,...,Kn,An),其中Ki为关键字,且Ki<Ki+1;Ai为指向子树根节点的指针,且Ai-所指向的子树中所有结点的关键字均小于Ki,An所指的子树中所有结点的关键字均大于Kn,n为关键字的个数([m/2]-1<=n<=m-1)
左图为2-3-4树,右图为转化来的B树
B树减少内外存访问机制:
对B树调整阶数,使得B数的阶数与硬盘的存储页面大小相匹配。例如一棵树的阶数为1001(即1结点1000个关键字),高度为2,则可存储超过10亿个关键字,只需要将根节点持久地存储在内存中,那么至多两次访问即可。
查找最坏情况分析:
即表示含n个关键字的B树上查找时,从根节点到关键字结点的路径上涉及的结点数不超过
B+树
B树在中序遍历时,需要在节点之间访问,也就是不同的页面之间跳转,为了让遍历时每个元素只访问一次,引入B+树。在原有B树的基础上加入了新的元素组织方式,在B树中所有的元素指挥出现一次,而在B+树中,出现在分支结点中的元素会被当做他们在该分支节点位置的中序后继者(叶子节点中)再次列出。另外,每一个叶子结点都会保存一个指向后一个叶子节点的指针。
一棵m阶B+树和B树的差异:
1.有n棵子树的结点中包含n个关键字
2.所有的叶子结点包含全部关键字的信息,及指向这些关键字记录的指针,叶子节点本身依关键字的大小自小而大顺序连接
3.所有的分支结点可以看成是索引,结点中仅含有其子树中的最大(或最小)关键字
结构优势:
随机查找,就从根节点出发,与B树的查找 方式一样,只不过即使在分支结点找到了待查找的关键字,它也只是用来索引的,不能提供实际记录的访问,还是需要到达包含此关键字的终端结点。
如果我们是需要从最小关键字进行从小到大的顺序查找,我们就可以从最左侧的叶子结点出发,不经过分支结点,而是延着指向下一叶子的指针就可遍历所有的关键字。
B+树的结构特别适合带有范围的查找。
B+树的插入、删除过程也都与 B 树类似,只不过插入和删除的元素都是在叶子结点上进行而已。