本周为学校的算法与数据结构实践周,博主接到的题目内容如下:
题B5:搜索树操作程序
开发建立搜索树的程序,在程序中可以进行参数设置、数据元素输入与操作选择,支持搜索树结构中的各种数据操作(如插入、删除、搜索等),可使用C、C++或Java等编程语言实现。
基本要求
(1) 连续输入若干数据元素,程序自动在画板上画出相应二叉搜索树;可以对已经生成的二叉搜索树进行插入、删除和搜索操作,程序动态显示操作结果。
(2) 连续输入若干数据元素,在画板上画出相应二叉平衡树;可以对已经生成的二叉平衡树进行插入、删除和搜索操作,程序动态显示操作结果。
(3) 实物演示时要求演示结果正确。
(4) 程序操作友好、健壮。
提高要求:
(1) 连续输入若干数据元素,程序能动态画出相应B-树。
(2) 可以对已经生成的B-树进行插入、删除和搜索操作,程序动态显示操作结果。
(3) 图形化界面,树形美观对称。
经过向老师询问,“动态”是一个开放的概念,看个人理解,那技术选择第二步再实施。
首先我们需要对二叉搜索树进行复习和巩固。
目录
二叉搜索树
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
复习参考博客,是我之前的博客:https://www.cnblogs.com/WittPeng/p/9013166.html
1.二叉搜索树的分类
按照上面的引用知识,我们可以知道二叉搜索树本身满足左子树所有节点的值比根节点的值小,右子树的所有节点的值比根节点的值大。但在此基础上,还有二叉平衡树的概念:
具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。 最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci(斐波那契)数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。
平衡树是为了避免二叉搜索数退化成链表而出现的,而且可以说,任务完成得很出色。
所以下面直接进入二叉平衡树的研究。
2.平衡因子
左子树的高度减右子树的高度,取值只可能为0,1,-1。
否则就要进行调整。
3.二叉平衡树的数据结构
typedef char KeyType; //关键字
typedef struct MyRcdType //记录
{
KeyType key;
}RcdType,*RcdArr;
typedef enum MyBFStatus //为了方便平衡因子的赋值,这里进行枚举
{ //RH,EH,LH分别表示右子树较高,左右子树等高,左子树较高
RH,EH,LH
}BFStatus;
typedef struct MyBBSTNode //树结点类型定义
{
RcdType data; //数据成员
BFStatus bf; //平衡因子
struct MyBBSTNode *lchild,*rchild; //左右分支
}BBSTNode,*BBSTree;
4.二叉搜索树的插入
二叉搜索树插入时,为满足搜索树要求,可能会一直输入比根节点小或大的元素,或者其他原因,使得二叉平衡树出现了失衡现象,此时就需要旋转最小失衡子树进行恢复平衡,有以下四种情况:
-
LL型
如果根节点为BL的树(或子树),此时根节点的平衡因子为1,即左子树已高于右子树,若有新节点插在BL的左子树的左子树上,则会出现失衡现象。此时需要单向向右旋转,调整以为BL根的子树(最小失衡子树),树的高度不变。
图示:
实现代码为:
/*
* 当T的左子树的左子树上的节点使得T的平衡度为2时,以T为中心进行右旋。
*/
bool LLRotate(BiTree *T)
{
BiTree lc;
lc = (*T)->lchild;
(*T)->lchild = lc->rchild;
lc->rchild = (*T);
//注意要更新结点的高度。整个树中只有*T的左子树和lc的右子树发生了变化,所以只需更改这两棵树的高度。
(*T)->height = max(GetHeight((*T)->lchild), GetHeight((*T)->rchild)) + 1;
lc->height = max(GetHeight(lc->lchild), GetHeight(lc->rchild)) + 1;
*T = lc;
return true;
}
-
LR型
如果根节点平衡因子为-1,且问题出在根结点左子树的根节点平衡因子为-1,而此时新插入的节点在左子树的右子树上,即左子树的平衡因子即将变为-2。这样的情况下,因最小失衡子树是根节点的左子树,虽然根节点本身也出现了平衡因子失衡,只需处理最小失衡子树即可,它是 主 要 矛 盾!(可参照毛概中对主要矛盾的解释)
结果为:旋转之后为“原来根结点的左孩子的右孩子作为新的根结点”。
步骤如下:
i. 将2的左子树作为1的右子树(维护树的有序性,只是此处为NULL而已)
ii. 将1作为2的左子树
iii. 将2作为3的左子树
代码如下:
/*
* 当T的左子树的右子树上的节点使得T的平衡度为2时,
* 先以T的左子树为中心进行左旋,再以T为中心进行右旋。
*/
bool LRRotate(BiTree *T)
{
RRRotate(&((*T)->lchild));
LLRotate(T);
return true;
}
- RR型
如果根节点为BL的树(或子树),此时根节点的平衡因子为-1,即左子树已低于右子树,若有新节点插在BL的右子树的右子树上,则会出现失衡现象。此时需要单向向左旋转,调整以为BL根的子树(最小失衡子树),树的高度不变。
结果为:“原来根结点的右孩子作为新的根结点”。
步骤为:
i. 将2作为根结点
ii. 将1作为2的左孩子
iii. 将2的左孩子作为1的右孩子(维护树的有序性,只是此处为NULL而已)
效果图:
实现代码:
/*
* 当T的右子树的右子树上的节点使得T的平衡度为-2时,以T为中心进行左旋。
*/
bool RRRotate(BiTree *T)
{
BiTree rc;
rc = (*T)->rchild;
(*T)->rchild = rc->lchild;
rc->lchild = (*T);
//注意要更新结点的高度。整个树中只有*T的左子树和lc的右子树发生了变化,所以只需更改这两棵树的高度。
(*T)->height = max(GetHeight((*T)->lchild), GetHeight((*T)->rchild)) + 1;
rc->height = max(GetHeight(rc->lchild), GetHeight(rc->rchild)) + 1;
*T = rc;
return true;
}
- RL型
与LR型类似,我们需要进行两次旋转。旋转之后为“原来根结点的右孩子的左孩子作为新的根结点”。
具体步骤如下
i. 将2作为1的右孩子
ii. 将3作为2的右孩子
iii. 将2的右孩子作为3的左孩子(维护树的有序性,只是此处为NULL而已)
图示:
iv. 将2作为根结点
v. 将1作为2的左孩子
vi. 将2的左孩子作为1的右孩子(维护树的有序性,只是此处为NULL而已)
图示:
代码为:
/*
* 当T的右子树的左子树上的节点使得T的平衡度为-2时,
* 先以T的右子树为中心进行右旋,再以T为中心进行左旋。
*/
bool RLRotate(BiTree *T)
{
LLRotate(&((*T)->rchild));
RRRotate(T);
return true;
合并后,插入操作的代码为:
/*
* 插入操作。
* 如果以*T为根结点的二叉平衡树中已有结点key,插入失败,函数返回FALSE;
* 否则将结点key插入到树中,插入结点后的树仍然为二叉平衡树,函数返回TRUE。
*/
bool AVLInsert(BiTree *T, TElemType key)
{
BiTree t;
//如果当前查找的根结点为空树,表明查无此结点,故插入结点。
if (!*T)
{
t = (BiTree)malloc(sizeof(BiNode));
t->data = key;
t->height = 1;
t->lchild = NULL;
t->rchild = NULL;
*T = t;
return true;
}
//已有此结点,不再插入。
else if (key == (*T)->data)
{
return false;
}
//在左子树中递归插入。
else if (key < (*T)->data)
{
if (!AVLInsert(&((*T)->lchild), key))
return false;
else
{
//插入成功,修改树的高度。
(*T)->height = max(GetHeight((*T)->lchild), GetHeight((*T)->rchild)) + 1;
//已在*T的左子树插入结点key,判断是否需要进行旋转以保持二叉平衡树的特性。
if (2 == GetHeight((*T)->lchild) - GetHeight((*T)->rchild))
{
//在左子树的左子树中插入结点。
if (GetHeight((*T)->lchild->lchild) > GetHeight((*T)->lchild->rchild))
{
LLRotate(T);
}
//在左子树的右子树中插入结点。
else
{
LRRotate(T);
}
}
return true;
}
}
//在右子树中递归插入。
else // (key > (*T)->data)
{
if (!AVLInsert(&(*T)->rchild, key))
return false;
else
{
//插入成功,修改树的高度。
(*T)->height = max(GetHeight((*T)->lchild), GetHeight((*T)->rchild)) + 1;
//已在*T的右子树插入结点key,判断是否需要进行旋转以保持二叉平衡树的特性。
if (-2 == GetHeight((*T)->lchild) - GetHeight((*T)->rchild))
{
//在右子树的左子树中插入结点。
if (GetHeight((*T)->rchild->lchild) > GetHeight((*T)->rchild->rchild))
{
RLRotate(T);
}
//在右子树的右子树中插入结点。
else
{
RRRotate(T);
}
}
return true;
}
}
}
5.二叉搜索树的删除及调整
删除会带来许许多多的问题,见下:
-
删除节点导致平衡二叉树失衡
二叉搜索树也是一棵二叉查找树,删除操作基于二叉删除树的删除之上,但是要在不平衡的时候进行调整。根据事实来看,在较低子树上进行删除常常会导致不平衡树的出现。例如:
- 调整不平衡子树可能带来更大的问题
最小不平衡子树为A,它为双亲结点b的左子树,而b的平衡因子为RH。假设我们现在对A进行了平衡处理,如上所讲,进行平衡处理将导致树高降低。即我们让b较矮的子树变得更矮了。此时对于b而言,同样也是不平衡的。此时,我们需要再一次进行一次平衡处理。例如:
假设我们删除了结点6.那么最小不平衡子树就是1,3,5对应的二叉树。它的双亲10的平衡因子为RH。我们首先对最小不平衡子树进行调整,结果如右图。我们发现,最小不平衡子树从根结点的左子树变成了整棵树,所以这个时候我们又要进行一次平衡调整。具体的平衡调整步骤与插入时是一致的,在这里就不再赘述。
那么结论就是:处理最小失衡子树绝对是没有问题的,而且慢慢地处理下,问题会逐步解决。
- LE与RE
LE与RE型的失衡树,在进行调整的时候,和LL与RR型的旋转方式是一致的。只是最后初始根结点的平衡因子不为EH而已。就拿上面的例子而言,调整后的结果如下。初始根结点的平衡因子为RH。相对应的,假如是LE的情况,调整后初始根结点的平衡因子为LH。
综合以上,删除操作的代码为:
/*
* 删除操作。
* 如果以*T为根结点的树中存在结点key,将结点删除,函数返回TRUE,
* 否则删除失败,函数返回FALSE。
*/
bool AVLDelete(BiTree *T, TElemType key)
{
BiTree pre, post;
//没有找到该结点。
if (!*T)
return false;
//找到结点,将它删除。
else if (key == (*T)->data)
{
//待删除节点为叶子结点。
if (!(*T)->lchild && !(*T)->rchild)
*T = NULL;
//待删除结点只有右孩子。
else if (!(*T)->lchild)
*T = (*T)->rchild;
//待删除结点只有左孩子。
else if (!(*T)->rchild)
*T = (*T)->lchild;
//待删除结点既有左孩子,又有右孩子。
else
{
//当待删除结点*T左子树的高度大于右子树的高度时,用*T的前驱结点pre代替*T,
//再将结点pre从树中删除。这样可以保证删除结点后的树仍为二叉平衡树。
if (GetHeight((*T)->lchild) > GetHeight((*T)->rchild))
{
//寻找前驱结点pre。
pre = (*T)->lchild;
while (pre->rchild)
{
pre = pre->rchild;
}
//用pre替换*T。
(*T)->data = pre->data;
//删除节点pre。
//虽然能够确定pre所属最小子树的根结点为&pre,
//但是不采用AVLDelete(&pre,pre->data)删除pre,目的是方便递归更改节点的高度。
AVLDelete(&((*T)->lchild), pre->data);
}
//当待删除结点*T左子树的高度小于或者等于右子树的高度时,用*T的后继结点post代替*T,
//再将结点post从树中删除。这样可以保证删除结点后的树仍为二叉平衡树。
else
{
//寻找后继节点post。
post = (*T)->rchild;
while (post->lchild)
post = post->lchild;
//用post替换*T。
(*T)->data = post->data;
//删除节点post。
//虽然能够确定post所属最小子树的根结点为&post,
//但是不采用AVLDelete(&post,post->data)删除post,目的是方便递归更改节点的高度。
AVLDelete(&((*T)->rchild), post->data);
}
}
return true;
}
//在左子树中递归删除。
else if (key < (*T)->data)
{
if (!AVLDelete(&((*T)->lchild), key))
return false;
else
{
//删除成功,修改树的高度。
(*T)->height = max(GetHeight((*T)->lchild), GetHeight((*T)->rchild)) + 1;
//已在*T的左子树删除结点key,判断是否需要进行旋转以保持二叉平衡树的特性。
if (-2 == GetHeight((*T)->lchild) - GetHeight((*T)->rchild))
{
if (GetHeight((*T)->rchild->lchild) > GetHeight((*T)->rchild->rchild))
{
RLRotate(T);
}
else
{
RRRotate(T);
}
}
return true;
}
}
//在右子树中递归删除。
else
{
if (!AVLDelete(&((*T)->rchild), key))
return false;
else
{
//删除成功,修改树的高度。
(*T)->height = max(GetHeight((*T)->lchild), GetHeight((*T)->rchild)) + 1;
//已在*T的右子树删除结点key,判断是否需要进行旋转以保持二叉平衡树的特性。
if (2 == GetHeight((*T)->lchild) - GetHeight((*T)->rchild))
{
if (GetHeight((*T)->lchild->lchild) > GetHeight((*T)->lchild->rchild))
{
LLRotate(T);
}
else
{
LRRotate(T);
}
}
return true;
}
}
}
二叉搜索树和平衡树的复习就到这里,根据实践周项目的提高要求,下篇博客复习B-树。