引言

AVL树是带有平衡条件的二叉查找树,它保证树的深度须是图解AVL树_图解AVL树的旋转操作
特性:它每个节点的左子树和右子树的高度最多差1。
空树的高度定义为-1。

图解AVL树_图解平衡二叉树_02

上图中左边的树是AVL树,而右边不是。右边树左子树高度为3,右子树高度为1,相差超过1。

实现

除了删除和插入方法和BST不同,其他方法(​​contains​​​、​​findMin​​​、​​findMax​​等)和BST是一样的。

BST介绍请移步图解二叉查找树

树节点结构

private static class Node<E> {
E data;
Node<E> left;
Node<E> right;
/**
* 以this为根的树的高度
*/
int height;

Node(E data) {
this(data, null, null);
}

Node(E data, Node<E> lt, Node<E> rt) {
this.data = data;
left = lt;
right = rt;
height = 0;
}
}

和BST相比,多了一个​​height​​属性。

所有的插入和删除操作都会影响AVL树的平衡性。因此重点是当平衡性被破坏后,如何恢复。我们先来看插入方法。

insert方法

插入一个节点可能会破坏AVL树的特性

图解AVL树_平衡二叉树Java实现_03


例如,将6插入到上图中的AVL树会破坏节点8处的平衡。这时,就要在这一步插入完成之前恢复平衡的性质。

实际上,可以通过对树进行简单的修正来做到,我们称其为旋转(rotation)

在插入以后,只有那些从插入点到根节点的路径上的节点的平衡可能被改变,比如上面改变的就是以8为根的子树的平衡。

我们把必须重新平衡的节点记作图解AVL树_AVL树Java实现_04。当图解AVL树_AVL树Java实现_04的两颗子树高度差2时,就需要重新平衡。这种不平衡可能出现在下面四种情况中:

  1. 图解AVL树_AVL树Java实现_06的左孩子的左子树进行一次插入(Left-left)
  2. 图解AVL树_平衡二叉树Java实现_07

  3. 图解AVL树_AVL树Java实现_06的左孩子的右子树进行一次插入(Left-right)
  4. 图解AVL树_AVL树Java实现_09

  5. 图解AVL树_AVL树Java实现_06的右孩子的左子树进行一次插入(Right-left)
  6. 图解AVL树_AVL树Java实现_11

  7. 图解AVL树_AVL树Java实现_06的右孩子的右子树进行一次插入(Right-right)
  8. 图解AVL树_图解平衡二叉树_13

情形1和4是关于图解AVL树_AVL树Java实现_04点的镜像对称,2和3同理。理论上只有两种情况。

情形1和4属于左-左或右-右的情况,该情况通过对树进行一次单旋转即可。

而情形2和3左-右或右-左的情况需要进行稍微复杂些的**双旋转(double rotatoin)**来处理。

图解AVL树_图解平衡二叉树_15


我们可以借助圆规来理解这个旋转过程。

下面一一来看该如何旋转:

  1. Left-left:

图解AVL树_AVL树Java实现_16

以6节点的左孩子为中心做一次右旋操作。节点5作为圆规的固定点,而节点6是动点(可以用两根手指上去比划),将圆规向右旋转(顺时针)。

注意节点5的右孩子旋转后会变成6的左孩子。旋转前mid也是6的左子树中的节点。

  1. Left-right:

图解AVL树_平衡二叉树Java实现_17

左-右双旋转操作:先对4-5进行左旋。5作为圆规的固定点,4作为动点,向进行左旋(可以先去看下情形4)。得到一个中间树,如果你观察仔细的话,可以发现这颗中间树和Left-left中的树是类似的,因此最后再进行一次右旋操作即可。

旋转时不要把子节点的左右孩子当成空,比如上图的节点5。

  1. Right-left

图解AVL树_图解平衡二叉树_18

右-左旋转操作:先对6-5进行右旋。5作为固定点,6作为动点,向右旋转。得到的中间树是Right-right的情况,此时再进行一次左旋即可。

4. Right-right

图解AVL树_AVL树Java实现_19


以4节点的右孩子为中心做一次左旋操作。此时5可以想象成圆规的固定点,4是动点,向左旋转(逆时针方向)。

注意节点5的左孩子旋转后会变成4的右孩子。

我们将所有情形的旋转操作通过一个方法来描述:

/**
* 对AVL树进行平衡操作
* @param node 失去平衡的子树根
* @return
*/
private Node<E> balance(Node<E> node) {
if (node == null) {
return null;
}

/**
* Left-left: 对node的左孩子的左子树进行一次插入 -> 做一次右旋操作
* Left-right: 先左旋,再右旋
* Right-right: 对node的右孩子的右子树进行一次插入 -> 做一次左旋操作
* Right-left: 先右旋,再左旋
*/
//左边比右边高
if (height(node.left) - height(node.right) > ALLOWED_IMBALANCE) {
if (height(node.left.left) >= height(node.left.right)) {
//Left-left
node = rotateRightWithLeftChild(node);
} else {
//Left-right
node = doubleWithLeftChild(node);
}
} else if (height(node.right) - height(node.left) > ALLOWED_IMBALANCE) {
if (height(node.right.right) >= height(node.right.left)) {
//Right-right
node = rotateLeftWithRightChild(node);
} else {
//Right-left
node = doubleWithRightChild(node);
}
}

//重新计算节点node的高度
computeHeight(node);
return node;
}

其中计算高度相关方法如下:

/**
* 返回节点的高度
* @param node
* @return
*/
private int height(Node<E> node) {
return node == null ? -1 : node.height;
}

private void computeHeight(Node<E> node) {
//因为height(null) == -1,所以+1为了保证非空叶子节点的高度为0
node.height = Math.max(height(node.left),height(node.right)) + 1;
}

以节点的左孩子为中心(固定点),节点为动点右旋,可对比前面的图进行理解。

/**
* 以节点的左孩子为中心做一次右旋
* @param node
* @return
*/
private Node<E> rotateRightWithLeftChild(Node<E> node) {
Node<E> tmp = node.left;
node.left = tmp.right;//节点的新左孩子为tmp的右孩子
tmp.right = node;
//重新计算高度,此时node为tmp的左孩子,因此先计算孩子的高度,再计算根节点(tmp)的高度
computeHeight(node);
computeHeight(tmp);
return tmp;
}

以node的右孩子为中心,节点为动点左旋:

/**
* 以node的右孩子为中心做一次左旋操作
* @param node
* @return
*/
private Node<E> rotateLeftWithRightChild(Node<E> node) {
//就是右旋的镜像操作,left和right互换
Node<E> tmp = node.right;
node.right = tmp.left;//节点的新右孩子为tmp的左孩子
tmp.left = node;
//重新计算高度,此时node为tmp的左孩子,因此先计算孩子的高度,再计算根节点(tmp)的高度
computeHeight(node);
computeHeight(tmp);
return tmp;
}

双旋操作可以利用上面两个方法,先左旋,再右旋:

/**
* 先左旋,再右旋
* @param node
* @return
*/
private Node<E> doubleWithLeftChild(Node<E> node) {
//对node的左子树进行左旋,注意是以node的左孩子的右孩子为中心,所以要传入node.left,并让node.left指向旋转后的子树
node.left = rotateLeftWithRightChild(node.left);
//再对node进行右旋,此时是以node的左孩子为中心旋转
return rotateRightWithLeftChild(node);
}

先右旋,再左旋

private Node<E> doubleWithRightChild(Node<E> node) {
//对node的右子树进行旋转
node.right = rotateRightWithLeftChild(node.right);
//再对node进行左旋
return rotateLeftWithRightChild(node);
}

接下来来看下​​insert​​函数的实现:

/**
* 插入
*
* @param x
*/
@Override
public void insert(E x) {
root = insert(x, root);
}

private Node<E> insert(E x, Node<E> node) {
if (node == null) {
return new Node<>(x);
}
int cmp = x.compareTo(node.data);
if (cmp < 0) {
node.left = insert(x, node.left);
} else if (cmp > 0) {
node.right = insert(x, node.right);
}
//插入后进行自平衡
return balance(node);
}

remove

由于BST的删除比插入更复杂,因此一般的想法是AVL树的删除也更复杂。其实只要像在插入中做的一样,把(BST的​​remove()​​​)最后一行改成​​balance​​方法就好了。

@Override
public void remove(E x) {
root = remove(x, root);
}

