前面讲到了二叉查找树,虽然能够很好的应用于大多数的场景,但是他们在最坏情况下性能还是很差的,如二叉查找树最坏情况下是一颗高度为N的树,显然不利于查找。因此我们需要让查找树保持一种平衡,如二叉查找树最优情况的状态一样。
一、AVL树基础
AVL树是一颗保持平衡的二叉查找树,保证了树的深度为O(logN)。
其每个节点的左子树和右子树的高度都最多相差1
上图就是一颗AVL树,其任意节点的左孩子和右孩子的高度差不超过1。
但是当我们插入或删除一个节点时,很容易破坏AVL树的平衡度,因此我们需要一种操作,在插入或删除完成前重新对树进行平衡,恢复AVL树的性质,这种操作就是旋转。
假设待平衡节点为 A,则不平衡的条件就是A 的两颗子树的高度差为2,因此就会出现四种情况,针对不同情况我们做不同的旋转操作:
- 1.在A的左孩子的左子树进行插入 (单旋转,向右旋转)
- 2.在A的左孩子的右子树进行插入 (双旋转,先左,再右)
- 3.在A的右孩子的左子树进行插入 (双旋转,先右再左)
- 4.在A的右孩子的右子树进行插入 (单旋转,向左旋转)
情况1:
如下图左树,插入了一个节点G后,在A点处失去了平衡(这里树的节点无意义,随意取的,只关注平衡性即可),因此我们需要在A处将树向右旋转,变成右边的树,可以想象成用手提着B,将其提起来,然后将B的右孩子变为A的左孩子
对应代码如下:
右旋转就先获取其左孩子left
1.node的左孩子变为left的右孩子
2.left的右孩子变为node
3.重新计算node和left的高度
/**
* 单旋转(右旋转)(情形1)
* @param node
* @return
*/
private AVLNode<T>rotateRight(AVLNode<T> node){
AVLNode<T> left=node.left;
node.left=left.right;
left.right=node;
node.height=Math.max(height(node.left),height(node.right))+1;
left.height=Math.max(height(node.left),node.height)+1;
return left;
}
情况4:
如下图1,在A的右孩子的右子树插入了节点G,此时在A节点失去了平衡,因此在A处向左旋转,原理同情况1。
实现如下:
左旋转就先获取其右孩子right
1.node的右孩子变为right的左孩子
2.right的左孩子变为node
3.重新计算node和right的高度
/**
* 单旋转(左旋转)(情形4)
* @param node
* @return
*/
private AVLNode<T> rotateLeft(AVLNode<T> node){
AVLNode<T> right=node.right;
node.right=right.left;
right.left=node;
node.height=Math.max(height(node.left),height(node.right))+1;
right.height=Math.max(node.height,height(right.right))+1;
return right;
}
情况2:
如下图1,在A的左孩子B的右子树插入了节点D后,在A处失去了平衡,因此需要先对其左孩子B进行左旋转,变成了图2,不难发现图2的情况就是情况1,因此我们只要再对A进行右旋转就可以恢复树的平衡性。
之所以会变成情况1,是因为对B进行左旋转后,按照左旋转的特点,会将B的右子树G提升一个高度到B的位置,B自然就会下去一个高度,再加上一个新的G,自然也就变成了情况1。
如下图3,对A进行右旋转后,树已恢复平衡。
实现如下:
有了情况1和情况4的基础,再实现情况2就很简单:
1.先对A的左孩子左旋转
2.再对A进行右旋转
/**
* 双旋转(先左后右)(针对情形2)
* @param node
* @return
*/
private AVLNode<T> doubleLeftAndRight(AVLNode<T> node){
node.left=rotateLeft(node.left);
return rotateRight(node);
}
情况3:
如图1,在A的右孩子的左子树加了一个节点D,造成A处失去平衡,因此先对E进行右旋转变成图2,此时变成情况4,再对A进行左旋转变成图3,就恢复了平衡。
实现如下:
/**
* 双旋转(先右再左) (针对情形3)
* @param node
* @return
*/
private AVLNode<T> doubleRightAndLeft(AVLNode<T> node){
node.right=rotateRight(node.right);
return rotateLeft(node);
}
有了上面的基础,再对AVL树操作就不困难了。
二、数据结构
也是一个左孩子,一个右孩子,一个数据域,外加一个当前节点的高度,当然你也可以使用Key-Value结构,按Key进行构造树,一个key关联一个value,这里简单使用了一个泛型T
public class AVLTree<T extends Comparable<? super T>> {
private AVLNode<T> root;
//节点的左右孩子高度差不能超过该值
private static final int HEIGHT_DIFFERENCE=1;
public AVLTree(AVLNode<T> root) {
this.root = root;
}
}
class AVLNode<T>{
AVLNode(T t){
this(t,null,null);
}
AVLNode(T data,AVLNode<T> lt,AVLNode<T> rt){
this.data=data;
left=lt;
right=rt;
this.height=0;
}
T data;
AVLNode<T> left;
AVLNode<T> right;
int height;
}
AVL树返回最大值最小值以及contains方法同上一篇二叉查找树相同,不赘述。
三、插入一个元素
public void insert(T data){
if (data==null){
throw new IllegalArgumentException("数据为空");
}
root= insert(data, this.root);
}
private AVLNode<T> insert(T data,AVLNode<T> t){
if (t==null){
return new AVLNode<>(data,null,null);
}
int compareResult=data.compareTo(t.data);
if (compareResult<0){
t.left=insert(data,t.left);
}else if (compareResult>0){
t.right=insert(data,t.right);
}else {
;
}
return balance(t);
}
可以看出,除了最后一行,其余部分同二叉查找树相同。
每次insert()调用后,都要对当前节点进行重新平衡,即当找到插入的节点后,按递归调用栈的顺序,对路径上的每个节点进行重新平衡。
balance()
节点平衡代码如下:
/**
* 对节点t进行平衡
* @param t
* @return
*/
private AVLNode<T> balance(AVLNode<T> t) {
if (t==null){
return null;
}
if (height(t.left)-height(t.right)>HEIGHT_DIFFERENCE){
//左边高
if (height(t.left.left)>=height(t.left.right)){
//左子树的左子树高
//此为情形1,在左子树的左子树插入了元素,直接右旋转
t=rotateRight(t);
}else {
//左子树的右子树高
//此为情形2,在左子树的右子树插入了元素,先左旋转再右旋转
t=doubleLeftAndRight(t);
}
}else if (height(t.right)-height(t.left)>HEIGHT_DIFFERENCE){
//右边高
if (height(t.right.left)>height(t.right.right)){
//右子树的左子树高了
//此为情形3,在右子树的左子树插入了元素,先右旋转再左旋转
t=doubleRightAndLeft(t);
}else {
//右子树的右子树高
//此为情形4,在右子树的右子树插入了元素,直接左旋转
t=rotateLeft(t);
}
}
//重新计算该节点的高度
t.height=Math.max(height(t.left),height(t.right))+1;
return t;
}
private int height(AVLNode<T> t){
return t==null?-1:t.height;
}
四、删除一个元素
逻辑同二叉查找树,同插入一样,需要在每次remove调用后对当前节点进行平衡。
/**
* 删除数据
* @param x
*/
public void remove( T x )
{
root = remove( x, root );
}
private AVLNode<T> remove(T data,AVLNode<T> root){
if (root==null){
return null;
}
int compareResult=data.compareTo(root.data);
if (compareResult<0){
root.left=remove(data,root.left);
}else if (compareResult>0){
root.right=remove(data,root.right);
}else if (root.left!=null&&root.right!=null){
//待删除节点有两个孩子
root.data=findMin(root).data;
root.right=remove(root.data,root.right);
}else {
//待删除节点没有孩子或有一个孩子
root=root.left==null?root.right:root.left;
}
//重新平衡树
return balance(root);
}
我们可以按照中序遍历返回AVL树的节点队列:
/**
* 返回数据队列
* @return
*/
public Queue<T>iterator(){
if (isEmpty()){
return null;
}else {
Queue<T>queue=new Queue<>();
return iterator(root,queue);
}
}
private Queue<T> iterator( AVLNode<T> t,Queue<T> queue )
{
if( t != null )
{
iterator( t.left,queue );
queue.enqueue(t.data);
iterator( t.right, queue);
}
return queue;
}
测试:
public static void main(String[] args) {
AVLTree<Integer> avlTree = new AVLTree<>(new AVLNode<>(1));
avlTree.insert(2);
avlTree.insert(3);
avlTree.insert(4);
avlTree.insert(5);
avlTree.insert(6);
avlTree.insert(7);
avlTree.insert(15);
avlTree.insert(16);
avlTree.insert(14);
//中序遍历打印
avlTree.printTree();
System.out.println("==========删除节点15=========");
avlTree.remove(15);
Queue<Integer> queue = avlTree.iterator();
while (!queue.isEmpty()){
System.out.println(queue.dequeue());
}
Integer max = avlTree.findMax();
System.out.println("最大元素:"+max);
System.out.println("节点17是否存在"+avlTree.contains(17));
}
结果:
1
2
3
4
5
6
7
14
15
16
==========删除节点15=========
1
2
3
4
5
6
7
14
16
最大元素:16
节点17是否存在false
总结:
AVL树总体上的实现和二叉查找树相同,只是为了重新让树恢复平衡需要在递归调用中对当前节点进行balance()。
AVL树的查找、插入或删除,最坏的情况下的复杂度均为O(log(n))