【转载】二叉平衡树的插入和删除操作

1.      二叉平衡树

二叉排序树查找、插入和删除操作的时间复杂度和树的深度n有关。构建树时,当先后插入的结点按关键字有序时,二叉排序树退化为单枝树,平均查找长度为(n+1)/2,查找效率比较低。提高查找效率,关键在于最大限度地降低树的深度n。因此需要在构建二叉排序树的过程中进行“平衡化”处理,使之成为二叉平衡树。

二叉平衡树,又称AVL树。它或者是一棵空树,或者是具有下列性质的树:

1)      具备二叉排序树的所有性质;

2)      左子树和右子树深度差的绝对值不超过1;

3)      左子树和右子树都是二叉平衡树。

二叉平衡树结点的平衡因子定义为左子树与右子树的深度之差。二叉平衡树结点的平衡因子只可能取-1,0,1三个值。含有n个结点的二叉平衡树的深度与logn同数量级,平均查找长度也和logn同数量级。

二叉平衡树采用二叉链表的结构进行存储。结构体中增加结点的高度,用以计算结点的平衡因子。结点高度定义:空结点的高度为0;非空结点的高度为以该结点为根结点的树的高度。

二叉链表:

/** 二叉树的二叉链表存储结构。* 额外添加树的高度,以判断结点的平衡度。*/
typedef int TElemType;
typedef struct BiNode
{TElemType data;struct BiNode *lchild;struct BiNode *rchild;int height;
}BiNode, *BiTree;

结点高度:

/** 当T=NULL ,即树为空树时,无法通过T->height获取树的高度0,所以要额外编写该函数。*/
int GetHeight(BiTree T)
{if (T)return T->height;return 0;
}

2.      处理失衡的四种旋转方式

如何在插入结点的时候进行“平衡化”处理?当在树中插入一个结点时,检查树是否因插入操作而失衡,若失衡,则找出其中的最小不平衡二叉树,对最小不平衡二叉树进行调整,以达到新的平衡。最小不平衡二叉树定义为以离插入结点最近,且平衡因子绝对值大于1的结点作为根结点的树。

对最小不平衡树进行调整的操作是旋转,共有4种旋转方式LL型,LR型,RL型,RR型,分别介绍如下:

1)        LL型(单次右旋)

当根结点左子树的左子树中的节点导致根结点的平衡因子为2时,采用LL型旋转进行调整。图示为两种需进行单次右旋的不平衡树。

LL型旋转即单次右旋,是将根结点的左孩子作为新的根结点,根结点左孩子的右子树作为老根结点的左子树。图示如下:

注意:旋转之后,整个树中只有结点k1,k2的高度发生变化,而x,y,z三棵子树中所有结点的高度均未发生变化。

代码:

/** 当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;
}

2)        RR型(单次左旋)

当根结点右子树的右子树中的节点导致根结点的平衡因子为-2时,采用RR型旋转进行调整。图示为两种需进行单次左旋的不平衡树。RR型旋转与LL型旋转相对称。

RR型旋转即单次左旋,是将根结点的右孩子作为新的根结点,根结点右孩子的左子树作为老根结点的右子树。图示如下:

注意:旋转之后,整个树中只有结点k1,k2的高度发生变化,而x,y,z三棵子树中所有结点的高度均未发生变化。

代码:

/** 当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;
}

3)        LR型(先单次左旋,再单次右旋)

当根结点左子树的右子树中的节点导致根结点的平衡因子为2时,采用LR型旋转进行调整。图示为两种需进行LR型旋转的不平衡树。

LR型旋转是先以根结点的左孩子为中心进行单次左旋,再以根结点为中心进行单次右旋。图示如下:

代码:

/** 当T的左子树的右子树上的节点使得T的平衡度为2时,* 先以T的左子树为中心进行左旋,再以T为中心进行右旋。*/
bool LRRotate(BiTree *T)
{RRRotate(&((*T)->lchild));LLRotate(T);return true;
}

4)        RL型(先单次右旋,再单次左旋)

当根结点右子树的左子树中的节点导致根结点的平衡因子为-2时,采用RL型旋转进行调整。图示为两种需进行RL型旋转的不平衡树。RL型旋转与LR型旋转相对应。

