AVL树
概念
苏联科学家发明的
节点的平衡因子
节点左右子树的高度差
AVL树特点
每个节点的平衡因子<1
增删改查的时间复杂度都是O(logN)
二叉树\二叉搜索树\AVL树继承结构
AVL树和红黑树都是二叉搜索树
AVL树添加操作
由于节点的添加是随机的,没法干预,只能再节点添加或者删除操作做完之后,对此节点进行调整,从而维持树的平衡.
(1)在添加节点之后增加了 afterAdd() 用于调整平衡;
(2)在删除节点之后增加了 afterRemove() 用于调整平衡;
1.添加导致失衡分析
(1)添加节点,不会导致直接父节点失衡
父节点有两种情况:叶子结点或者度为1,二者添加子节点都不会失衡。
(2)祖先节点可能会失衡
1.当祖先节点的左右子树的高度差为0,添加不会导致失衡。
2.当祖先节点的左右子树高度差为1,添加可能会导致失衡,并且会造成连锁反应。
(3)非祖先节点都不会失衡
2.修复平衡的操作
LL – 右旋转(单旋)
RR – 左旋转(单旋)
LR – 先左旋,再右旋(双旋)
RL – 先右旋,再左旋(双旋)
经过旋转后仍然是二叉平衡树
有些教程里面:
• 把右旋转叫做 zig,旋转之后的状态叫做 zigged
• 把左旋转叫做 zag,旋转之后的状态叫做 zagged
(1) LL- 右旋转
前提:
添加节点的父节点:n
n的父节点p
p的父节点g
右旋转的情况:
n = p.left
p = g.left
n节点下添加了新的节点,导致g节点失衡
处理方法:右旋转
让p的右子树变为g的左子树
g变为p的右子节点
右旋:
变成当前节点左子节点的右子节点
右旋的特点
:对g进行右旋转,我们假设g的父节点为G,那么旋转后会发现G的子节点为p,并且G子树由添加后失衡变为回复平衡;回复平衡后的G子树的高度和添加节点前的高度是一样的,因此整个二叉树恢复了平衡。
(2) RR – 左旋转(单旋)
RR情况:
n = p.right
p = g.right
失衡节点:g
处理方式:g节点左旋
左旋特点:g变成右子节点的左子节点
(3)LR
LR处理方式:
先对p进行左旋 ,变成LL
在对g进行右旋
LR特点:n变为子节点
(4)LR
添加后修复总结
- 修复操作核心:不改变BST性质
- 修复操作特点:对于添加新节点后的失衡节点g来说,修复平衡后g的高度是不变的!
由于修复后,g高度不变,因此g的祖先节点在g恢复平衡之后就恢复平衡了
那么我们只需要修复g点即可!
3.afterAdd
修复平衡的操作是在一个新的节点添加进去完成后,开始进行修复;
因此需要在add()方法中 元素成功添加进去之后添加一个afterAdd()进行修复
1.二叉搜索树代码调整:
1.在二叉搜索树中添加一个afterAdd()方法,但是自身不实现这个方法
2.继承的AVL树中实现afterAdd()方法即可
如此一来,AVL树能实现平衡而不影响二叉搜索树
(1)添加方法:
//todo 提供一个add后的操作,普通BST中不做任何实现
protected void afterAdd(Node<E> node){}
(2)修改add()方法: 尾部插入afterAdd()
public void add(E element){
elementNotNullCheck(element);
if(root == null){
//root = new Node(element,null);
root = createNode(element,null);
size++;
afterAdd(root);
return;
}
//1.新节点插到哪个节点下面?
//2.新节点插到该节点的左边还是右边?
Node<E> node = root;
Node<E> parent = null;
int cmp = 0;
while(node!=null){
parent = node;
cmp = Compare(node.element,element);
if(cmp > 0){
node = node.left;
}else if(cmp < 0){
node = node.right;
}else{
return;
}
}
//Node<E> newNode = new Node(element,parent);
Node<E> newNode = createNode(element,parent);
if(cmp>0){
parent.left = newNode;
}else{
parent.right = newNode;
}
size++;
afterAdd(newNode);
}
2.AVL树实现afterAdd(Node)
@Override
protected void afterAdd(Node<E> node) {
}
1.明确失衡节点是谁,怎么修复
按照前面分析,失衡节点只会是node的祖先节点
只要修复了高度最低的失衡祖先节点,那么整个二叉树就平衡了
伪代码:
protected void afterAdd(Node<E> node) {
while((node = node.parent ) != null){
// if(node 是平衡的){
//
// }else{
//
// }
}
//出来了,node = null
}
2.1 判断平衡
根据节点的平衡因子来判断是否平衡;
平衡因子和节点的左右子树的高度有关系;
-> 因此可以对node类进行修改,在node中维护一个height属性
->但是只有在AVL树中需要height属性,普通BST不需要height属性
解决方法
在AVL树中自己维护一个Node类,继承BST中的Node
private static class AVLNode<E> extends Node<E>{
private int height = 1; // 叶子节点的高度为1
AVLNode(){}
AVLNode(E element,Node<E> parent){
this.element = element;
this.parent = parent;
}
问题2
在AVL树中维护了自己的AVLNode之后,但是add()方法是继承自BST的,那么就需要对创建Node进行一个封装,返回父类Node类型
protected Node<E> createNode(E element,Node<E> parent){
return new Node(element,parent);
}
在AVLTree中,就要override此方法,返回AVLNode对象
@Override
protected Node<E> createNode(E element, Node<E> parent) {
return new AVLNode<E>(element,parent);
}
如此,在add()方法中,创建新的node的时候,就各自维护自己的node
AVLNode中添加有关平衡因子的方法
(1)由于left和right属性是从Node类继承过来的,因此这里需要进行强转才能使用height属性
//获取节点的平衡因子
public int balanceFacotr(){
int leftHeight = left==null? 0 : ((AVLNode<E>)left).height;
int rightHeight = right==null? 0 : ((AVLNode<E>)right).height;
return leftHeight - rightHeight;
}
在AVLTree中添加判断节点是否平衡的方法:
private boolean isBalanced(Node<E> node){
return Math.abs(((AVLNode<E>)node).balanceFacotr()) <= 1;
}
2.2更新高度
在AVLNode中添加更新高度的方法:
//更新节点的高度
public void updateHeight(){
int leftHeight = left==null? 0 : ((AVLNode<E>)left).height;
int rightHeight = right==null? 0 : ((AVLNode<E>)right).height;
height = Math.max(leftHeight,rightHeight) + 1;
}
在AVLTree中:
private void updateHeight(Node<E> node){
((AVLNode<E>)node).updateHeight();
}
问题1:一个节点什么时候会被更新高度:
(1)只有添加的新节点node的祖先节点会更新高度;
(2)失衡节点g以及g的祖先节点不需要更新高度
因此如果要更新AVL树中节点的高度,需要从新节点遍历到g点,此间的每个节点需要更新高度
在伪代码中:
protected void afterAdd(Node<E> node) {
while((node = node.parent ) != null){
// if(isBalanced(node)){
//
// }else{
//
// }
}
//出来了,node = null
}
在获取平衡因子balanceFacotr()的时候是依赖左右子节点的高度的
我们需要沿着新节点的parent,挨个用isBalanced(node)
判断祖先节点是否是平衡的,如果是平衡的,那就更新高度;如果不是平衡的,说明遇到g点了,此时就要做恢复平衡操作,并且g点往后不需要再更新高度了(参考:添加后修复总结的说明)
protected void afterAdd(Node<E> node) {
//沿着parent一直往上找到第一个失衡的父节点
while((node=node.parent)!= null){
//新节点的父节点肯定是平衡的,因此从这往上一直更新到
//失衡祖宗节点的高度
if(isBalanced(node)){
updateHeight(node);
}else{
//这是第一个不平衡的节点
rebalance(node);
break;//只需要恢复第一个不平衡节点即可
}
}
}
2.3恢复平衡
恢复平衡的时候,我们必须要知道是哪种场景;
以LL为例,我们会发现p是g的较高子节点;同理,n是p的较高子节点;
也就是说:
1.获取g的较高子节点p,判断g和p的关系
2.获取p的较高子节点n,判断n和p的关系
在BinaryTree的Node中添加两个辅助方法:
public boolean isLeftChild(){ // 判断自己是不是左子树
return parent!=null && this==parent.left;
}
public boolean isRightChild(){ // 判断自己是不是右子树
return parent!=null && this==parent.right;
}
在AVLNode中:
//返回较高的子节点,如果一样高,返回同方向的子节点
public Node<E> tallerChild(){
int leftHeight = left==null? 0 : ((AVLNode<E>)left).height;
int rightHeight = right==null? 0 : ((AVLNode<E>)right).height;
Node<E> ReNode = leftHeight > rightHeight? left : right;
if(leftHeight == rightHeight){//返回相同方向的
if(this.isLeftChild()){
ReNode = left;
}else{
ReNode = right;
}
}
return ReNode;
}
}
balance方法:
//给Node恢复平衡 node是高度最低的失衡节点g
private void rebalance(Node<E> node){
//必须得获取n\p\g的关系,才能知道是四种失衡情况的哪一种
//p是g左右子树中高度最高的节点
//n是p左右子树中高度最高的节点
Node<E> parent = ((AVLNode<E>)node).tallerChild();
Node<E> n = ((AVLNode<E>)parent).tallerChild();
if(parent.isLeftChild()){
if(n.isLeftChild()){
//LL
rotateRight(node);
}else{
//LR
rotateLeft(parent);
rotateRight(node);
}
}else{
if(n.isLeftChild()){
//RL
rotateRight(parent);
rotateLeft(node);
}else{
//RR
rotateLeft(node);
}
}
}
旋转操作:
//左旋转
protected void rotateLeft(Node<E> node) {
Node<E> parent = node.right;
Node<E> child = parent.left;
parent.left = node;
node.right = child;
//更新父子关系
afterRotate(node,parent,child);
//旋转之后,要更新grand和parent的高度
updateHeight(node);
updateHeight(p);
}
protected void rotateRight(Node<E> node){
Node<E> parent = node.left;
Node<E> child = parent.right;
parent.right = node;
node.left = child;
//更新父子关系
afterRotate(node,parent,child);
//旋转之后,要更新grand和parent的高度
updateHeight(node);
updateHeight(p);
}
protected void afterRotate(Node<E> grand,Node<E> parent,Node<E> child){
//todo 1.更新parent的 父子关系
parent.parent = grand.parent;
if(grand.isLeftChild()){
grand.parent.left = parent;
}else if(grand.isRightChild()){
grand.parent.right = parent;
}else{
root = parent;
}
//todo 2.更新child的父子关系
if(child != null){
child.parent = grand;
}
//todo 3.更新grand的父子关系
grand.parent = parent;
}
2.4恢复平衡的统一操作
g>f>e>d>c>b>a
四种情况,最终旋转完毕的结果都是一样的,d成为根节点
a和b的关系以及f和g的关系始终不变,因此a和g可以不处理。
private void rotate(
Node<E> r, // 子树的根节点
Node<E> b, Node<E> c,
Node<E> d,
Node<E> e, Node<E> f) {
// 让d成为这颗子树的根结点
d.parent = r.parent;
if (r.isLeftChild()) {
r.parent.left = d;
} else if (r.isRightChild()) {
r.parent.right = d;
} else {
root = d;
}
// b-c
b.right = c;
if (c != null) {
c.parent = b;
}
updateHeight(b);
// e-f
f.left = e;
if (e != null) {
e.parent = f;
}
updateHeight(f);
// b-d-f
d.left = b;
d.right = f;
b.parent = d;
f.parent = d;
updateHeight(d);
}
注意:
只有左右子树改变的需要更新高度
更新高度的顺序必须是先更新低的节点
对应的修改rebalance方法:
4.AVL树删除操作
1.删除节点造成的失衡分析
删除节点,导致直接父节点 或 祖先节点 失衡
删除节点只会导致一个节点失衡
分析:
失衡节点在失衡前肯定是平衡的且平衡因子必须为1
因此删除节点后,该失衡节点的平衡因子变成了2
这意味着删除了该失衡节点的较短的一条腿
那么就不会影响失衡节点的高度
既然失衡节点高度不会变,那么失衡节点的祖先节点的高度就不会受到影响;
因此只会导致一个节点失衡;
2.删除导致失衡的恢复方案
1)LL
恢复平衡的过程中,可能会引发连锁反应
如图所示,删除的节点是红色的,当红色节点被删除,变成LL的情况,此时需要对g进行右旋转。
如果绿色节点不存在,旋转之后,该子树的高度-1,这就有可能引发连锁反应。
极端情况下被删除节点的所有祖先节点都需要进行恢复平衡的操作,共O(logn) 次
调整
说白了,对节点g进行rebalance操作,会导致以g为根节点的子树的高度降低1,这和删除一个节点造成的情况是一样的!
因此在remove方法中找到被删除节点del之后,对del进行删除
然后从del节点的parent开始找失衡节点,不管节点是否失衡,都需要更新高度
2)RR
3)LR
4)RL
3.删除代码实现
添加afterRemove(Node node)
由于节点的删除也是随机的,因此只能在删除操作完成之后,添加一个后处理方法,对树进行平衡恢复
1.方法添加位置
在节点被删除后;
这里需要注意:参数node是被删除的节点,对于度为2的节点的删除,删除的是其前驱节点或者后继节点。
private void remove(Node<E> node){
if(node == null) return;
//先处理度为2的节点,因为也是删除度为1的节点
// 找到要被删除的节点
Node<E> del = null;
if(node.left != null && node.right != null){
del = predesessor(node);//前继节点
node.element = del.element;
}else{
del = node;
}
Node<E> child = del.left == null ? del.right:del.left;
if(child != null){
child.parent = del.parent;
}
if(del.parent == null){
root = child;
}else{
if(del == del.parent.left){
del.parent.left = child;
}else{
del.parent.right = child;
}
}
//节点真正被删除的时候删除
afterRemove(del);
size--;
}
2.具体的实现
protected void afterRemove(Node<E> node) {
//沿着parent一直往上找到第一个失衡的父节点
while((node=node.parent)!= null){
//新节点的父节点肯定是平衡的,因此从这往上一直更新到
//失衡祖宗节点的高度
if(isBalanced(node)){
updateHeight(node);
}else{
//这是第一个不平衡的节点
rebalance(node);
//break;
}
}
}
- 对比 afterAdd()只需要修复最低的失衡节点,因此在修复后break终止循环;而删除节点可能会引发多个祖父节点失衡,因此afterRemove()只需要去掉break即可。
- 需要注意的是:在删除节点的过程中,我们并没有修改del节点的parent,因此node=node.parent是一直有效的。
- 当节点是平衡的 只做高度的更新
- 当节点是失衡的,做恢复平衡的操作(在afterRotate()中有对失衡节点的高度更新操作)
所以:删除操作,del节点的所有祖先节点都需要更新高度 增加操作,高度更新只需要到失衡节点即可
AVL树总结
- AVL树在二叉搜索树引入了平衡因子的概念;通过维护每个节点的平衡因子<1 从而实现二叉搜索树不会退化成链表;
- AVL树核心操作是在BST的基础上增加了afterAdd()和afterRemove()两个操作,对增和删操作进行平衡修复
添加操作
失衡:添加导致g点失衡,g点的高度会改变,从而可能会导致g点所有祖先节点都失衡
失衡节点不会是直接父节点
修复:修复后的g点高度和失衡前的g点高度是一样的,因此只要让高度最低的失衡节点恢复平衡,整棵树就恢复平衡
【仅需 O(1) 次调整】
- 删除
失衡:删除导致g点失衡,但是g的高度不会改变,从而只会造成一个节点失衡。
失衡节点可能是直接父节点或 祖先节点
修复:g点恢复平衡可能会改变g点高度,因此恢复平衡后,可能会导致更高层的祖先节点失衡
【最多需要 O(logn) 次调整】
平均时间复杂度
- 搜索:O(logn)
- 添加:O(logn),仅需 O(1) 次的旋转操作
- 删除:O(logn),最多需要 O(logn) 次的旋转操作