AVL树特性
AVL树,之所以叫AVL是因为由三个人发明的。这三个人分别是GM Adelson,Velsky和EM Landis。他们的名字缩写就是AVL。AVL树是一种平衡二叉搜索树,相对红黑树来说比较简单。
二叉树的意思,就是树只有两个节点,平衡的意思是左边和右边的层数要平衡。也就是层数相差不能超过一,而且每个子树也要是平衡二叉树。搜索的意思是这是个排序树,就是左孩子要比父节点小,并且右孩子比父节点大。如下图就是一颗AVL树:
因为我用的是java编程语言实现的AVL树,所以我使用了几级的类继承结构。如下图:
首先是树,子类是二叉树,再下面是排序树,再下面是平衡树。这样就做成了一个平衡二叉树。
当然,除了树还有节点类,我也使用了多级类继承结构。
平衡二叉树的旋转
二叉树的核心就是添加或删除完成之后,判断一下树是否平衡,如果不平衡就进行旋转调整。旋转时是值的拷贝,而不是把根换掉。而且旋转时要寻找最小子树。旋转类型分为四种,对应四种不平衡场景,LL LR都属于L开头,意思是左边深度大于右边深度+1,也就是左边深度至少是右边的深度+2。RL和RR同理。下边我细细讲一讲:
LL型旋转
LL旋转又叫右旋。是在左子树深度远大于右子树的情况下进行的旋转。意思是左子树加了个左孩子的意思。很明显这是一种不平衡,如图,左子树深度为2,右子树深度为0。其旋转调整方法,是将左子树作为新的根节点,旧的根节点作为右节点。调整为下图所示状态:
代码如下:
/**
* 3 2
* 2 -> 1 3
* 1
* @return
*/
private void ll() {
final AVLNode<T> newRight = new AVLNode<>(this.value);
newRight.setRight(this.getRight());
newRight.setLeft(this.getLeft().getRight());
this.setRight(newRight);
this.value = this.getLeft().value;
this.getLeft().parent = null;
this.setLeft(this.getLeft().getLeft());
}
RR型旋转
同理,RR是右子树多了个右孩子。
调整后是这个样子:
代码如下:
/**
* 1 2
* 2 -> 1 3
* 3
* @return
*/
private void rr() {
final AVLNode<T> newLeft = new AVLNode<>(this.value);
newLeft.setLeft(this.getLeft());
newLeft.setRight(this.getRight().getLeft());
this.setLeft(newLeft);
this.value = this.getRight().value;
this.getRight().parent = null;
this.setRight(this.getRight().getRight());
}
LR型旋转
LR是左子树多了个右孩子。
调整后:
调整的代码完全可以复用LL旋转和RR旋转的代码,也就是先变成LL型,再执行LL型的旋转方法:
/**
* 7 3
* 2 -> 2 7
* 3
* @return
*/
private void lr() {
this.getLeft().rr();
this.ll();
}
RL型旋转
RL是右子树多了个左孩子。如图
旋转后:
旋转的代码:
/**
* 5 6
* 7 -> 5 7
* 6
* @return
*/
private void rl() {
this.getRight().ll();
this.rr();
}
AVL树的插入
插入首先要按二叉排序树的方法进行插入,也就是在左右子树循环找,进行比较,直到找到合适的位置进行插入,代码代码如下:
/**
* 内部添加方法
* @param newNode
* @return
*/
protected OrderBinaryNode<T> innerAdd(OrderBinaryNode<T> newNode) {
OrderBinaryNode<T> node = this;
T t = newNode.value;
while (node != null) {
if (t.compareTo(node.getValue()) > 0) {
// 如果right == null
if (node.getRight() == null) {
node.setRight(newNode);
return node;
}
node = node.getRight();
} else if (t.compareTo(node.getValue()) < 0) {
if (node.getLeft() == null) {
node.setLeft(newNode);
return node;
}
node = node.getLeft();
} else {
throw new DuplicateKeyException(t);
}
}
return null;
}
插入之后需要按四大旋转调整节点,寻找最小不平衡子树,并且调整的代码如下:
private void fixToBalance(AVLNode<T> avlNode) {
// 往上找直到子树不平衡
for(AVLNode<T> pointer = avlNode;pointer != null; pointer = (AVLNode<T>) pointer.parent) {
if (pointer.getLeftHeight() - pointer.getRightHeight() > 1) {
// LL 或 LR
final AVLNode<T> left = pointer.getLeft();
if (left.getLeftHeight() > left.getRightHeight()) {
// LL
pointer.ll();
break;
}
if (left.getLeftHeight() < left.getRightHeight()) {
// LR
// 先子树RR,再整体LL
pointer.lr();
break;
}
}
if (pointer.getLeftHeight() - pointer.getRightHeight() < -1) {
// RR 或 RL
final AVLNode<T> right = pointer.getRight();
if (right.getLeftHeight() > right.getRightHeight()) {
// RL
pointer.rl();
break;
}
if (right.getLeftHeight() < right.getRightHeight()) {
// RR
pointer.rr();
break;
}
}
}
}
从代码可以看出是从新加的节点进行向上寻找,然后判断不平衡的类型。
AVL树的删除
AVL树的删除比添加节点要复杂。
第一步是删除吧。但是删除之后要符合二叉树的特征。
比如这棵树
要删除6,可以把7代替原来的位置,也可以用5代替原来的位置。但是如果删除的是8呢?只能将7放过来。代替原来的位置。
具体操作就是一个判断,如果左子树不为空,在左子树一直找右子节点,直到没有了为止。这样就找到了最大值。
否则,如果右子树不为空,找右子树的最小值。
在删除之后就进行旋转调整,同样是四种旋转。
代码如下:
// 排序二叉树的删除
final OrderBinaryNode<T> node = findNode(t);
if (node == null) {
return;
}
// 如果是root,并且为空
if (node == root && root.isEmpty()) {
root = null;
return;
}
// 如果是叶子
if (node.isEmpty()) {
final BinaryNode<T> parent = node.getParent();
parent.removeChild(node);
return;
}
// 否则就自己调整了
node.delete();
删除但是不调整的代码如下:
public void delete() {
// 排序树节点,删除自己之后还是个排序树。
// 这是自己不空的情况
// 分两种情况
if (getLeft() != null) {
// 左节点找出最大值
OrderBinaryNode<T> node = this.getLeft();
while (node.getRight() != null) {
node = node.getRight();
}
// 这是最大值,删除了自己
this.value = node.value;
node.parent.removeChild(node);
} else if (getRight() != null) {
// 右节点找出最小值
OrderBinaryNode<T> node = this.getRight();
while (node.getLeft() != null) {
node = node.getLeft();
}
this.value = node.value;
node.parent.removeChild(node);
}
}
而调节平衡调用添加时的调节方法就行了.