RL型旋转是先以根结点的右孩子为中心进行单次右旋,再以根结点为中心进行单次左旋。图示如下:

代码:

/** 当T的右子树的左子树上的节点使得T的平衡度为-2时,* 先以T的右子树为中心进行右旋,再以T为中心进行左旋。*/
bool RLRotate(BiTree *T)
{LLRotate(&((*T)->rchild));RRRotate(T);return true;
}

3.      插入操作

插入操作的代码如下。

/** 插入操作。* 如果以*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;}}
}

以下图为例进行两个关键点的说明:进行旋转的树为最小不平衡二叉树;插入结点之后父结点高度的递归修正。

假如要在图一二叉平衡树中插入结点1,

函数调用步骤:

1

调用函数AVLInsert(&9,1)(为表述方便,以&9代表指向结点9的指针);

2

由于1<9,继续调用AVLInsert(&7,1);

3

由于1<7,继续调用AVLInsert(&3,1);

4

由于1<3,继续调用AVLInsert(&2,1);

5

由于2<1,继续调用AVLInsert(NULL,1),此时由于*T为空树,增加结点1,并把结点1的高度设置为1,左右孩子分别为空树,如图二所示。函数返回TRUE;

6

AVLInsert(NULL,1)函数返回TRUE,并返回至AVLInsert(&2,1),因插入成功,所以更新结点2的高度为max(1,0)+1=2,结点2的平衡因子为1,不进行旋转操作,函数返回TRUE;

7

AVLInsert(&2,1)函数返回TRUE,并返回至AVLInsert(&3,1),因插入成功,所以更新结点3的高度为max(2,1)+1=3,结点3的平衡因子为1,不进行旋转操作,函数返回TRUE;

8

AVLInsert(&3,1)函数返回TRUE,并返回至AVLInsert(&7,1),因插入成功,所以更新结点7的高度为max(3,1)+1=4,结点7的平衡因子为2,进行旋转操作,旋转之后,更新结点3的高度为2,结点7的高度为2,如图三所示。函数返回TRUE;

9

AVLInsert(&7,1)函数返回TRUE,并返回至AVLInsert(&9,1),因插入成功,所以更新结点9的高度为max(2,1)+1=4,结点9的平衡因子为1,不进行旋转操作,函数返回TRUE,插入过程结束。

插入的结点一定为叶子结点,插入结点之后依次进行递归调用的返回操作,在返回之后,修正父结点的高度(2->3->7->9),之后判断父结点的平衡因子,当平衡因子超范围(结点7)时,以该结点为根结点的树为最小不平衡二叉树,此时进行旋转操作。当AVLInsert(&7,1)函数返回之后,以结点7的父结点9为根结点的树将不再需要进行旋转操作。因此每次通过函数AVLInsert()插入一个结点时,旋转操作只在最小不平衡二叉树中进行一次。已插入结点的父结点的高度是在递归过程中依次进行修正的。

4.      删除操作

删除操作的代码如下。

/** 删除操作。* 如果以*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;}}
}

关键点:

1. 当待删除结点*T既有左子树又有右子树且左子树高度大于右子树高度时,用结点*T的前驱结点pre替换*T,之后再删除前驱结点pre;当右子树高度大于左子树高度时,用结点*T的后继节点post替换结点*T,之后再删除后继结点post。这样可以保证在删除操作之后,树在不进行旋转操作的情况下仍为二叉平衡树。

2.   在删除前驱结点pre和后继结点post时,使用AVLDelete(&((*T)->lchild), pre->data)和AVLDelete(&((*T)->rchild), post->data)。这样可以保证被删除结点pre和post的父节点直至根结点的结点高度都会被递归修正一次。结点高度的递归修正同插入操作。

5.      时间复杂度

在平衡树上进行查找的过程和排序树相同,因此在查找过程中和给定值进行比较的关键字个数不超过树的深度。假设F(N)表示N层平衡二叉树的最少结点个数,则F[1]=1,F[2]=2,F(N)=F(N-2)+F(N-1)+1。在平衡树上进行查找的时间复杂度为O(logn)。

6.      测试结果

二叉平衡树的插入和删除操作相关推荐

  1. 二叉搜索树的插入与删除图解

    =================================================================== 一.二叉搜索树(BSTree)的概念        二叉搜索树又 ...

  2. 二叉搜索树的插入、删除、修剪、构造操作(leetcode701、450、669、108)

    目录 1.leetcode 701. 二叉搜索树中的插入操作 1.题目 2.递归法 3.迭代法 2.leetcode 450. 二叉搜索树中的插入操作 1.题目 2.思路+递归法 3.迭代法 4.删除 ...

  3. 二叉搜索树的插入,删除,和中序遍历

    构建一个值的类型为int的二叉搜索树,输入N和M,然后进行N次插入操作,每次插入之后进行一次遍历验证代码正确性.然后进行M次删除操作,每次删除之后进行一次遍历验证代码正确性. #include &qu ...

  4. 二叉搜索树的插入与删除(C语言)

    代码如下: BinTree Insert( BinTree BST, ElementType X ) {if( !BST ){ /* 若原树为空,生成并返回一个结点的二叉搜索树 */BST = (Bi ...

  5. 二叉堆(插入、删除)

    二叉堆分为大顶堆和小顶堆,是一颗完全二叉树.但是实际上的存储一般用数组存储. 大顶堆:任何父节点都比子节点大,但左右子节点大小情况任意. 小顶堆:任何父节点都比子节点小,但左右子节点大小情况任意. t ...

  6. 基于二叉平衡树的学生信息管理系统

    二叉平衡树的插入,删除函数参考了这位大佬的代码 详见 https://www.cnblogs.com/sench/p/7786718.html添加链接描述 对函数进行了一点改进 注意:本程序中的二叉平 ...

  7. 入门二叉平衡树的世界

    入门二叉平衡树的世界 1. 二叉平衡树的概念     二叉平衡树又称AVL树,它或者是一棵空二叉树,或者是具有下列性质的二叉树: 1) 根的左右子树高度之差的绝对值不超过1: 2) 根的左右子树都是二 ...

  8. 【Python数据结构】——二叉平衡树AVL(查找、构建、删除、插入、打印、遍历)

    #!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/7/28 20:57 # @Author : @linlianqin # @S ...

  9. 数据结构源码笔记(C语言):二叉平衡树的相关操作算法

    //二叉平衡树的相关运算 #include<stdio.h> #include<malloc.h> #include<string.h>typedef char I ...

