“树”据结构二:AVL树

前言

二叉搜索树比起来,AVL树(平衡二叉搜索树)才是应用最广泛的。因为二叉搜索树一旦退化成或者趋近于链表,那其存在的意义便消失了。所以,二叉搜索树要在每次增删之后重新恢复平衡,才能在查找的时候保证足够的效率。换句话说,AVL树的查找代价被平摊到每次rebalance上了,因此才会使得AVL树的搜索变得非常高效。

定义

AVL树,其名称来源于其创作者们的首字母缩写,不再赘述。根据维基百科的定义,AVL树有以下关键概念:

节点高度

某个节点所在的高度可以理解为以该节点为根的子树的高度。可以定义叶结点高度为1,其父结点的高度依次+1即可。

平衡因子

一个节点的平衡因子指其右子树与左子树的高度差:

BalanceFactor(N):=Height(RightSubtree(N))Height(LeftSubtree(N)) B a l a n c e F a c t o r ( N ) := H e i g h t ( R i g h t S u b t r e e ( N ) ) – H e i g h t ( L e f t S u b t r e e ( N ) ) 只有当所有节点的平衡因子在-1、0、1的范围内时,该二叉搜索树才能被称为AVL树,即该二叉搜索树是“平衡的”(
balanced)。当平衡因子小于0时,左子树高于右子树,该节点可被称为“左边偏重”(
left-heavy),反之则为“右边偏重”(
right-heavy)。

二叉树的旋转

在了解AVL树的平衡操作之前,先需要知道二叉树的两种旋转操作及其意义:左旋转、右旋转。
《“树”据结构二:AVL树》

左旋转

假设node节点为需要旋转的节点,当向左旋转时,node成为right节点的左子节点,right节点成为node的父节点,同时如果right存在左子节点,(由于现在right的左子节点被node占了)则此节点成为node的右子节点。

左旋转一共变动了三个指针(图中红色标注。当然,如果节点中定义了指向父节点的指针,这些指针自然也是需要变动的。)

左旋转的意义:使node节点的平衡因子-2。

右旋转

右旋转和左旋转是对称的。节点的变动如图右侧所示。

右旋转的意义:使node节点的平衡因子+2。

失衡类型及其调整

当二叉树上任意一个节点的平衡因子小于-1或大于1时,我们就说二叉树失衡了。

根据AVL失衡的定义,失衡无非两种情况:
1. Left-heavy;
2. Right-heavy.

先看第一种情况。假设失衡的节点为z,当z属于left-heavy时,其左子树(假设根结点为y)比右子树高2。假设右子树高为1,则左子树高为3。这也就是说子树y的高度为3,那么有可能y的左子树高度为2,也有可能y的右子树高度为2,但不可能二者皆为2:

因为AVL树每次失衡之后都会立即调节为平衡树,这也就意味着平衡状态下,在增删某个节点使该树重新变为不平衡之前,子树y的高度最高是2。因此在增删某个节点之后,子树y最多只有一棵子树高度能达到2。

L-L

对于上面的情况,可看到最长的分支在树z的子树y的子树上,也被称为Left-Left类型的失衡:

T1, T2, T3 and T4 are subtrees.
         z                                      y 
        / \                                   /   \        y   T4      Right Rotate (z)          x      z
      / \          - - - - - - - - ->      /  \    /  \ 
     x   T3                               T1  T2  T3  T4
    / \   T1   T2

这种情况下,z的平衡因子为-2。对于这种失衡,解决方式就是右旋转z节点,刚刚说道,右旋转可使节点平衡因子+2,因此z的平衡因子从-2变为0,重新归为平衡。(这一步只是让节点z平衡了,至于其他的节点,需要逐一去处理)

L-R

同样,left-heavy也有可能是树z的子树y的子树过高导致,这种情况被称为Left-Right类型的失衡:

     z                               z                           x
    / \                            /   \                        /  \ 
   y   T4  Left Rotate (y)        x    T4  Right Rotate(z)    y      z
  / \      - - - - - - - - ->    /  \      - - - - - - - ->  / \    / \ T1   x                          y    T3                    T1  T2 T3  T4
    / \                        / \   T2   T3                    T1   T2

