1.AVL树介绍

  前面我们已经介绍了二叉搜索树。普通的二叉搜索树在插入、删除数据时可能使得全树的数据分布不平衡,退化,导致二叉搜索树最关键的查询效率急剧降低。这也引出了平衡二叉搜索树的概念,平衡二叉搜索树在此前的基础上,通过一系列的等价变换使二叉搜索树得以始终处于"平衡"的状态,拥有稳定且高效的查询效率。
  AVL树是最早被计算机科学家发明的自平衡二叉搜索树,AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

  不同的平衡二叉搜索树对所谓"平衡"有不同的定义,AVL树认为当所有节点的左右子树高度之差不超过正负1时,全树是平衡的

  在插入新节点和删除旧节点之后,AVL树会判断当前是否有节点失去平衡,并对失衡的节点进行相应的重平衡操作,恢复到AVL树定义的适度平衡状态。

2.AVL树重平衡原理

2.1 等价变换介绍

  二叉搜索树有一个重要的特性:所存储元素的中序遍历次序是有序的,这也是二叉搜索树能够使用二分法加速查找的基础。

  对于同一中序遍历次序,二叉搜索树的拓扑结构可以是多样的,在保持二叉搜索树中序遍历次序不变的情况下(等价),对其拓扑结构进行一系列变换(变换),被称为等价变换

  等价变换是平衡二叉搜索树重平衡操作的理论基础。

  自己动手实现java数据结构(七) AVL树-LMLPHP

2.2 等价变换方式

  等价变换的基本操作有两种:

    顺时针旋转(zig),可以形象的理解为x->y箭头方向下午两点变成了下午四点,也被称为右旋

    逆时针旋转(zag),可以形象的理解为y->x箭头方向上午十点变成了上午八点,也被称为左旋

  顺时针旋转和逆时针旋转的操作是互逆的,复杂等价变换操作可以看成是由这两种基本操作组合而成。

顺时针旋转:

  自己动手实现java数据结构(七) AVL树-LMLPHP

逆时针旋转:

  自己动手实现java数据结构(七) AVL树-LMLPHP

2.3 AVL树的失衡状态及重平衡

在了解如何恢复AVL树平衡状态之前,需要先理解几个关键点:

  1.无论是插入新节点还是删除老节点,最多只会导致插入/删除节点位置的上层的历代祖先节点失去平衡(数量大致为 logN),而不会导致全树节点都失去平衡,因此只需要判断其历代祖先节点的平衡状态,即可判断其是否破坏了AVL树的平衡状态

  2.当原先较高的子树插入新节点时,可能会导致历代祖先节点失衡(部分祖先节点,而非全部)

  3.当原先较低的子树删除老节点时,可能会导致历代祖先节点失衡(部分祖先节点,而非全部)

  针对AVL树的失衡节点进行重平衡时,主要关注失衡节点自身(N Node)、引起失衡方向的孩子节点(S Son)、引起失衡方向的孙子节点(GS GrandSon)及所属子树,对其进行等价变换操作,使之恢复平衡。

  我们从失衡节点出发,依据失衡节点和其引起失衡方向的孩子节点、其引起失衡方向的孙子节点的共同构成的拓扑结构,共以下四种形态:

  自己动手实现java数据结构(七) AVL树-LMLPHP

2.3.1 左左形

左左形 插入节点引起失衡及重平衡:

自己动手实现java数据结构(七) AVL树-LMLPHP

左左形 删除节点引起失衡及重平衡:

自己动手实现java数据结构(七) AVL树-LMLPHP

2.3.3 左右形

左右形 插入节点引起失衡及重平衡:

自己动手实现java数据结构(七) AVL树-LMLPHP

左右形 删除节点引起失衡及重平衡:

自己动手实现java数据结构(七) AVL树-LMLPHP

2.3.2 右右形

  右右形和左左形是镜像的拓扑结构,失衡场景和重平衡手段与"2.3.1 左左形"原理相同,限于篇幅,不再展开说明。

2.3.4 右左形

  右左形和左右形是镜像的拓扑结构,失衡场景和重平衡手段与"2.3.2 左右形"原理相同,限于篇幅,不再展开说明。

