AVL树
本章代码是上一篇《二叉树初步总结》的序章,主要记录AVL树的学习过程。
概念:AVL树是一种自平衡树,添加或移除节点时AVL树会尝试保持自平衡,任意一个节点的左子树和右子树高度最多相差1,添加或移除节点时,AVL树会尽可能尝试转换为完全树。
首先,定义一个AVLTree类
该类只需要结成BinarySearchTree类,再对其中的插入、移除方法进行覆盖即可。
class AVLTree extends BinarySearchTree { constructor(compareFn = defaultCompare) { super(compareFn) this.compareFn = compareFn this.root = null } }
其次,定义一个计算节点高度的方法
getNodeHeight(node) { if(node == null){ return -1 } return Math.max(this.getNodeHeight(node.left),this.getNodeHeight(node.right))+1 }
该方法利用了递归原理,只要节点不为空,就一直向下查,每查一次就+1,直到查到最终结果为止。
再定义一个计算平衡因子的方法
getBlanceFactor(node) { const heightDifference = this.getNodeHeight(node.left) - this.getNodeHeight(node.right) switch (heightDifference) { case -2: return BalanceFactor.UNBALANCED_RIGHT case -1: return BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT case 1: return BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT case 2: return BalanceFactor.UNBALANCED_RIGHT default: return BalanceFactor.BALANCED } }
为了避免在代码中直接处理平衡因子的数值,创建一个用来作为计数器的常量
const BalanceFactor = { UNBALANCED_RIGHT:1, SLIGHTLY_UNBALANCED_RIGHT:2, BALANCED:3, SLTGHTLY_UNBALANCED_LEFT:4, UNBALANCED_LEFT:5 }
如图所示,可以清晰的看出AVL树与非AVL树的区别。
旋转
为了将不平衡的树进行平衡,就需要通过旋转的方式来进行操作,旋转应该如何理解?
右旋转
如图示,这样一棵树左右子节点的高度差为1,他是处于平衡状态的,但是,当我们再插入一个节点5之后
当前树的左右子节点高度差达到了2,这个时候平衡性已经被破坏了,就需要进行一些操作去平衡它。
平衡之后树为
这时候执行的操作就是旋转,这么说可能不太明白
看图示,这个时候就是以节点7为原点,将15节点向右侧旋转,旋转之后再用7这个节点去替代原来的15节点,即可以生成一个满足平衡需求的树。注意,在调整时,找离最新插入的节点最近的不平衡的树进行调整。
现在看一种复杂一点的情况,是节点15存在右侧子节点的时候,这个时候我们需要执行四步操作
- 将节点15置于节点20所在的位置
- 将节点15的左侧子节点保持不变
- 将节点20的左侧子节点置为节点15的右侧子节点
- 将节点15的右侧子节点置为节点20
最终结果是这样的
整个流程的代码如下
// 向右的单旋转 rotationR(node){ const tmp = node.left node.left = tmp.right tmp.right = node return tmp }
左旋转
左旋转即某个节点的右侧子节点导致不平衡的时候,需要对右侧子节点进行左旋转来保证平衡,整体思路与右转选一样,代码如下
// 向左的单旋转 rotationL(node){ const tmp = node.right node.right = tmp.left tmp.left = node return tmp }
先左旋转再右旋转
如图所示,这种情况是根节点的左侧子节点的右侧子节点导致的不平衡,首先,我们对左侧子节点进行平衡,我们知道当右侧子节点不平衡时使用左转
对根节点20的左侧子节点进行左旋之后的结果如图所示,这个时候就变成了所有导致不平衡的情况都是左侧子节点导致的,所以这个时候再对整棵树进行向右的旋转
代码如下:
// 向右的双旋转,先左再又 rotationLR(node){ node.left = this.rotationL(node.left) return this.rotationR(node) }
先右旋转再左旋转
先右再左是当右侧子节点的左侧子节点导致不平衡时的情况使用的,和先左再右相反,代码如下:
// 向左的双旋转 rotationRL(node){ node.right = this.rotationR(node.right) return this.rotationL(node) }
向AVL树中插入节点
与普通二叉树不同的是,每次插入值之后,都需要去判断一下当前这个节点是否失衡了,如果失衡了需要去进行相应的旋转平衡节点
代码如下:
insert(key){ this.root = this.insertNode(this.root,key) } insertNode(node,key){ if(node == null){ return new Node(key) }else if(this.compareFn(key,node.key) === Compare.LESS_THAN){ node.left = this.insertNode(node.left,key) }else if(this.compareFn(key,node.key) == Compare.BIGGER_THAN){ node.right = this.insertNode(node.right,key) }else{ return node } // 如果需要,就去对树进行平衡操作 const balanceFactor = this.getBlanceFactor(node) if(balanceFactor === balanceFactor.UNBALANCED_LEFT) { if(this.compareFn(key,node.left.key) === Compare.LESS_THAN){ node = this.rotationR(node) }else{ node = this.rotationLR(node) } } if(balanceFactor === balanceFactor.UNBALANCED_RIGHT){ if(this.compareFn(key,node.right.key) === Compare.BIGGER_THAN){ node = this.rotationR(node) }else{ return this.rotationRL(node) } } return node }
整体思路为,先插入相应节点,然后去判断这个节点在插入子节点之后是否失衡了,如果是左侧子节点失衡再去判断插入的值是插入了左侧子节点还是右侧子节点,如果是左侧子节点那么直接右旋,如果是右侧子节点就需要先左旋再右旋。当是右侧失衡时思路相反。
移除节点
移除节点时直接调用之前的移除节点的方法,然后再对树的平衡进行检测,再针对左侧子节点的左侧子节点不平衡、左侧子节点的右侧子节点不平衡,右侧子节点的左侧子节点不平衡,右侧子节点的右侧子节点不平衡这几种情况进行相应的旋转处理。