private Node<E> remove(E x, Node<E> node) {
/**
*
* 如果待删除节点是叶子节点,则可以直接被删除
* 如果节点只有一个孩子,让孩子节点顶替它的位置即可(让待删除节点的父节点指向其子节点)
* 如果有两个孩子,让其右子树的最小节点的数据代替该节点的数据并递归地删除该最小节点。
*/
if (node == null) {
return null;
}
//将当前节点的值与x进行比较
int cmp = x.compareTo(node.data);
if (cmp < 0) {
node.left = remove(x, node.left);
} else if (cmp > 0) {
node.right = remove(x, node.right);
} else {
//找到了待删除节点
if (node.left != null && node.right != null) {
//如果有两个孩子 其右子树的最小节点的数据代替该节点的数据
node.data = findMin(node.right).data;
//递归地删除右子树的最小节点,同时node.right指向更新后的节点
node.right = remove(node.data,node.right);
} else {
node = node.left != null ? node.left : node.right;
}
}
return balance(node);
}

注意,删除可能造成整颗树的不平衡,需要进一步进行自平衡。因为可能调整后的子树高度和调整之前的子树高度不一样(通常是调整后高度变低了)。

比如,以下面的树为例:

图解AVL树_平衡二叉树_20


图中的红色实心圈代表空引用。

注意该图中非空叶子节点高度为1,我们讨论的情况它的高度为0。因此这也是没问题的。

这是一颗标准的AVL树

图解AVL树_平衡二叉树_21


这里注意一下,之前根节点50的右子树的高度是3,左子树高度是4。

图解AVL树_平衡二叉树_22


假设我们要删除80这个节点,删除后以75节点为根的树就变成不平衡的。根据上面的知识,我们要进行一次右旋操作。

图解AVL树_图解平衡二叉树_23


上图右边的树是进行右旋操作后的结果。

图解AVL树_AVL树Java实现_24


50节点的右子树是平衡了,但是右子树的高度比之前减了1,因此导致整棵树变成不平衡的。

图解AVL树_AVL树Java实现_25

注意到我们的删代码,此时​​node​​​为50元素的节点,在 ​​node.right = remove(x, node.right);​​​代码执行完毕,它的右子树达到平衡后,该代码会到达​​return balance(node);​​。这里会对它的左子树进行一次右旋操作即可。

最终的结果如下:

│           ┌── 75
│ ┌── 60
│ │ └── 55
│ ┌── 50
│ │ └── 30
│ │ └── 27
└── 25
│ ┌── 15
└── 10
└── 5
└── 1

把头转一下就能看到结果了哈,我就不画图了。躲个懒。。​​^_^​

还未结束,还有一个特殊情况要考虑。比如这样一棵树:

图解AVL树_平衡二叉树Java实现_26


同样是要删除80节点。

图解AVL树_AVL树Java实现_27


将80节点删除,并进行一次右旋操作后如上图所示。此时整颗树是不平衡的。左子树的高度过高,因此需要调整。

我们注意​​balance()​​中的这段代码

if (height(node.left) - height(node.right) > ALLOWED_IMBALANCE) {
if (height(node.left.left) >= height(node.left.right)) {
//Left-left
node = rotateRightWithLeftChild(node);
} else {
//Left-right
node = doubleWithLeftChild(node);
}
} else if ...

这里的​​node​​是50。

​height(node.left.left)​​​ = 2,而​​height(node.left.right)​​ 也是2。在这种相等的情况下,做一次右旋操作即可(以25为中心,50为动点)。旋转之后结果如下。

图解AVL树_图解平衡二叉树_28

因此这里的条件是​​>=​​​ 而不是​​>​​。

完整代码

package com.algorithms.tree;

import com.algorithms.stack.Stack;

import java.util.NoSuchElementException;
import java.util.SplittableRandom;