2.3.5 插入和删除操作重平衡的区别

  在示意图中,注意观察标识树高层次的横向细长的白色箭头

  插入和删除操作会导致操作节点位置的历代祖先失去平衡,这需要从低到高依次检查平衡状态并进行重平衡处理。

  插入新节点导致失衡并重平衡的过程中,当前失衡节点(最低的失衡祖先节点)所在子树的高度在重平衡操作完成后和未插入新节点之前是一样的。因而当最低位置的祖先节点恢复了平衡后,更高的祖先节点也将自动的恢复平衡(因为插入前是平衡的,而插入后失衡子树的高度在重平衡后也和原先一致了)。

  删除老节点导致失衡并重平衡的过程中,当前失衡节点(最低的失衡祖先节点)所在子树的高度在重平衡操作完成后和未插入新节点之前可能是不一样的(所在子树高度降低1)。因而当最低位置的祖先节点恢复平衡后,可能会导致更高的祖先节点进而失去平衡,这种现象会向上逐层传播。最坏的极端情况下,每当恢复一个较低的祖先节点平衡时,都会导致更高一层祖先节点失衡,直至根节点完成重平衡,这样的重平衡操作最多需要执行(logN)次。

2.4 3+4重构

  前面介绍了针对不同失衡状态拓扑结构进行重平衡的方法,都是使用一次或多次旋转的方式完成的。

  如果我们聚焦于重平衡操作所涉及的元素,会发现其实质只是改变了NodeSonGrandSon三个节点及所挂载的四颗子树(T0,T1,T2,T3)的位置关系。更为重要的是,无论是左左形还是其它三种形状,其重平衡完成之后的拓扑结构是一样的,区别只在于N,S,GS三个节点的相对位置不同

  因此,便有了另一种更高效的重平衡等价变换的方式,被称为"3+4重构"。3+4重构在重平衡时,暴力的将元素打散,并不使用旋转的技巧,而是直接改变节点和节点、节点和子树之间的引用关系,一口气将其转变为重平衡之后的最终拓扑结构,直达目标。

  自己动手实现java数据结构(七) AVL树-LMLPHP

3.AVL树实现细节

3.1 二叉搜索树拓展

  AVL树的实现继承自前面介绍的普通二叉搜索树—TreeMap类。由于AVL树通过树高作为判断平衡的依据,因此在二叉搜索树TreeMap的节点中增加了一个height(高度)属性。

/**
* 二叉搜索树 内部节点
* */
static class EntryNode<K,V> implements Map.EntryNode<K,V>{
/**
* key值
* */
K key; /**
* value值
* */
V value; /**
* 高度值
* */
int height; /**
* 左孩子节点
* */
EntryNode<K,V> left; /**
* 右孩子节点
* */
EntryNode<K,V> right; /**
* 双亲节点
* */
EntryNode<K,V> parent; EntryNode(K key, V value) {
this.key = key;
this.value = value;
this.height = 1;
} EntryNode(K key, V value,EntryNode<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
this.height = 1;
} @Override
public K getKey() {
return key;
} @Override
public V getValue() {
return value;
} @Override
public void setValue(V value) {
this.value = value;
} @Override
public String toString() {
return key + "=" + value + " height=" + height;
}
}

