一、什么是平衡二叉树?

平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉排序树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。

我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是-1、0和1。距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树

平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。

在插入过程中,当最小不平衡子树根结点的平衡因子 BF 大于 1 时,就右旋;小于 -1 时就左旋。插入结点后,最小不平衡子树的 BF 与它的子树的 BF 符号相反时,就需要对结点先进行一次旋转以使得符号相同后,再反向旋转一次才能够完成平衡操作。

局限性:由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当然,如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树。

二、平衡二叉树不平衡的情形

把需要重新平衡的结点叫做 α(下面是 6 和2),由于任意两个结点最多只有两个儿子,因此高度不平衡时,α 结点的两颗子树的高度相差 2。容易看出,这种不平衡可能出现在下面 4 中情况中:

情形 1 和情形 4 是关于 α 的镜像对称,二情形 2 和情形 3 也是关于 α 的镜像对称,因此理论上看只有两种情况,但编程的角度看还是四种情形。

第一种情况是插入发生在 “外边” 的情形(左左或右右),该情况可以通过一次单旋转完成调整;第二种情况是插入发生在 “内部” 的情形(左右或右左),这种情况比较复杂,需要通过双旋转来调整。

三、调整措施

一、单旋转

上图是左左的情况,k2 结点不满足平衡性,它的左子树 k1 比右子树 z 深两层,k1 子树中更深的是 k1 的左子树 x,因此属于左左情况。

为了恢复平衡,我们把 x 上移一层,并把 z 下移一层,但此时实际已经超出了 AVL 树的性质要求。为此,重新安排结点以形成一颗等价的树。为使树恢复平衡,我们把 k2 变成这棵树的根节点,因为 k2 大于 k1,把 k2 置于 k1 的右子树上,而原本在 k1 右子树的 Y 大于 k1,小于 k2,就把 Y 置于 k2 的左子树上,这样既满足了二叉查找树的性质,又满足了平衡二叉树的性质。

这种情况称为单旋转。

二、双旋转

对于左右和右左两种情况,单旋转不能解决问题,要经过两次旋转。

对于上图情况,为使树恢复平衡,我们需要进行两步,第一步,把 k1 作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以 k2 为根的平衡二叉树。

四、平衡二叉树实现算法

二叉排序树的结点结构:(增加一个 bf,用来存储平衡因子)

