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 等价变换介绍

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

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

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

  

2.2 等价变换方式

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

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

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

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

顺时针旋转:

  

逆时针旋转:

  

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

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

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

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

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

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

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

  

2.3.1 左左形

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

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

2.3.3 左右形

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

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

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重构在重平衡时,暴力的将元素打散,并不使用旋转的技巧,而是直接改变节点和节点、节点和子树之间的引用关系,一口气将其转变为重平衡之后的最终拓扑结构,直达目标。

  

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,存在许多不足之处,请多多指教。

自己动手实现java数据结构(七) AVL树的更多相关文章

  1. 自己动手实现java数据结构(一) 向量

    1.向量介绍 计算机程序主要运行在内存中,而内存在逻辑上可以被看做是连续的地址.为了充分利用这一特性,在主流的编程语言中都存在一种底层的被称为数组(Array)的数据结构与之对应.在使用数组时需要事先 ...

  2. Java数据结构和算法(七)--AVL树

    在上篇博客中,学习了二分搜索树:Java数据结构和算法(六)--二叉树,但是二分搜索树本身存在一个问题: 如果现在插入的数据为1,2,3,4,5,6,这样有序的数据,或者是逆序 这种情况下的二分搜索树 ...

  3. 自己动手实现java数据结构(六)二叉搜索树

    1.二叉搜索树介绍 前面我们已经介绍过了向量和链表.有序向量可以以二分查找的方式高效的查找特定元素,而缺点是插入删除的效率较低(需要整体移动内部元素):链表的优点在于插入,删除元素时效率较高,但由于不 ...

  4. AVL树的JAVA实现及AVL树的旋转算法

    1,AVL树又称平衡二叉树,它首先是一颗二叉查找树,但在二叉查找树中,某个结点的左右子树高度之差的绝对值可能会超过1,称之为不平衡.而在平衡二叉树中,任何结点的左右子树高度之差的绝对值会小于等于 1. ...

  5. linux 内核数据结构之 avl树.

    转载: http://blog.csdn.net/programmingring/article/details/37969745 https://zh.wikipedia.org/wiki/AVL% ...

  6. 数据结构之AVL树

    AVL树是高度平衡的而二叉树.它的特点是:AVL树中任何节点的两个子树的高度最大差别为1. 旋转 如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡.这种失去平衡的可以概括为4种姿态:LL ...

  7. [算法] 数据结构之AVL树

    1 .基本概念 AVL树的复杂程度真是比二叉搜索树高了整整一个数量级——它的原理并不难弄懂,但要把它用代码实现出来还真的有点费脑筋.下面我们来看看: 1.1  AVL树是什么? AVL树本质上还是一棵 ...

  8. D&F学数据结构系列——AVL树(平衡二叉树)

    AVL树(带有平衡条件的二叉查找树) 定义:一棵AVL树是其每个节点的左子树和右子树的高度最多差1的二叉查找树. 为什么要使用AVL树(即为什么要给二叉查找树增加平衡条件),已经在我之前的博文中说到过 ...

  9. [javaSE] 数据结构(AVL树基本概念)

    AVL树是高度平衡的二叉树,任何节点的两个子树的高度差别<=1 实现AVL树 定义一个AVL树,AVLTree,定义AVLTree的节点内部类AVLNode,节点包含以下特性: 1.key——关 ...

随机推荐

  1. JMD Handy Baby 2 to Decode & Adding New BMW 525 ID46 Key

    Here OBD2TOOL share the guide on how to use JMD Handy Baby II to decode and add new keys for BMW 525 ...

  2. 《笨方法学Python》加分题15

    本题本题开始涉及文件的操作,文件操作是一件危险的事情,需要仔细细心否则可能导致重要的文件损坏. 本题除了 ex15.py 这个脚本以外,还需要一个用来读取的文件 ex15_sample.txt 其内容 ...

  3. Apache Beam是什么?

    Apache Beam 的前世今生 1月10日,Apache软件基金会宣布,Apache Beam成功孵化,成为该基金会的一个新的顶级项目,基于Apache V2许可证开源. 2003年,谷歌发布了著 ...

  4. jdk8 永久代变更

    java8 去掉了永久代permgen(又称非堆,其实也是堆的一部分),类的方法代码,常亮,方法名,静态变量等存放在永久代中 改为使用元空间 Metaspace , Metaspace 不在是堆的一部 ...

  5. [uboot] (第四章)uboot流程——uboot编译流程

    http://blog.csdn.net/ooonebook/article/details/53000893 以下例子都以project X项目tiny210(s5pv210平台,armv7架构)为 ...

  6. python持久化

    将对象转为二进制字节流(pickle) import pickle my_list = [1,2,3] pickle_file = open('my_list.pkl', 'wb') #注意二进制写入 ...

  7. Vuejs——(1)入门(单向绑定、双向绑定、列表渲染、响应函数)

    版权声明:出处http://blog.csdn.net/qq20004604   目录(?)[+]   参照链接: http://cn.vuejs.org/guide/index.html [起步]部 ...

  8. [bug]”System.InvalidProgramException:公共语言运行时检测到无效程序“解决方案

    Visual Studio 2017版本15.8.x运行某些程序会报这样的错误:“System.InvalidProgramException:公共语言运行时检测到无效程序” 此问题的临时解决方案: ...

  9. DRF 商城项目 - 日志处理

    logging  模块 logging 模块是最基本的日志处理模块 缺陷 但是拥有一些很致命的缺陷 要求用户主动查询, 需要登录到服务器才可以查看日志文件 自带的报错外部通知也没办法判断同类取舍, 短 ...

  10. fscanf_s与scanf_s的宽度参数与缓冲区参数分析

    fscanf_s函数 在文件操作中经常会用到fscanf这个函数,但是在VC和VS中会有警告 意思是编译器觉得fscanf不安全,叫你考虑用一下fscanf_s这个函数来代替fscanf,fscanf ...