3.2 辅助方法

  在AVL树的实现过程中,存在一些通用的,细节的逻辑,将其抽取成辅助函数,简化主要代码逻辑,增强代码可读性和可维护性。

   /**
* 当前节点 是否满足AVL树约定的平衡条件
* */
private boolean isAVLBalanced(EntryNode<K,V> entryNode){
//获得 左子树高度
int leftChildHeight = getHeight(entryNode.left);
//获得右子树高度
int rightChildHeight = getHeight(entryNode.right); //获得左右子树高度差
int difference = leftChildHeight - rightChildHeight; //高度差绝对值在1之内,认为是满足AVL平衡条件
return -1 <= difference && difference <= 1;
} /**
* 更新当前节点高度
* */
private void updateHeight(EntryNode<K,V> entryNode){
int leftHeight = getHeight(entryNode.left);
int rightHeight = getHeight(entryNode.right); //左右子树高度较高者 + 1
entryNode.height = 1 + Math.max(leftHeight,rightHeight);
} /**
* 获得当前节点的高度
* */
private int getHeight(EntryNode<K,V> entryNode){
if(entryNode == null){
return 0;
}else{
return entryNode.height;
}
} /**
* 获得较高子树分支孩子节点
*/
private EntryNode<K,V> getTallerChild(EntryNode<K,V> entryNode){
int leftChildHeight = getHeight(entryNode.left);
int rightChildHeight = getHeight(entryNode.right); if(leftChildHeight > rightChildHeight){
//左子树高度 > 右子树高度
return entryNode.left;
}else{
//左子树高度 <= 右子树高度
return entryNode.right;
}
} /**
* 是否是左孩子
* */
private boolean isLeftChild(EntryNode<K,V> parent,EntryNode<K,V> target){
return getRelativeByParent(parent,target) == RelativePosition.LEFT;
}

3.3 3+4重构实现

  refactor34方法:方法的参数为3+4重构目标拓扑结构所需的三个节点(左,中,右),左右孩子的分别挂载的四颗子树。在refactor34方法中,依照3+4重构的原理直接调整节点和子树的关系引用,拼接成最终的所需的结果。

  rotateAt方法:方法的参数为重平衡所涉及到的祖孙三代节点(Node、Son、GrandSon),通过判断N、S、GS的拓扑结构,决定调用refactor34方法时传递的参数。方法的返回值为3+4重构后的子树树根节点,便于重平衡操作之后,将重构后新的子树重新接入整颗AVL树中。

   /**
* 3+4 重构
* */
private void refactor34(
EntryNode<K,V> leftNode, EntryNode<K,V> middleNode, EntryNode<K,V> rightNode,
EntryNode<K,V> llChild, EntryNode<K,V> lrChild,
EntryNode<K,V> rlChild, EntryNode<K,V> rrChild){ //调整 左节点和对应子树的拓扑结构
leftNode.left = llChild;
if(llChild != null){
llChild.parent = leftNode;
} leftNode.right = lrChild;
if(lrChild != null){
lrChild.parent = leftNode;
}
//更新高度
updateHeight(leftNode); //调整 右节点和对应子树的拓扑结构
rightNode.left = rlChild;
if(rlChild != null){
rlChild.parent = rightNode;
} rightNode.right = rrChild;
if(rrChild != null){
rrChild.parent = rightNode;
}
//更新高度
updateHeight(rightNode); //调整 中间节点 和左、右节点的拓扑结构
middleNode.left = leftNode;
middleNode.right = rightNode; leftNode.parent = middleNode;
rightNode.parent = middleNode;
//更新高度
updateHeight(middleNode);
} /**
* 进行旋转,使用3+4重构完成重平衡
* @return 重构之后子树的树根节点
* */
private EntryNode<K,V> rotateAt(EntryNode<K,V> currentNode,EntryNode<K,V> sonNode,EntryNode<K,V> grandSonNode){
if(isLeftChild(currentNode,sonNode)){
//左 zig
if(isLeftChild(sonNode,grandSonNode)){
//左-左 zig-zig旋转
refactor34(grandSonNode,sonNode,currentNode,
grandSonNode.left,grandSonNode.right,sonNode.right,currentNode.right); return sonNode;
}else{
//左-右 zig-zag旋转
refactor34(sonNode,grandSonNode,currentNode,
sonNode.left,grandSonNode.left,grandSonNode.right,currentNode.right); return grandSonNode;
}
}else{
//右 zag
if(isLeftChild(sonNode,grandSonNode)){
//右-左 zag-zig旋转
refactor34(currentNode,grandSonNode,sonNode,
currentNode.left,grandSonNode.left,grandSonNode.right,sonNode.right); return grandSonNode;
}else{
//右-右 zag-zag旋转
refactor34(currentNode,sonNode,grandSonNode,
currentNode.left,sonNode.left,grandSonNode.left,grandSonNode.right); return sonNode;
}
}
}

