平衡二叉树

参考网站

一、为什么要有平衡二叉树

二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如图 1.1。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)。


搜索二叉树的退化

在此二叉搜索树中查找元素 6 需要查找 6 次。
二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 A,将其改为图 1.2 的方式存储,查找元素 6 时只需比较 3 次,查找效率提升一倍。


平衡二叉树

可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。
这种左右子树的高度相差不超过 1 的树为平衡二叉树。

二、平衡二叉树的定义

typedef struct AVLNode *Tree;

typedef int ElementType;

class AVLNode{

public:
    int depth; //深度,这里计算每个结点的深度,通过深度的比较可得出是否平衡

    AVLNode* parent; //该结点的父节点

    ElementType val; //结点值

    AVLNode* lchild;

    AVLNode* rchild;

    AVLNode(int val=0) {
        this->parent = NULL;
        this->depth = 0;
        this->lchild = NULL;
        this->rchild = NULL;
        this->val=val;
    }
};

平衡二叉查找树:简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。它具有如下几个性质:
(1)可以是空树。
(2)假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。平衡之意,如天平,即两边的分量大约相同。
例如图 2.1 不是平衡二叉树,因为结点 60 的左子树不是平衡二叉树。


三、平衡因子

定义:某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。




四、AVL树插入时的失衡与调整

平衡二叉树

插入节点99后的二叉树

节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2,树失去平衡。以节点 66 为父节点的那颗树就称为 最小失衡子树。
最小失衡子树:在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 的结点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。
平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋 与 右旋 。
旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。

4.1 左旋


上图为例子加入新节点 99 后, 节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2。为保证树的平衡,此时需要对节点 66 做出旋转,因为右子树高度高于左子树,对节点进行左旋操作,流程如下:
(1)节点的右孩子替代此节点位置 (2)右孩子的左子树变为该节点的右子树 (3)节点本身变为右孩子的左子树


左旋操作

1、节点的右孩子替代此节点位置 —— 节点 66 的右孩子是节点 77 ,将节点 77 代替节点 66 的位置
2、右孩子的左子树变为该节点的右子树 —— 节点 77 的左子树为节点 75,将节点 75 挪到节点 66 的右子树位置
3、节点本身变为右孩子的左子树 —— 节点 66 变为了节点 77 的左子树

4.2 右旋

右旋操作与左旋类似,操作流程为:
(1)节点的左孩子代表此节点
(2)节点的左孩子的右子树变为节点的左子树
(3)将此节点作为左孩子节点的右子树。


右旋操作

(该操作中的数值为75节点有误,应该是65才是对的)

五、AVL树的四种插入节点方式

假设一颗 AVL 树的某个节点为 A,有四种操作会使 A 的左右子树高度差大于 1,从而破坏了原有 AVL 树的平衡性。平衡二叉树插入节点的情况分为以下四种:

判断是哪一种情况的方式:

如果存在失衡,先找离插入点最近的失衡节点,然后看插入的节点是在失衡节点的左边还是右边,如果在左边就属于L,反之就是R,然后再看插入节点的父亲节点(未插入新的节点之前为叶节点,如果插入后是失衡的)是属于左叶子还是有叶子结点,如果是左叶子节点则是L,右叶子节点为R。

插入方式 描述 旋转方式
LL 在 A 的左子树根节点的左子树上插入节点而破坏平衡 右旋转
RR 在 A 的右子树根节点的右子树上插入节点而破坏平衡 左旋转
LR 在A的左子树根节点的右子树上插入节点而破坏平衡 先左旋后右旋
RL 在 A 的右子树根节点的左子树上插入节点而破坏平衡 先右旋后左旋

无论是哪一种情况,首先找到离插入点最近的失衡节点,再针对该节点进行分析,是属于哪一种情况的插入方式。

5.1 A的左孩子的左子树插入节点 LL类型

LL类型
//LL型调整函数
//返回:新父节点
Tree LL_rotate(Tree node){
    //node为离操作结点最近的失衡的结点
    Tree parent=NULL,son;
    //获取失衡结点的父节点
    parent=node->parent;
    //获取失衡结点的左孩子
    son=node->lchild;
    //设置son结点右孩子的父指针
    if (son->rchild!=NULL)  son->rchild->parent=node;
    //失衡结点的左孩子变更为son的右孩子
    node->lchild=son->rchild;
    //更新失衡结点的高度信息
    update_depth(node);
    //失衡结点变成son的右孩子
    son->rchild=node;
    //设置son的父结点为原失衡结点的父结点
    son->parent=parent;
    //如果失衡结点不是根结点,则开始更新父节点
    if (parent!=NULL){
        //如果父节点的左孩子是失衡结点,指向现在更新后的新孩子son
        if (parent->lchild==node){
            parent->lchild=son;
        }else{
             //父节点的右孩子是失衡结点
              parent->rchild=son;
        }
     }
    //设置失衡结点的父亲
    node->parent=son;
    //更新son结点的高度信息
    update_depth(son);
    return son;
}