这种情况下,z的平衡因子为-2,但是不能直接右旋转z,因为根据右旋转的方式,旋转过后子树x成为了z的左子树,此时z的高度为3,而y的高度为1,z的平衡因子变为3-1=2,于是我们把一个left-heavy的节点旋转成了right-heavy,依旧是不平衡的。

解决的方法也很简单:先把z的左子树变成L-L的情况,然后再右旋转z即可。所以先左旋转z的左子树的根y,再右旋转z,节点z重归平衡。

以上只是假设使节点z失衡的为left-heavy情况,同理,对于right-heavy情况也有两种:

R-L

树z的子树y的子树过高,Right-Left

   z                            z                            x
  / \                          / \                          /  \ 
T1   y   Right Rotate (y)    T1   x      Left Rotate(z)   z      y
    / \  - - - - - - - - ->     /  \   - - - - - - - ->  / \    / \    x   T4                      T2   y                  T1  T2  T3  T4
  / \                              /  \ T2   T3                           T3   T4

z的平衡因子为2,所以先右旋转z的右子树的根y,再左旋转左子树z。

R-R

树z的子树y的子树过高,Right-Right

  z                                y
 /  \                            /   \ 
T1   y     Left Rotate(z)       z      x
    /  \   - - - - - - - ->    / \    / \    T2   x                     T1  T2 T3  T4
       / \      T3  T4

z的平衡因子为2,左旋转可使其平衡因子-2,重归平衡。

总结一下:
– L-L:rotate right;
– R-R:rotate left;
(和名字顺序颠倒,L则右转,R则左转)
– L-R:rotate left, then rotate right;
– R-L:rotate right, then rotate left.
(和名字顺序一样,L-R就是先左后右,R-L就是先右后左)

算法

数据结构

AVL树跟普通二叉搜索树相比,需要额外存储平衡因子(或者说存储树的高度,由此算出平衡因子),因此和二叉搜索树的节点可以是继承关系:

    protected class AVLNode<T extends Comparable<T>> extends Node<T> {

        protected int height = 1;

        /** * Constructor for an AVL node * * @param parent * Parent of the node in the tree, can be NULL. * @param value * Value of the node in the tree. */
        protected AVLNode(Node<T> parent, T value) {
            super(parent, value);
        }

        /** * Determines is this node is a leaf (has no children). * * @return True if this node is a leaf. */
        protected boolean isLeaf() {
            return ((lesser == null) && (greater == null));
        }

        /** * Updates the height of this node based on it's children. */
        protected int updateHeight() {
            int lesserHeight = getSubTreeHeight(lesser);
            int greaterHeight = getSubTreeHeight(greater);

            return height = lesserHeight > greaterHeight ? lesserHeight + 1 : greaterHeight + 1;
        }

        /** * Get the balance factor(greaterHeight - lesserHeight) for this node. * * @return An integer representing the balance factor for this node. It * will be negative if the lesser branch is longer than the * greater branch. */
        protected int getBalanceFactor() {
            return getSubTreeHeight(greater) - getSubTreeHeight(lesser);
        }

        private int getSubTreeHeight(Node<T> subRoot) {
            int height = 0;
            if (subRoot != null) {
                AVLNode<T> subRootAVLNode = (AVLNode<T>) subRoot;
                height = subRootAVLNode.height;
            }
            return height;
        }

        /** * {@inheritDoc} */
        @Override
        public String toString() {
            return "value=" + id + " height=" + height + " parent=" + ((parent != null) ? parent.id : "NULL")
                    + " lesser=" + ((lesser != null) ? lesser.id : "NULL") + " greater="
                    + ((greater != null) ? greater.id : "NULL")youbianpianzhong;
        }
    }

AVL树与普通二叉搜索树在查询上并没有什么区别,在增删上也一样。但是增删过后可能会使二叉搜索树不再平衡,所以每次增删过后都需要对树进行rebalance操作。

    /** * {@inheritDoc} */
    @Override
    protected Node<T> addValue(T id) {
        Node<T> nodeToReturn = super.addValue(id);
        AVLNode<T> nodeAdded = (AVLNode<T>) nodeToReturn;

        // this new added node has no children:
        // no need to update height
// nodeAdded.updateHeight();
        // no need to balance
// balanceAfterInsert(nodeAdded);

        nodeAdded = (AVLNode<T>) nodeAdded.parent;
        while (nodeAdded != null) {
            int h1 = nodeAdded.height;

            nodeAdded.updateHeight();
            balanceAfterInsert(nodeAdded);

            // If height before and after balance is the same, stop going up the tree
            int h2 = nodeAdded.height;
            if (h1==h2)
                break;

            nodeAdded = (AVLNode<T>) nodeAdded.parent;
        }
        return nodeToReturn;
    }