/**
* @author yjw
* @date 2019/6/18/018
*/
public class AVLTree<E extends Comparable<? super E>> implements BinaryTree<E> {
private static final int ALLOWED_IMBALANCE = 1;
/**
* 根节点
*/
private Node<E> root;

/**
* 插入
*
* @param x
*/
@Override
public void insert(E x) {
root = insert(x, root);
}

private Node<E> insert(E x, Node<E> node) {
if (node == null) {
return new Node<>(x);
}
int cmp = x.compareTo(node.data);
if (cmp < 0) {
node.left = insert(x, node.left);
} else if (cmp > 0) {
node.right = insert(x, node.right);
}
return balance(node);
}

/**
* 对AVL树进行平衡操作
*
* @param node 失去平衡的子树根
* @return
*/
private Node<E> balance(Node<E> node) {
if (node == null) {
return null;
}

/**
* Left-left: 对node的左孩子的左子树进行一次插入 -> 做一次右旋操作
* Left-right: 先左旋,再右旋
* Right-right: 对node的右孩子的右子树进行一次插入 -> 做一次左旋操作
* Right-left: 先右旋,再左旋
*/
//左边比右边高
if (height(node.left) - height(node.right) > ALLOWED_IMBALANCE) {
if (height(node.left.left) >= height(node.left.right)) {
//Left-left
node = rotateRightWithLeftChild(node);
} else {
//Left-right
node = doubleWithLeftChild(node);
}
} else if (height(node.right) - height(node.left) > ALLOWED_IMBALANCE) {
if (height(node.right.right) >= height(node.right.left)) {
//Right-right
node = rotateLeftWithRightChild(node);
} else {
//Right-left
node = doubleWithRightChild(node);
}
}

//重新计算节点node的高度
computeHeight(node);
return node;
}

@Override
public void remove(E x) {
root = remove(x, root);
}

private Node<E> remove(E x, Node<E> node) {
/**
*
* 如果待删除节点是叶子节点,则可以直接被删除
* 如果节点只有一个孩子,让孩子节点顶替它的位置即可(让待删除节点的父节点指向其子节点)
* 如果有两个孩子,让其右子树的最小节点的数据代替该节点的数据并递归地删除该最小节点。
*/
if (node == null) {
return null;
}
//将当前节点的值与x进行比较
int cmp = x.compareTo(node.data);
if (cmp < 0) {
node.left = remove(x, node.left);
} else if (cmp > 0) {
node.right = remove(x, node.right);
} else if (node.left != null && node.right != null) {
//如果有两个孩子 其右子树的最小节点的数据代替该节点的数据
node.data = findMin(node.right).data;
node.right = remove(node.data,node.right);
} else {
node = node.left != null ? node.left : node.right;
}
//每次删除操作会都会调用该方法
return balance(node);
}

@Override
public boolean contains(E x) {
return contains(x, root);
}

private boolean contains(E x, Node<E> node) {
if (node == null) {
//递归跳出条件
return false;
}

//将当前节点的值与x进行比较
int cmp = x.compareTo(node.data);
if (cmp == 0) {
return true;
} else if (cmp < 0) {
//x < current node data
//若x小于当前节点,则比较左子树
return contains(x, node.left);
} else {
return contains(x, node.right);
}
}

@Override
public boolean isEmpty() {
return root == null;
}

@Override
public void makeEmpty() {
root = null;
}

@Override
public void printTree() {
if (isEmpty()) {
System.out.println("Empty tree");
} else {
print(root);
}
}

public E findMin() {
checkEmpty();
return findMin(root).data;
}

private void checkEmpty() {
if (isEmpty()) {
throw new NoSuchElementException();
}
}

private void print(Node<E> node) {
System.out.println(node);
}

/**
* 一直向左走就能找到最小值
*
* @param node
* @return
*/
private Node<E> findMin(Node<E> node) {
while (node.left != null) {
node = node.left;
}
return node;
}

public E findMax() {
checkEmpty();
return findMax(root).data;
}

private Node<E> findMax(Node<E> node) {
while (node.right != null) {
node = node.right;
}
return node;
}

/**
* 删除AVL树中最小节点
*/
public void removeMin() {
root = removeMin(root);
}

/**
* 删除node节点最小子树节点
*
* @param node
* @return 新的子树根节点,而不是返回删除的节点
*/
private Node<E> removeMin(Node<E> node) {
//找到了最小左子树节点,返回其右子树
//目的是让其父节点的left指向其右子树
if (node.left == null) {
return node.right;
}
//更新引用
node.left = removeMin(node.left);
return node;
}


/**
* 返回节点的高度
*
* @param node
* @return
*/
private int height(Node<E> node) {
return node == null ? -1 : node.height;
}

/**
* 计算节点的高度,通常在旋转后
*
* @param node
*/
private void computeHeight(Node<E> node) {
//因为height(null) == -1,所以+1为了保证非空叶子节点的高度为0
node.height = Math.max(height(node.left), height(node.right)) + 1;
}

/**
* 以节点的左孩子为中心做一次右旋
*
* @param node
* @return
*/
private Node<E> rotateRightWithLeftChild(Node<E> node) {
Node<E> tmp = node.left;
node.left = tmp.right;//节点的新左孩子为tmp的右孩子
tmp.right = node;
//重新计算高度,此时node为tmp的左孩子,因此先计算孩子的高度,再计算根节点(tmp)的高度
computeHeight(node);
computeHeight(tmp);
return tmp;
}

/**
* 先左旋,再右旋
*
* @param node
* @return
*/
private Node<E> doubleWithLeftChild(Node<E> node) {
//对node的左子树进行左旋,注意是以node的左孩子的右孩子为中心
node.left = rotateLeftWithRightChild(node.left);
//再对node进行右旋,此时是以node的左孩子为中心旋转
return rotateRightWithLeftChild(node);
}

/**
* 先右旋,再左旋
*
* @param node
* @return
*/
private Node<E> doubleWithRightChild(Node<E> node) {
//对node的右子树进行旋转
node.right = rotateRightWithLeftChild(node.right);
//再对node进行左旋
return rotateLeftWithRightChild(node);
}

/**
* 以node的右孩子为中心做一次左旋操作
*
* @param node
* @return
*/
private Node<E> rotateLeftWithRightChild(Node<E> node) {
//就是右旋的镜像操作,left和right互换
Node<E> tmp = node.right;
node.right = tmp.left;//节点的新右孩子为tmp的左孩子
tmp.left = node;
//重新计算高度,此时node为tmp的左孩子,因此先计算孩子的高度,再计算根节点(tmp)的高度
computeHeight(node);
computeHeight(tmp);
return tmp;
}

/**
* 检查是否为平衡二叉树
*/
public void checkBalance() {
checkBalance(root);
}

private int checkBalance(Node<E> node) {
if (node == null) {
return -1;
}
int hl = checkBalance(node.left);
int hr = checkBalance(node.right);
if (Math.abs(height(node.left) - height(node.right)) > ALLOWED_IMBALANCE
|| height(node.left) != hl || height(node.right) != hr) {
throw new IllegalStateException("Not Balanced");
}
return height(node);
}

/**
* 非递归实现中序遍历
*/
public void inOrderNonRecursion() {
inOrderNonRecursion(root);
System.out.println();
}

private void inOrderNonRecursion(Node<E> node) {
//栈中保存的是左侧节点
Stack<Node<E>> stack = new Stack<>();
while (true) {
goLeft(node,stack);
if (stack.isEmpty()) {
break;
}
//弹出最左侧元素,其左子树为空或已经访问过
node = stack.pop();
//访问它
System.out.print(node.data + " ");
//接下来访问右子树(可能为空),相当于 goLeft(node.right,stack);
node = node.right;
}
}

private void goLeft(Node<E> node,Stack<Node<E>> stack) {
while (node != null) {
//沿左侧分支,反复地入栈
stack.push(node);
node = node.left;
}
}

private static class Node<E> {
E data;
Node<E> left;
Node<E> right;
/**
* 以this为根的树的高度
*/
int height;

Node(E data) {
this(data, null, null);
}

Node(E data, Node<E> lt, Node<E> rt) {
this.data = data;
left = lt;
right = rt;
height = 0;
}

private void fillString(String prefix, boolean isTail, StringBuilder sb) {
if (right != null) {
right.fillString(prefix + (isTail ? "│ " : " "), false, sb);
}
sb.append(prefix).append(isTail ? "└── " : "┌── ").append(data).append("\n");
if (left != null) {
left.fillString(prefix + (isTail ? " " : "│ "), true, sb);
}
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
this.fillString("", true, sb);
return sb.toString();
}
}

public static void main(String[] args) {
/**
* 验证AVL的正确性
*/
SplittableRandom random = new SplittableRandom();
int [] values = random.ints(500,0,1000).toArray();

AVLTree<Integer> tree = new AVLTree<>();
for (int i = 0; i < values.length; i++) {
tree.insert(values[i]);
}

tree.checkBalance();

for (int i = 0; i < values.length; i++) {
tree.remove(values[i]);
tree.checkBalance();
}


tree = new AVLTree<>();
int [] newValues = {50,25,75,10,30,60,80,5,15,27,31,55,1,6,26};
for (int i = 0; i < newValues.length; i++) {
tree.insert(newValues[i]);
}
tree.printTree();

tree.remove(80);
tree.checkBalance();
tree.printTree();

}
}

本文相关代码实现涉及到栈和队列的请访问:栈和队列的实现

优缺点

优点

  • 无论查找、插入或删除,最坏的情况下的复杂度都是图解AVL树_图解AVL树的旋转操作_29图解AVL树_AVL树Java实现_30的存储空间

缺点

  • 比BST多了一个高度字段
  • 实现算法较为复杂
  • 插入/删除后的旋转,成本不低
  • 删除操作,最坏情况下需要图解AVL树_图解AVL树的旋转操作_31次旋转

若需频繁进行插入/删除操作,效率并没想象的那么高。