如何写一棵AVL树

二叉查找树

二叉查找树有一个缺陷就是查询效率跟树的高度有关。在极端情况下,查询效率为n。
《如何写一棵AVL树》

如何解决二叉查找树效率低问题?

要增加查询效率,高效的方案是在插入的时候对树进行一下平衡操作,降低树的高度,从而减少查询次数。
《如何写一棵AVL树》

如何将普通二叉树变为平衡二叉树

解决方案:在插入和删除阶段进行适当的调整

在平衡二叉树中有这样一个规定:

对于任意一个节点,如果其左右子树高度差小于1,那么该节点是平衡的

所以对于节点X,他平衡的条件是:

BF=getHeight(x.left)getHeight(x.right)<2;

在AVL树中有一个概念叫做平衡因子(Balance Factor),即上式中的BF;

先做一个说明,为了更好的理解旋转,本文的BF是实时计算的,这样可以专心于旋转。

AVL树的旋转

旋转其实很简单,只有两种:左旋和右旋。
但是两仪生四象,实际存在四种使用情况:

  1. 单左旋
  2. 单右旋
  3. 先左旋再右旋
  4. 先右旋在左旋

什么时候需要旋转?

前文已经解释了平衡因子的概念,只要该节点的BF不符合条件,就要对该节点进行调整。

左旋

实现分析:

《如何写一棵AVL树》

如图所示,新插入节点80(绿色)以后,节点B(蓝色)失去平衡,那么就要对节点B进行调整。
记住此时的平衡因子状态:

getBF(B)==2

getBF(B.right)==1

因为右子树更高,所以我们进行左旋转,将根节点甩到左下角。

左旋动态:

《如何写一棵AVL树》

代码实现

    public Node rotateLeft(Node node){
            Node temp = node;
            node = node.right;
            temp.right = node.left;
            node.left = temp;
            return node;
        }

右旋

实现分析:
《如何写一棵AVL树》

如图所示,新插入节点50(绿色)以后,节点B(蓝色)失去平衡,那么就要对节点B进行调整。
记住此时的平衡因子状态:

getBF(B)==2

getBF(B.left)==1

因为左子树更高,所以我们进行右旋转,将根节点甩到右下角。

右旋动态:

《如何写一棵AVL树》

代码实现

    public Node rotateRight(Node node){
            Node temp = node;
            node = node.left;
            temp.left = node.right;
            node.right = temp;
            return node;
        }

先左旋再右旋

实现分析:
《如何写一棵AVL树》

如图所示,新插入节点72(绿色)以后,节点B(蓝色)失去平衡,那么就要对节点B进行调整。
记住此时的平衡因子状态:

getBF(B)==2

getBF(B.left)==1

对于这种情况,我们不能简单的通过一种旋转来完成平衡,而是要先对节点B左子树进行一个左旋转,再对节点B进行右旋。

先左旋再右旋动态:

《如何写一棵AVL树》

代码实现
此处不用特别去实现,直接调用已经实现的左旋和右旋就可以。

 B.left = rotateLeft(B.left);
 B = rotateRight(B);

先右旋再左旋

实现分析:
《如何写一棵AVL树》

如图所示,新插入节点72(绿色)以后,节点B(蓝色)失去平衡,那么就要对节点B进行调整。
记住此时的平衡因子状态:

getBF(B)==2

getBF(B.right)==1

对于这种情况,我们不能简单的通过一种旋转来完成平衡,而是要先对节点B右子树进行进行一个左旋旋转,再对节点B进行左旋。

先右旋再左旋动态:

《如何写一棵AVL树》
代码实现
此处不用特别去实现,直接调用已经实现的左旋和右旋就可以。

B.right = rotateRight(B.right);
B = rotateLeft(B);

如何选择旋转的方式

本文我们采用的是**
左子树 – 右子树**的方式来计算平衡因子BF。

BF=Math.abs(getHeight(x.left)getHeight(x.right))

所以说的粗糙一点,BF只会是:0,1,-1,2,-2;因为到2就要进行平衡处理,所以不会有其他值。
如果getBF(x)==2,说明左子树更高,需要右旋;
右旋是对左子树进行操作,所以需要分析左子树。

  • 如果getBF(x.left) == 1;进行单左旋。
  • 如果getBF(x.left) == -1;进行先右旋再左旋。

如果BF==-2,说明右子树更高,需要左旋;
左旋是对右子树进行操作,所以需要分析右子树。

  • 如果getBF(x.right) == -1;进行单左旋。
  • 如果getBF(x.right) == 1;进行先左旋再右旋。

说点题外话,如果细心的话,可以发现上面的各种操作是镜面对称的。

实现

public class AVLTree<Key extends Comparable<Key>,Value> {
    private Node root;
    class Node{
        private Key key;
        private Value value;
        private Node left,right;
        public Node(Key key,Value value){
            this.key = key;
            this.value = value;
        }

    //返回节点高度
    public int getHeight(Node node){
        if(node == null) return 0;
        int leftHeight = getHeight(node.left) ;
        int rightHeight = getHeight(node.right) ;
        return Math.max(leftHeight, rightHeight) + 1;
    }

    //返回节点平衡因子
    public int getBF(Node node){
        return getHeight(node.left) - getHeight(node.right) ;
    }