增加节点之后,该节点的父节点也许不再平衡,因此对其父节点递归进行rebalance操作。

考虑一种情况:如果对某节点rebalance前后,该节点高度并没有变化,则该节点的rebalance不会影响其父节点的平衡性,那么rebalance过程可以就此终止,无需一直递归到root节点。

rebalance after add

具体的rebalance操作:

    /** * Balance the tree according to the AVL post-insert algorithm. * * @param node * Root of tree to balance. */
    private void balanceAfterInsert(AVLNode<T> node) {
        int balanceFactor = node.getBalanceFactor();

        // need to balance
        if (balanceFactor > 1 || balanceFactor < -1) {
            AVLNode<T> child = null;
            Balance balance = null;
            // left-heavy
            if (balanceFactor < 0) {
                child = (AVLNode<T>) node.lesser;
                balanceFactor = child.getBalanceFactor();
                // left-heavy
                if (balanceFactor < 0) {
                    // left-left(-heavy)
                    balance = Balance.LEFT_LEFT;
                }
                else {
                    // left-right(-heavy)
                    balance = Balance.LEFT_RIGHT;
                }
            // right-heavy
            } else {
                child = (AVLNode<T>) node.greater;
                balanceFactor = child.getBalanceFactor();
                // left-heavy
                if (balanceFactor < 0) {
                    // right-left(-heavy)
                    balance = Balance.RIGHT_LEFT;
                }
                // right-heavy
                else {
                    // right-right(-heavy)
                    balance = Balance.RIGHT_RIGHT;
                }
            }

            if (balance == Balance.LEFT_RIGHT) {
                // Left-Right (Left rotation, right rotation)
                rotateLeft(child);
                rotateRight(node);
            } else if (balance == Balance.RIGHT_LEFT) {
                // Right-Left (Right rotation, left rotation)
                rotateRight(child);
                rotateLeft(node);
            } else if (balance == Balance.LEFT_LEFT) {
                // Left-Left (Right rotation)
                rotateRight(node);
            } else {
                // Right-Right (Left rotation)
                rotateLeft(node);
            }

            child.updateHeight();
            node.updateHeight();
        }
    }

具体的rebalance过程就是:
1. 判断该node是否失衡;
2. 如果失衡,属于哪种失衡(LL/LR/RL/RR);
3. 对于每一种失衡,进行相应的旋转操作即可。

rotate left

    /** * Rotate tree left at sub-tree rooted at node. * * @param node * Root of tree to rotate left. */
    protected void rotateLeft(Node<T> node) {
        Node<T> parent = node.parent;
        Node<T> greater = node.greater;
        Node<T> lesser = greater.lesser;

        greater.lesser = node;
        node.parent = greater;

        node.greater = lesser;

        if (lesser != null)
            lesser.parent = node;

        if (parent!=null) {
            if (node == parent.lesser) {
                parent.lesser = greater;
            } else if (node == parent.greater) {
                parent.greater = greater;
            } else {
                throw new RuntimeException("Yikes! I'm not related to my parent. " + node.toString());
            }
            greater.parent = parent;
        } else {
            root = greater;
            root.parent = null;
        }
    }

具体的旋转,无非就是按照之前的图示,调整几个指针(红色箭头)的指向。由于我们给AVL节点增加了parent指针,所以当节点变动的时候,同时需要调整其parent节点的指向。

rotate right

    /** * Rotate tree right at sub-tree rooted at node. * * @param node * Root of tree to rotate right. */
    protected void rotateRight(Node<T> node) {
        Node<T> parent = node.parent;
        Node<T> lesser = node.lesser;
        Node<T> greater = lesser.greater;

        lesser.greater = node;
        node.parent = lesser;

        node.lesser = greater;

        if (greater != null)
            greater.parent = node;

        if (parent!=null) {
            if (node == parent.lesser) {
                parent.lesser = lesser;
            } else if (node == parent.greater) {
                parent.greater = lesser;
            } else {
                throw new RuntimeException("Yikes! I'm not related to my parent. " + node.toString());
            }
            lesser.parent = parent;
        } else {
            root = lesser;
            root.parent = null;
        }
    }

