转载:什么是平衡二叉树(AVL)(+新增理解笔记)

转载:什么是平衡二叉树

什么是平衡二叉树(AVL)

typedef int ElementType;

struct AVLNode{

<span class="kt">int</span> <span class="n">depth</span><span class="p">;</span> <span class="c1">//深度,这里计算每个结点的深度,通过深度的比较可得出是否平衡


Tree parent; //该结点的父节点

ElementType val; //结点值

Tree lchild;

<span class="n">Tree</span> <span class="n">rchild</span><span class="p">;</span>

<span class="n">AVLNode</span><span class="p">(</span><span class="kt">int</span> <span class="n">val</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">parent</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
    <span class="n">depth</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">lchild</span> <span class="o">=</span> <span class="n">rchild</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
    <span class="k">this</span><span class="o">-&gt;</span><span class="n">val</span><span class="o">=</span><span class="n">val</span><span class="p">;</span>
<span class="p">}</span>

};

5. AVL树插入时的失衡与调整

图 5.1 是一颗平衡二叉树



在此平衡二叉树插入节点 99 ,树结构变为:



在动图 5.2 中,节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2,树失去平衡。

在动图 5.2 中,以节点 66 为父节点的那颗树就称为 最小失衡子树

最小失衡子树:在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 的结点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。

平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋右旋

旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。

5.1 左旋



以图 5.1.1 为例,加入新节点 99 后, 节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2。为保证树的平衡,此时需要对节点 66 做出旋转,因为右子树高度高于左子树,对节点进行左旋操作,流程如下:

(1)节点的右孩子替代此节点位置 (2)右孩子的左子树变为该节点的右子树 (3)节点本身变为右孩子的左子树

整个操作流程如动图 5.1.2 所示。



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

5.2 右旋

右旋操作与左旋类似,操作流程为:

(1)节点的左孩子代表此节点 (2)节点的左孩子的右子树变为节点的左子树 (3)将此节点作为左孩子节点的右子树。



6. AVL树的四种插入节点方式

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

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

具体分析如下:

6.1 A的左孩子的左子树插入节点(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->lchildnode){
parent->lchild=son;
}else{
//父节点的右孩子是失衡结点
parent->rchild=son;
}
}
//设置失衡结点的父亲
node->parent=son;
//更新son结点的高度信息
update_depth(son);
return son;
}

6.2 A的右孩子的右子树插入节点(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->lchildnode){
parent->lchild=son;
}else{
//父节点的右孩子是失衡结点
parent->rchild=son;
}
}
//设置失衡结点的父亲
node->parent=son;
//更新son结点的高度信息
update_depth(son);
return son;
}

6.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);
}

6.4 A的右孩子的左子树插入节点(RL)

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

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







也就是说,经过这两步操作,使得 原来根节点的右孩子的左孩子 D 节点成为了新的根节点

代码实现:

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

补充

上述四种插入方式的代码实现的辅助代码如下:

//更新当前深度
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);
}
}

6.5 小总结

  1. 在所有的不平衡情况中,都是按照先 寻找最小不平衡树,然后 寻找所属的不平衡类别,再 根据 4 种类别进行固定化程序的操作
  2. LL , LR ,RR ,RL其实已经为我们提供了最后哪个结点作为新的根指明了方向。如 LR 型最后的根结点为原来的根的左孩子的右孩子,RL 型最后的根结点为原来的根的右孩子的左孩子。只要记住这四种情况,可以很快地推导出所有的情况。
  3. 维护平衡二叉树,最麻烦的地方在于平衡因子的维护。建议读者们根据小吴提供的图片和动图,自己动手画一遍,这样可以更加感性的理解操作。

7. AVL树的四种删除节点方式

AVL 树和二叉查找树的删除操作情况一致,都分为四种情况:

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

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

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

  • 以前三种情况为基础尝试删除节点,并将访问节点入栈。
  • 如果尝试删除成功,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空。
  • 如果尝试删除失败,证明是第四种情况。这时先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈。
  • 再依次检查栈顶节点的平衡状态和修正直到栈空。

对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了。

总结

AVL 的旋转问题看似复杂,但实际上如果你亲自用笔纸操作一下还是很好理解的。

==========================================================================================================================================
难点提示:
二叉搜索树,就是二叉树

树的查找效率取决于什么?
[
二叉搜索树的查找效率取决于树的高度,树高度越小,查找需要的次数越小,轮询次数小,所以效率就变高,
因此保持树的高度最小,即可保证树的查找效率

节点数目一定,保持树的左右两端保持平衡,树的查找效率最高
]