3.4 插入方法重写

  AVL树的实现中,重写了普通二叉搜索树的插入方法(put),整体逻辑和之前TreeMap的实现大致一样,唯一的区别在于,当插入了新的节点之后,会调用afterNewNodeInsert方法,进行AVL树重平衡的一系列操作。

  afterNewNodeInsert方法:

  参数为新插入的节点。从下至上,遍历检查新插入节点的历代祖先,判断其是否失衡。一旦发现当前迭代的祖先节点失衡,则调用rotateAt方法,使其恢复平衡,全树重新接入子树;

  插入节点时,导致的失衡不会向上传播,所属子树的高度能够复原,在恢复平衡之后,直接结束方法的执行,不再继续向上检查。另外,对于未失衡的祖先节点,其子树插入新节点可能会导致高度上升,因此需要更新其高度。

    @Override
public V put(K key, V value) {
if(this.root == null){
this.root = new EntryNode<>(key,value);
this.size++;
return null;
} //获得目标节点
TargetEntryNode<K,V> targetEntryNode = getTargetEntryNode(key);
if(targetEntryNode.relativePosition == RelativePosition.CURRENT){
//目标节点存在于当前容器 //暂存之前的value
V oldValue = targetEntryNode.target.value;
//替换为新的value
targetEntryNode.target.value = value;
//返回之前的value
return oldValue;
}else{
//目标节点不存在于当前容器
EntryNode<K,V> parent = targetEntryNode.parent;
EntryNode<K,V> newEntryNode = new EntryNode<>(key,value,parent);
if(targetEntryNode.relativePosition == RelativePosition.LEFT){
//目标节点位于左边
parent.left = newEntryNode;
}else{
//目标节点位于右边
parent.right = newEntryNode;
} //插入新节点后进行重平衡操作
afterNewNodeInsert(newEntryNode); this.size++;
return null;
}
} /**
* 插入后 重平衡操作
* @param newEntryNode 新插入的节点
* */
private void afterNewNodeInsert(EntryNode<K,V> newEntryNode){
EntryNode<K,V> currentAncestorNode = newEntryNode.parent; //遍历新插入节点的祖先节点,逐层向上
while(currentAncestorNode != null){
//判断当前祖先节点是否失去平衡
if(!isAVLBalanced(currentAncestorNode)){
//不平衡 //获得重构之前 失衡节点的父节点及其相对位置,用于之后重新连接重平衡后的子树
EntryNode<K,V> parent = currentAncestorNode.parent; //获得更高子树分支对应的孙辈节点,决定AVL树重平衡的策略
EntryNode<K,V> tallerSonNode = getTallerChild(currentAncestorNode);
EntryNode<K,V> tallerGrandSonNode = getTallerChild(tallerSonNode);
//以孙辈节点为基准,进行旋转,重平衡
EntryNode<K,V> newSubTreeRoot = rotateAt(currentAncestorNode,tallerSonNode,tallerGrandSonNode); //重构之后的子树 重新和全树对接
newSubTreeRoot.parent = parent;
//可能currentAncestorNode是根节点,不存在双亲节点
if(parent != null){
//原子树根节点的双亲节点 和新的子树进行连接
if(isLeftChild(parent,currentAncestorNode)){
parent.left = newSubTreeRoot;
}else{
parent.right = newSubTreeRoot;
}
}else{
this.root = newSubTreeRoot;
}
//插入时,最低失衡节点重平衡后,全树即恢复平衡,因此结束循环
return;
}else{
//平衡 //更新当前祖先节点的高度
updateHeight(currentAncestorNode);
} //指向上一层祖先节点
currentAncestorNode = currentAncestorNode.parent;
}
}

3.5 删除方法重写

  AVL树的实现中,重写了普通二叉搜索树的删除方法(remove),整体逻辑和之前TreeMap的实现大致一样,唯一的区别在于,当删除了之前老的节点之后,会调用afterNodeRemove方法,进行AVL树重平衡的一系列操作。

  afterNodeRemove方法:

  参数为之前被删除节点的双亲节点。从下至上,遍历检查被删除节点双亲的历代祖先,判断其是否失衡。一旦发现当前迭代的祖先节点失衡,则调用rotateAt方法,使其恢复平衡,全树重新接入子树。

  删除节点时,失衡现象会向上传播,因此必须一直向上遍历至根节点。另外,对于未失衡的祖先节点,子树删除老节点可能会导致高度降低,因此需要更新其高度。

 @Override