// 二叉树的二叉链表结点结构定义
// 结点结构
typedef struct BiTNode
{
// 结点数据
int data;
// 结点的平衡因子
int bf;
// 左右孩子指针
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

4.1 左旋操作

对于左旋操作,代码如下:

// 对以指针T所指结点为根的二叉树作左平衡旋转处理
// 本算法结束时,指针T指向新的根结点
void leftBalance(BiTree *T)
{
BiTree L, Lr;
L = (*T)->lchild; // L指向T的左子树根结点 // 检查T的左子树的平衡度,并作相应平衡处理
switch (L->bf)
{
case LH: // 新结点插入在T的左孩子的左子树上,要作单右旋处理
(*T)->bf = L->bf = EH;
rightRotate(T);
break; case RH: // 新结点插入在T的左孩子的右子树上,要作双旋处理
Lr = L->rchild; // Lr指向T的左孩子的右子树根 // 修改T及其左孩子的平衡因子
switch (Lr->bf)
{
case LH:
(*T)->bf = RH;
L->bf = EH;
break; case EH:
(*T)->bf = L->bf = EH;
break; case RH: (*T)->bf = EH;
L->bf = LH;
break;
} Lr->bf = EH;
leftRotate(&(*T)->lchild); // 对T的左子树作左旋平衡处理
rightRotate(T); // 对T作右旋平衡处理
}
}

4.2 右旋操作

右旋操作代码如下:

// 对以指针T所指结点为根的二叉树作右平衡旋转处理,
// 本算法结束时,指针T指向新的根结点
void rightBalance(BiTree *T)
{
BiTree R, Rl;
R = (*T)->rchild; // R指向T的右子树根结点 // 检查T的右子树的平衡度,并作相应平衡处理
switch (R->bf)
{
// 新结点插入在T的右孩子的右子树上,要作单左旋处理
case RH:
(*T)->bf = R->bf = EH;
leftRotate(T);
break; // 新结点插入在T的右孩子的左子树上,要作双旋处理
case LH:
Rl = R->lchild; // Rl指向T的右孩子的左子树根 // 修改T及其右孩子的平衡因子
switch (Rl->bf)
{
case RH:
(*T)->bf = LH;
R->bf = EH;
break; case EH:
(*T)->bf = R->bf = EH;
break; case LH:
(*T)->bf = EH;
R->bf = RH;
break;
} Rl->bf = EH;
rightRotate(&(*T)->rchild); // 对T的右子树作右旋平衡处理
leftRotate(T); // 对T作左旋平衡处理
}
}

总结:右旋,则原左子树的根结点变新树根结点,它的右子树根结点是原来的树根结点,那么它原来的右子树根结点呢?变成原来的树根结点(即现在的右子树根结点)的左子树根结点,因为此结点的左子树变成新的树根结点了,左子树也就空缺了。左旋类似。

五、完整实现

实现代码如下:

#include <stdio.h>
#include <stdlib.h> #define TRUE 1
#define FALSE 0
#define MAXSIZE 100 /* 存储空间初始分配量 */ typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如TRUE等 */ /* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
int data; // 结点数据
int bf; // 结点的平衡因子
struct BiTNode *lchild, *rchild; // 左右孩子指针
} BiTNode, *BiTree; // 对以p为根的二叉排序树作右旋处理,
// 处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点
void rightRotate(BiTree *P)
{
BiTree L;
L = (*P)->lchild; // L指向P的左子树根结点
(*P)->lchild = L->rchild; // L的右子树挂接为P的左子树
L->rchild = (*P);
*P = L; // P指向新的根结点
} // 对以P为根的二叉排序树作左旋处理,
// 处理之后P指向新的树根结点,即旋转处理之前的右子树的根结点0
void leftRotate(BiTree *P)
{
BiTree R;
R = (*P)->rchild; // R指向P的右子树根结点
(*P)->rchild = R->lchild; // R的左子树挂接为P的右子树
R->lchild = (*P);
*P = R; // P指向新的根结点
} #define LH +1 // 左高
#define EH 0 // 等高
#define RH -1 // 右高 // 对以指针T所指结点为根的二叉树作左平衡旋转处理
// 本算法结束时,指针T指向新的根结点
void leftBalance(BiTree *T)
{
BiTree L, Lr;
L = (*T)->lchild; // L指向T的左子树根结点 // 检查T的左子树的平衡度,并作相应平衡处理
switch (L->bf)
{
case LH: // 新结点插入在T的左孩子的左子树上,要作单右旋处理
(*T)->bf = L->bf = EH;
rightRotate(T);
break; case RH: // 新结点插入在T的左孩子的右子树上,要作双旋处理
Lr = L->rchild; // Lr指向T的左孩子的右子树根 // 修改T及其左孩子的平衡因子
switch (Lr->bf)
{
case LH:
(*T)->bf = RH;
L->bf = EH;
break; case EH:
(*T)->bf = L->bf = EH;
break; case RH: (*T)->bf = EH;
L->bf = LH;
break;
} Lr->bf = EH;
leftRotate(&(*T)->lchild); // 对T的左子树作左旋平衡处理
rightRotate(T); // 对T作右旋平衡处理
}
} // 对以指针T所指结点为根的二叉树作右平衡旋转处理,
// 本算法结束时,指针T指向新的根结点
void rightBalance(BiTree *T)
{
BiTree R, Rl;
R = (*T)->rchild; // R指向T的右子树根结点 // 检查T的右子树的平衡度,并作相应平衡处理
switch (R->bf)
{
// 新结点插入在T的右孩子的右子树上,要作单左旋处理
case RH:
(*T)->bf = R->bf = EH;
leftRotate(T);
break; // 新结点插入在T的右孩子的左子树上,要作双旋处理
case LH:
Rl = R->lchild; // Rl指向T的右孩子的左子树根 // 修改T及其右孩子的平衡因子
switch (Rl->bf)
{
case RH:
(*T)->bf = LH;
R->bf = EH;
break; case EH:
(*T)->bf = R->bf = EH;
break; case LH:
(*T)->bf = EH;
R->bf = RH;
break;
} Rl->bf = EH;
rightRotate(&(*T)->rchild); // 对T的右子树作右旋平衡处理
leftRotate(T); // 对T作左旋平衡处理
}
} // 若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个
// 数据元素为e的新结点,并返回1,否则返回0。若因插入而使二叉排序树
// 失去平衡,则作平衡旋转处理,布尔变量taller反映T长高与否。
Status insertAVL(BiTree *T, int e, Status *taller)
{
// 插入新结点,树“长高”,置taller为TRUE
if (!*T)
{
*T = (BiTree)malloc(sizeof(BiTNode));
(*T)->data = e;
(*T)->lchild = (*T)->rchild = NULL;
(*T)->bf = EH;
*taller = TRUE;
}
else
{
// 树中已存在和e有相同关键字的结点则不再插入
if (e == (*T)->data)
{
*taller = FALSE; return FALSE;
} // 应继续在T的左子树中进行搜索
if (e<(*T)->data)
{
// 未插入
if (!insertAVL(&(*T)->lchild, e, taller))
return FALSE; // 已插入到T的左子树中且左子树“长高”
if (*taller)
{
// 检查T的平衡度
switch ((*T)->bf)
{
case LH: // 原本左子树比右子树高,需要作左平衡处理
leftBalance(T);
*taller = FALSE;
break; case EH: // 原本左、右子树等高,现因左子树增高而使树增高
(*T)->bf = LH;
*taller = TRUE;
break; case RH: // 原本右子树比左子树高,现左、右子树等高
(*T)->bf = EH;
*taller = FALSE;
break;
}
} }
else
{
// 应继续在T的右子树中进行搜索
if (!insertAVL(&(*T)->rchild, e, taller)) // 未插入
return FALSE; // 已插入到T的右子树且右子树“长高”
if (*taller)
{
// 检查T的平衡度
switch ((*T)->bf)
{
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;
} // 中序递归遍历
void inOrderTraverse(BiTree T)
{
// 判断二叉树是否存在
if (T == NULL)
return; inOrderTraverse(T->lchild); // 中序遍历左子树
printf("%d ", T->data); // 显示结点数据,可以更改为其它对结点操作
inOrderTraverse(T->rchild); // 最后中序遍历右子树
} int main(void)
{
int a[10] = { 3, 2, 1, 4, 5, 6, 7, 10, 9, 8 };
BiTree T = NULL;
Status taller; // 插入操作
for (int i = 0; i<10; i++)
{
insertAVL(&T, a[i], &taller);
} // 中序递归遍历
printf("中序递归遍历:");
inOrderTraverse(T);
printf("\n"); printf("\n另外,本样例建议断点跟踪查看平衡二叉树结构\n\n"); return 0;
}

输出结果如下图所示:

参考:

《大话数据结构 - 第8章》 查找

平衡二叉树详解

[数据结构 - 第6章] 树之二叉平衡树(C语言实现)的更多相关文章

  1. [数据结构 - 第6章] 树之链式二叉树(C语言实现)

    一.什么是二叉树? 1.1 定义 二叉树,是度为二的树,二叉树的每一个节点最多只有二个子节点,且两个子节点有序. 1.2 二叉树的重要特性 (1)二叉树的第 i 层上节点数最多为 2n-1: (2)高 ...

  2. AVL树(二叉平衡树)详解与实现

    AVL树概念 前面学习二叉查找树和二叉树的各种遍历,但是其查找效率不稳定(斜树),而二叉平衡树的用途更多.查找相比稳定很多.(欢迎关注数据结构专栏) AVL树是带有平衡条件的二叉查找树.这个平衡条件必 ...

  3. 各种查找算法的选用分析(顺序查找、二分查找、二叉平衡树、B树、红黑树、B+树)

    目录 顺序查找 二分查找 二叉平衡树 B树 红黑树 B+树 参考文档 顺序查找 给你一组数,最自然的效率最低的查找算法是顺序查找--从头到尾挨个挨个遍历查找,它的时间复杂度为O(n). 二分查找 而另 ...

  4. java项目---用java实现二叉平衡树(AVL树)并打印结果(详)(3星)

    package Demo; public class AVLtree { private Node root; //首先定义根节点 private static class Node{ //定义Nod ...

  5. HDU 1166 敌兵布阵(线段树 or 二叉索引树)

    http://acm.hdu.edu.cn/showproblem.php?pid=1166 题意:第一行一个整数T,表示有T组数据. 每组数据第一行一个正整数N(N<=50000),表示敌人有 ...

  6. 【数据结构05】红-黑树基础----二叉搜索树(Binary Search Tree)

    目录 1.二分法引言 2.二叉搜索树定义 3.二叉搜索树的CRUD 4.二叉搜索树的两种极端情况 5.二叉搜索树总结 前言 在[算法04]树与二叉树中,已经介绍过了关于树的一些基本概念以及二叉树的前中 ...

  7. 从零开始学算法---二叉平衡树(AVL树)

    先来了解一些基本概念: 1)什么是二叉平衡树? 之前我们了解过二叉查找树,我们说通常来讲, 对于一棵有n个节点的二叉查找树,查询一个节点的时间复杂度为log以2为底的N的对数. 通常来讲是这样的, 但 ...

  8. Algorithms: 二叉平衡树(AVL)

    二叉平衡树(AVL):   这个数据结构我在三月份学数据结构结构的时候遇到过.但当时没调通.也就没写下来.前几天要用的时候给调好了!详细AVL是什么,我就不介绍了,维基百科都有.  后面两月又要忙了. ...

  9. 判断一颗二叉树是否为二叉平衡树 python 代码

    输入一颗二叉树,判断这棵树是否为二叉平衡树.首先来看一下二叉平衡树的概念:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树.因此判断一颗二叉平衡树的关键在于 ...

随机推荐

  1. Mobx | 强大的状态管理工具 | 可以用Mobx来替代掉redux

    来源简书 电梯直达 https://www.jianshu.com/p/505d9d9fe36a Mobx是一个功能强大,上手非常容易的状态管理工具.就连redux的作者也曾经向大家推荐过它,在不少情 ...

  2. 利用python画小猪佩奇

    import turtle as t t.pensize(4) t.hideturtle() t.colormode(255) t.color((255,155,192),"pink&quo ...

  3. 用户路径分析(User Path Analysis)

    什么是用户路径? 用户路径即抽象用户在网站或APP中的访问路径.其可用桑基图展现,称为用户路径图. 什么是用户路径分析? 用户路径分析追踪用户从某个开始事件直到结束事件的行为路径,即对用户流向进行监测 ...

  4. PHP Record the number of login users

    Function to record how many times the user logs in Connect to the database first: you can create a n ...

  5. K8s configMap原理介绍

    给容器内应用程序传递参数的实现方式: 1. 将配置文件直接打包到镜像中,但这种方式不推荐使用,因为修改配置不够灵活. 2. 通过定义Pod清单时,指定自定义命令行参数,即设定 args:[" ...

  6. nginx 常用全局变量

    变量 说明 $args 请求中的参数,如www.123.com/1.php?a=1&b=2的$args就是a=1&b=2 $content_length HTTP请求信息里的" ...

  7. curl 设置超时时间

    使用CURL时,有两个超时时间:一个是连接超时时间,另一个是数据传输的最大允许时间.连接超时时间用--connect-timeout参数来指定,数据传输的最大允许时间用-m参数来指定. curl -- ...

  8. java 多页pdf转化为多张图片

    相关jar包: <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian ...

  9. sql脱库的几种方法

    当发现sql注入之后,脱库的方法,有以下几种:   (1)当目标主机支持外部连接时,使用Navicat 进行连接!当时目标主机不同,使用的Navicat种类不一样: mysql : Navicat f ...

  10. Transform the vot dataset into 4 corner format

    Transform the vot dataset into 4 corner format Matlab code to change the 8 value ground truth into 4 ...