平衡二叉树:
[
任一节点的左右子树的高度相差不超过 1 的树为平衡二叉树

平衡二叉树,树高度最小,所以轮询时查找需要的次数最小,效率最高
]

如何判断是不是平衡二叉树?
[
不是看节点数,是看高度差
可以把所看位置的高度当作0,然后向下,一层一层依次取高度为1,2,3…

图2.1:
结点 60 位置当作高度0,左子树高度2,右子树没有,差值大于1

图2.2:节点66左子树高度3,右子树高度1,差值大于1

图2.3:同理
]

5.1 左旋
[
99节点向上看,第一个不平衡的就是66,所以需要对66左旋

左旋的看法:
把旋转的点的左边的全部断掉重新连接,然后把整体变成旋转点的左节点

右旋同理,
把旋转的点的右边的全部断掉重新连接,然后把整体变成旋转点的右节点

]

6.AVL树的四种插入节点方式
[
LL、RR、LR、RL --就是3层树的最下面4个地方

其中后面两个,先旋转是为归到同一边,变成LL或RR,然后再变

Tree LL_rotate(Tree node) --形参node是离操作结点最近的失衡的结点,本图中对应A --右旋
–按各个节点看,分别看对应的左节点、右节点、父节点
–最终保证是能够连起来就行了,这里的代码里处理是先处理E、A,然后处理B,最后处理A

Tree RR_rotate(Tree node) --同理,左旋

Tree LR_rotate(Tree node) --//LR型,先对失衡节点的左孩子做左旋转,归边变成LL,再对失衡节点做右旋转
–这里图错了,代码对,注意Tree LL_rotate是右旋,Tree RR_rotate是左旋

Tree RL_rotate(Tree node) --//RL型,先对失衡节点的右孩子做右旋转,归边变成RR,再对失衡节点做左旋转
–这里图对,代码也对,注意Tree LL_rotate是右旋,Tree RR_rotate是左旋

void update_depth(Tree node) --代码里取的是"传入节点的孩子"的深度,所以对"传入节点"来说,要+1

]

7.AVL树的四种删除节点方式
[

[
四种删除节点方式:
(1)删除叶子节点
(2)删除的节点只有左子树
(3)删除的节点只有右子树
(4)删除的节点既有左子树又有右子树

删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡节点进行修正,
而删除操作需要修正栈中的所有非平衡节点

删除操作的大致步骤如下:
以前三种情况为基础尝试删除节点,并将访问节点入栈。
如果尝试删除成功,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空。
如果尝试删除失败,证明是第四种情况。这时先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈。
再依次检查栈顶节点的平衡状态和修正直到栈空。

对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,
然后再对应上插入的四种情况选择相应的旋转就好了。
]

上面意思分析:
[
1.删除方法
对删除节点,先尝试删除,并放入堆栈,然后
如果删除成功,说明是前3种情况,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空;
如果删除失败,说明是第4种情况,然后先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈,再依
次检查栈顶节点的平衡状态和修正直到栈空
(提示:第4种情况,删除的节点是左子树和右子树都有的,删除后,需要把后续的节点接上删除的位置,就相当于删
除了被删除节点的右子树最小节点,然后接上原始删除的位置)

说明:链表和树删除后,需要把后面的接上去,而不是把后面的都删掉,这和MFC的CTree区别开来,不要混了,因为CTree
是删除某个node,如果带子节点的,会把该node和子节点都删掉

2.删除后自平衡的处理
对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了

就是把删除操作,转换成对应的插入操作,然后调用插入的4种旋转函数进行处理

删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡节点进行修正,
而删除操作需要修正栈中的所有非平衡节点
]

]

=>
总结:
[
平衡二叉树的增删改查,增加和删除后,每次都要通过左旋和右旋来维护二叉树的平衡,

具体自平衡方法是:
总共上面4种平衡情况,插入时要对插入栈中的弹出的第一个非平衡节点进行修正,最终是转换为上面的4种自平衡方法;
删除时,需要修正栈中的所有非平衡节点,删除最终是转为插入情况最终也是调用上面的4种自平衡方法

4种自平衡函数是:
Tree LL_rotate(Tree node) --右旋
Tree RR_rotate(Tree node) --左旋
Tree LR_rotate(Tree node) --//LR型,先对失衡节点的左孩子做左旋转,归边变成LL,再对失衡节点做右旋转
Tree LR_rotate(Tree node) --//RL型,先对失衡节点的右孩子做右旋转,归边变成RR,再对失衡节点做左旋转
]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值