    //判断节点是否平衡
    public boolean isBalanced(Node node){
        if(Math.abs(getBF(node))<2) return true;
        return false;
    }
    //左旋
    public Node rotateLeft(Node node){
        Node temp = node;
        node = node.right;
        temp.right = node.left;
        node.left = temp;
        return node;
    }
    //右旋
    public Node rotateRight(Node node){
        Node temp = node;
        node = node.left;
        temp.left = node.right;
        node.right = temp;
        return node;
    }

    public void add(Key key,Value value){
        root = add(root,key,value);
    }
    //分析
    private Node add(Node currentRoot,Key key,Value value){}

分析旋转的具体应用

插入和删除都有可能打破平衡,所以都可能进行旋转操作。其应用方式基本一样,现在我们使用插入来进行一个分析。

private Node add(Node currentRoot,Key key,Value value){
    if(currentRoot==null) return new Node(key,value);//递归基
            int cmp=key.compareTo(currentRoot.key);
            if(cmp<0){
                currentRoot.left = add(currentRoot.left,key,value);
            }else if(cmp>0){
                currentRoot.right = add(currentRoot.right,key,value);
            }else{
                //相等,进行覆盖操作
                currentRoot.value = value;
            }

    //插入操作完成
    //进行平衡检查与操作,此部分可以单独拿出去写
    currentRoot = balance(currentRoot);
    return currentRoot;
}


public Node balance(Node currentRoot){
    //检查是否平衡,如果不平衡,则进行调整
    if(!isBanlaced(currentRoot)){
        //如果平衡因子大于0,大趋势是右旋
        if(getBF(currentRoot)>0){
            //检查左子树平衡因子,如果左子树平衡因子等于1,单右旋
            if(getBF(currentRoot.left) == 1){
                currentRoot = rotateRight(currentRoot);
            //如果左子树平衡因子等于-1,先对左子树进行左旋,再对自身进行右旋
             }else if(getBF(currentRoot.left) == -1){
                currentRoot.left =rotateLeft(currentRoot.left);
                currentRoot = rotateRight(currentRoot);
            }
        }else{
            //如果平衡因子小于0,大趋势是左旋
            //检查右子树平衡因子,如果右子树平衡因子等于-1,单左旋
            if(getBF(currentRoot.right) == -1){
                currentRoot = rotateLeft(currentRoot);
            //如果右子树平衡因子等于1,先对右子树右旋,再对自身进行左旋
            }else if(getBF(currentRoot.right) == 1){
               currentRoot.right =rotateRight(currentRoot.right);
               currentRoot = rotateLeft(currentRoot);
            }
        }
    }
    //返回调整过的节点
    return currentRoot;
}

效率改进,增加height属性

上面的实现中要获得节点的高度需要遍历整颗子树,下面我们在节点的属性中添加height属性。

class Node{
        private Key key;
        private Value value;
        private Node left,right;
        private int BF;
        private int height;
        public Node(Key key,Value value){
            this.key = key;
            this.value = value;
            this.BF = 0;
            this.height = 1;
        }
    }

修改getHeight(Node node)方法

private int getHeight(Node node){
    if(node == null) return 0;
    if(node.left==null){
        if(node.right==null){
            return 1;
        }else{
            return node.right.height + 1;
        }
    }else{
        if(node.right == null){
            return node.left.height + 1;
        }else{
            return  Math.max(node.left.height, node.right.height) + 1;
        }
    }

由此,我们需要在添加节点、删除节点、旋转时重新计算高度并保存。

public Node rotateRight(Node node){
        Node temp = node;
        node = node.left;
        temp.left = node.right;
        node.right = temp;
        //调整高度
        node.right.height = height(node.right);
        node.height = height(node);
        return node;
    }

public Node rotateLeft(Node node){
        Node temp = node;
        node = node.right;
        temp.right = node.left;
        node.left = temp;
        //调整高度
        node.left.height = height(node.left);
        node.height = height(node);
        return node;
    }

private Node add(Node currentRoot,Key key,Value value){
        if(currentRoot==null) return new Node(key,value);//递归基
        int cmp=key.compareTo(currentRoot.key);
        if(cmp<0){
            currentRoot.left = add(currentRoot.left,key,value);
        }else if(cmp>0){
            currentRoot.right = add(currentRoot.right,key,value);
        }else{
            //相等,进行覆盖操作
            currentRoot.value = value;
        }
        //调整高度
        currentRoot.height = height(currentRoot);
        currentRoot = balance(currentRoot);
        return currentRoot;
    }

结束

目前已经实现一棵带插入功能的AVL树了,看完基本可以掌握AVL树的2种旋转方式和4种旋转场景。
但是目前的平衡因子是实时计算的,每次计算都会对对相应子树进行遍历。在完整的AVL树中,平衡因子BF是作为节点的属性保存在节点中的,这样每次调整平衡因子的时候可以只针对个别的节点进行操作,可以大大提高效率。
但引入平衡因子的计算会使问题变得复杂许多,不易于理解,所以本文并没有增加平衡因子计算,而是打算在将来单独拿出来写。

    原文作者:AVL树
    原文地址: https://blog.csdn.net/a298804870/article/details/53956224
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