public V remove(K key) {
if(this.root == null){
return null;
} //查询目标节点
TargetEntryNode<K,V> targetEntryNode = getTargetEntryNode(key);
if(targetEntryNode.relativePosition != RelativePosition.CURRENT){
//没有找到目标节点
return null;
}else{
//找到了目标节点
EntryNode<K,V> target = targetEntryNode.target;
//先保存被删除节点 删除之前的双亲节点
EntryNode<K,V> parent = target.parent; //从二叉树中删除目标节点
deleteEntryNode(target); //删除节点后,对其历代祖先节点进行重平衡操作
afterNodeRemove(parent); return targetEntryNode.target.value;
}
} /**
* 插入后 重平衡操作
* @param deletedNode 被删除的节点
* */
private void afterNodeRemove(EntryNode<K,V> deletedNode){
EntryNode<K,V> currentAncestorNode = deletedNode; //遍历新插入节点的祖先节点,逐层向上
while(currentAncestorNode != null){
//判断当前祖先节点是否失去平衡
if(!isAVLBalanced(currentAncestorNode)){
//不平衡 //获得重构之前 失衡节点的父节点及其相对位置
EntryNode<K,V> parent = currentAncestorNode.parent;
//获得更高子树分支对应的孙辈节点,决定AVL树重平衡的策略
EntryNode<K,V> tallerSonNode = getTallerChild(currentAncestorNode);
EntryNode<K,V> tallerGrandSonNode = getTallerChild(tallerSonNode);
//以孙辈节点为基准,进行旋转,重平衡
EntryNode<K,V> newSubTreeRoot = rotateAt(currentAncestorNode,tallerSonNode,tallerGrandSonNode); //重构之后的子树 重新和全树对接
newSubTreeRoot.parent = parent;
//可能currentAncestorNode是根节点,不存在双亲节点
if(parent != null){
//原子树根节点的双亲节点 和新的子树进行连接
if(isLeftChild(parent,currentAncestorNode)){
parent.left = newSubTreeRoot;
}else{
parent.right = newSubTreeRoot;
}
}else{
this.root = newSubTreeRoot;
}
}else{
//平衡 //更新当前祖先节点的高度
updateHeight(currentAncestorNode);
} //指向上一层祖先节点
currentAncestorNode = currentAncestorNode.parent;
}
}

4.AVL树性能

4.1 插入性能

  AVL树的插入操作引起的失衡不会向上传播,只需要一次重平衡操作。

  相比普通二叉搜索树,AVL树插入重平衡操作额外引入的时间复杂度为O(1),十分高效。

4.2 删除性能

  AVL树的删除操作引起的失衡会向上传播,最坏情况下每一个祖先节点都需要进行重平衡。

  相比普通二叉搜索树,AVL树删除重平衡操作额外引入的时间复杂度为O(logN),删除效率比起红黑树(O(1))等更加高效的BBST相对要差。

4.3 查询性能

  AVL树适度平衡的条件比较苛刻,因此AVL树是非常接近完全二叉树的一种BBST。其查询效率和红黑树等综合性能更加优秀的BBST相比,查询时,虽然渐进的时间复杂度都为O(logN),但在常数倍率上看,效率要稍高一点点。

5.AVL树总结

  AVL树作为最早被发明的自平衡二叉搜索树,其删除效率与随后被发明的红黑树相比,相差一个数量级(O(logN) vs O(1))。其主要原因是红黑树等BBST在"平衡"的定义上进一步放松了标准,全树结构不如AVL树那么紧凑,略为降低了查询效率,但换来了删除效率的巨大提升。

  AVL树作为一种相对效率较低的BBST,其原理相较红黑树更简单。理解AVL树是理解更为强大、复杂的BBST的基础之一。

  本系列博客的代码在我的 github上:https://github.com/1399852153/DataStructures,存在许多不足之处,请多多指教。

05-11 13:38