最新文章

  1. maven的setting.xml文件配置信息【仅仅更改了一处】
  2. 技术13期:一文读懂Flink的流式处理及窗口理解
  3. 微信商城小程序操作为产品增加颜色尺寸长度等多规格内容
  4. Linux日志系统-06:案例2-实现httpd的日志滚动分割
  5. Django查询 – id vs pk
  6. python--*args和**kwargs可变参数
  7. 【学习总结】GirlsInAI ML-diary day-11-while循环
  8. JSP基础之 C标签中的 varStatues属性
  9. 希望直接访问系统内某个链接,跳过登录验证等过程
  10. 声卡loopback有什么用_萌新做音乐那点事 | 外置专业声卡的选择方法与推荐
  11. [thinkphp] page类整合bootstrap分页样式
  12. Java 学习笔记(手写版)
  13. Crime HDU - 4623(状压DP,不同进制转换)
  14. C# 中国大陆二代身份证号生成及格式验证
  15. U盘病毒的传播途径和如何安全使用U盘
  16. RNA-seq的典型流程(protocol)
  17. Linux系统的注销与关闭
  18. ×××网站与***的秘密
  19. SkyWalking安装配置,ElasticSearch存储,nexus私有maven库进行SkyWalking客户端探针的打包和拉取
  20. 如何下载Ubuntu镜像

热门文章

  1. tiktok中文叫什么?能做什么?
  2. SonarQube 数据清理,从100G 到9G
  3. oracle身份证的正则表达式,Oracle 正则表达式实例详解
  4. eNSP连接局域网和互联网
  5. win7 电脑卡住了怎么办
  6. 顶级专家谈中国脑科学研究最新成果
  7. Java命名规范【全】
  8. win10怎么设置不睡眠熄屏?win10设置永不睡眠的方法
  9. 试论我国建设世界一流大学的措施
  10. 不是光好看就够了,酷狗耳机将“把话讲清楚”推到新高度