5.2 A的右孩子的右子树插入节点RR类型

RR类型
//RR型调整函数
//返回新父节点
Tree RR_rotate(Tree node){
    //node为离操作结点最近的失衡的结点
    Tree parent=NULL,son;
    //获取失衡结点的父节点
    parent=node->parent;
    //获取失衡结点的右孩子
    son=node->rchild;
    //设置son结点左孩子的父指针
    if (son->lchild!=NULL){
          son->lchild->parent=node;
    }
    //失衡结点的右孩子变更为son的左孩子
    node->rchild=son->lchild;
    //更新失衡结点的高度信息
    update_depth(node);
    //失衡结点变成son的左孩子
    son->lchild=node;
    //设置son的父结点为原失衡结点的父结点
    son->parent=parent;
    //如果失衡结点不是根结点,则开始更新父节点
    if (parent!=NULL){
        //如果父节点的左孩子是失衡结点,指向现在更新后的新孩子son
        if (parent->lchild==node){
            parent->lchild=son;
        }else{
            //父节点的右孩子是失衡结点
            parent->rchild=son;
        } 
    }
    //设置失衡结点的父亲
    node->parent=son;
    //更新son结点的高度信息
    update_depth(son);
    return son;
}

5.3 A的左孩子的右子树插入节点(LR类型)

若 A 的左孩子节点 B 的右子树 E 插入节点 F ,导致节点 A 失衡,如图:



A 的平衡因子为 2 ,若仍按照右旋调整,则变化后的图形为这样:



经过右旋调整发现,调整后树仍然失衡,说明这种情况单纯的进行右旋操作不能使树重新平衡。那么这种插入方式需要执行两步操作,使得旋转之后为 原来根结点的左孩子的右孩子作为新的根节点 。
(1)对失衡节点 A 的左孩子 B 进行左旋操作,即上述 RR 情形操作。 (2)对失衡节点 A 做右旋操作,即上述 LL 情形操作。

调整过程如下:




也就是说,经过这两步操作,使得 原来根节点的左孩子的右孩子 E 节点成为了新的根节点。
//LR型,先左旋转,再右旋转
//返回:新父节点
Tree LR_rotate(Tree node){
    RR_rotate(node->lchild);
    return LL_rotate(node);
}

5.4 A的右孩子的左子树插入节点(RL类型)

右孩子插入左节点的过程与左孩子插入右节点过程类似,也是需要执行两步操作,使得旋转之后为 原来根结点的右孩子的左孩子作为新的根节点 。

(1)对失衡节点 A 的右孩子 C 进行右旋操作,即上述 LL 情形操作。 (2)对失衡节点 A 做左旋操作,即上述 RR 情形操作。




//RL型,先右旋转,再左旋转
//返回:新父节点
Tree RL_rotate(Tree node){
    LL_rotate(node->rchild);
    return RR_rotate(node);
}

5.5 辅助代码

//更新当前深度
void update_depth(Tree node){
    if (node==NULL){
        return;
    }else{
        int depth_Lchild=get_balance(node->lchild); //左孩子深度
        int depth_Rchild=get_balance(node->rchild); //右孩子深度
        node->depth=max(depth_Lchild,depth_Rchild)+1;
    }
}

//获取当前结点的深度
int get_balance(Tree node){
    if (node==NULL){
         return 0;
    }
    return node->depth;
}

//返回当前平衡因子
int is_balance(Tree node){
    if (node==NULL){
         return 0;
    }else{
         return get_balance(node->lchild)-get_balance(node->rchild); 
    }
}

六、AVL树删除节点

(1)删除叶子节点 (2)删除的节点只有左子树 (3)删除的节点只有右子树 (4)删除的节点既有左子树又有右子树

只不过 AVL 树在删除节点后需要重新检查平衡性并修正,同时,删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡节点进行修正,而删除操作需要修正栈中的所有非平衡节点。

删除操作的大致步骤如下:

(1)以前三种情况为基础尝试删除节点,并将访问节点入栈。
(2)如果尝试删除成功,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空。
(3)如果尝试删除失败,证明是第四种情况。这时先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈。(找左子树最大的节点,找右子树最小的节点,确保可以要删除的节点交换后是不会影响查找二叉树的性质的)
(4)再依次检查栈顶节点的平衡状态和修正直到栈空。
对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了。

利用栈的特性来保存遍历平衡二叉树的节点,其实在插入的时候也可以这么操作,在AVL Tree中插入数据的时候,可以根据二叉查找树的特性来找到要插入的叶子位置,在寻找对应位置的时候将遍历到的节点一个个都进栈,插入完了之后,一个个出栈来更新其深度,顺便判断到离插入点最近的失衡节点是哪一个。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342