右旋转同理。

    /** * {@inheritDoc} */
    @Override
    protected Node<T> removeValue(T value) {
        // Find node to remove
        Node<T> nodeToRemoved = this.getNode(value);
        if (nodeToRemoved==null)
            return null;

        // Find the replacement node
        Node<T> replacementNode = this.getReplacementNode(nodeToRemoved);

        // Find the parent of the replacement node to re-factor the height/balance of the tree
        AVLNode<T> nodeToRefactor = null;
        if (replacementNode != null)
            nodeToRefactor = (AVLNode<T>) replacementNode.parent;
        if (nodeToRefactor == null)
            nodeToRefactor = (AVLNode<T>) nodeToRemoved.parent;
        if (nodeToRefactor != null && nodeToRefactor == nodeToRemoved)
            nodeToRefactor = (AVLNode<T>) replacementNode;

        // Replace the node
        replaceNodeWithNode(nodeToRemoved, replacementNode);

        // Re-balance the tree all the way up the tree
        while (nodeToRefactor != null) {
            nodeToRefactor.updateHeight();
            balanceAfterDelete(nodeToRefactor);

            nodeToRefactor = (AVLNode<T>) nodeToRefactor.parent;
        }

        return nodeToRemoved;
    }

删节点同二叉搜索树中的操作。删后需要做rebalance,和增加节点一样,一直循环对该节点的父节点做rebalance操作即可。

rebalance after delete

    /** * Balance the tree according to the AVL post-delete algorithm. * * @param node * Root of tree to balance. */
    private void balanceAfterDelete(AVLNode<T> node) {
        int balanceFactor = node.getBalanceFactor();
        if (balanceFactor == -2 || balanceFactor == 2) {
            if (balanceFactor == -2) {
                AVLNode<T> ll = (AVLNode<T>) node.lesser.lesser;
                int lesser = (ll != null) ? ll.height : 0;
                AVLNode<T> lr = (AVLNode<T>) node.lesser.greater;
                int greater = (lr != null) ? lr.height : 0;
                if (lesser >= greater) {
                    rotateRight(node);
                    node.updateHeight();
                    if (node.parent != null)
                        ((AVLNode<T>) node.parent).updateHeight();
                } else {
                    rotateLeft(node.lesser);
                    rotateRight(node);

                    AVLNode<T> p = (AVLNode<T>) node.parent;
                    if (p.lesser != null)
                        ((AVLNode<T>) p.lesser).updateHeight();
                    if (p.greater != null)
                        ((AVLNode<T>) p.greater).updateHeight();
                    p.updateHeight();
                }
            } else if (balanceFactor == 2) {
                AVLNode<T> rr = (AVLNode<T>) node.greater.greater;
                int greater = (rr != null) ? rr.height : 0;
                AVLNode<T> rl = (AVLNode<T>) node.greater.lesser;
                int lesser = (rl != null) ? rl.height : 0;
                if (greater >= lesser) {
                    rotateLeft(node);
                    node.updateHeight();
                    if (node.parent != null)
                        ((AVLNode<T>) node.parent).updateHeight();
                } else {
                    rotateRight(node.greater);
                    rotateLeft(node);

                    AVLNode<T> p = (AVLNode<T>) node.parent;
                    if (p.lesser != null)
                        ((AVLNode<T>) p.lesser).updateHeight();
                    if (p.greater != null)
                        ((AVLNode<T>) p.greater).updateHeight();
                    p.updateHeight();
                }
            }
        }
    }

同样也是先判断失衡类型(LL/LR/RL/RR),然后进行相应的旋转操作。

总结

AVL树才是最常用的二叉搜索树。其存在的意义显而易见,关键是要知道和rebalance相关的旋转操作是怎么进行的,以及旋转能达到什么效果。

参阅

对以下内容作者深表感谢:
1. https://en.wikipedia.org/wiki/AVL_tree
2. https://www.geeksforgeeks.org/avl-tree-set-1-insertion/
3. https://github.com/puppylpg/java-algorithms-implementation/blob/master/src/com/jwetherell/algorithms/data_structures/AVLTree.java

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