一种简单的平衡树-AVL树

AVL 树

AVL树(Adelson-Velskii 和 Laandis)树是带有平衡条件(balance condition)的二叉查找树。这个平衡条件必需要容易保持,并且他保证树的深度必须是 O(log N)。最简单的想法是要求左右子树具备相同的高度。java

clipboard.png

另外一种平衡条件是要求每一个节点都必须有相同高度的左子树和右子树。若是空子树的高度定义为 -1,那么只有具备 2k-1 个节点的理想平衡树(perfectly balanced tree)知足这个条件。所以,虽然这种平衡条件保证了树的深度小,可是太严格难以使用。ide

AVL 树是其每一个节点的左子树和右子树的高度最多差 1 的二叉查找树(空树的高度定义为 -1)。函数

clipboard.png

下图是一棵具备最少节点(143)和高度为 9 的 AVL 树。这棵树的左子树是高度为 7 且大小最小的 AVL 树,右子树是高度为 8 且大小最小的 AVL 树。他告诉咱们,在高度为 h 的 AVL 树种,最少节点数由this

  • S(h) = S(h - 1) + S(h - 2) + 1 给出spa

对于 h = 0,S(h) = 1;h = 1, S(h) = 2。函数 S(h) 与斐波那契数列密切相关。3d

clipboard.png

所以,出去可能的插入外(咱们将假设懒惰删除),全部的树操做均可以以时间 O(log N) 执行。当执行插入操做时,咱们须要更新通向根节点路径上的那些节点的全部平衡信息,而插入操做隐含的困难的缘由在于,插入一个节点可能破坏 AVL 树的特性。若是发生这种状况,那么就要在考虑这一步插入完成以前回复平衡的性质。
事实上,咱们能够经过旋转来对树进行简单的修正来作到。code

  • 在插入之后,只有那些从插入点到根节点的路径上的节点的平衡可能被改变,由于只有这些节点的子树可能发生变化。当咱们沿着这条路径上行到根并更新平衡信息时,能够发现一个节点,它的新平衡破坏了 AVL 条件。咱们将支出如何在第一个这样的节点(即最深的节点)从新平衡这棵树,并证实这一从新平衡保证整个树知足 AVL 性质。blog

咱们把必须从新平衡的节点叫作 α。因为任意节点最多只有两个儿子,所以出现高度不平衡就须要 α 点的两颗子树的高度差 2。这可能出如今一下四中状况中:递归

  1. 对 α 的左儿子的左子树进行一次插入;ip

  2. 对 α 的左儿子的右子树进行一次插入;

  3. 对 α 的右儿子的左子树进行一次插入;

  4. 对 α 的右儿子的右子树进行一次插入。

情形 1 和 4 是关于 α 点的镜像对称,而 2 和 3 是关于 α 点的镜像对称。

第一种状况是插入发生在 "外边" 的状况(即左-左的状况或右-右的状况),该状况经过对树的一次单旋转(single rotation)而完成调整。第二种状况是插入发生在 "内部" 的状况(即左-右的状况或右-左的状况),该状况经过稍微复杂一些的双旋转(double rotation)实现。

clipboard.png

情形可能以下所示:

/**
     * 考虑对以下的树中插入数字 2
     * 
     *                 5
     *               /   \
     *             4      6
     *           /   \
     *         3       ?
     * 
     *                  4
     *               /     \
     *             3        5
     *           /        /   \
     *         2        ?      6
     * 
     * ? 表明可能存在也可能不存在,不会影响结果.而且,在这里,咱们能够将 ? 当作 5 的左节点,也能够将 
     * ? 当作 3 的左节点
     * @param k2
     * @return
     */
  • k2不知足 AVL 平衡性质,由于他的左子树比右子树深 2 层(图中间的虚线表示树的各层)。该图所描述的状况是情形 1 的一种可能的状况,在插入以前 k2知足 AVL 性质,可是在插入以后,子树 X 长出一层,这使得他比子树 Z 深出 2 层。Y 不可能与新 X 在同一水平上,由于那样 k2 在插入之前就已经失去平衡了;Y 也不可能与 Z 在同一层上,由于那样 k1 就会是在通向根路径上破坏 AVL 平衡条件的第一个节点。

如今咱们观察这棵树,咱们会获得一些结论:

kX < k1 < kY < k 2 < kZ,由于如今节点 X 的深度为 2,因此节点 X 必须为节点 1 的一个单独的节点;
又由于 kY < k2 < kZ。因此可使用 k2 做为根节点,kY 和 kZ 做为 k2 的左节点和右节点生成新的 AVL 树。
最后,以这棵新生成的 AVL 树做为 k1 的右节点。

clipboard.png

咱们沿着插入的节点上行,找到第一个特殊的节点,这个特殊的节点破坏了 AVL 树的平衡;
若是是状况 1,那么咱们假设链接插入数据的节点和特殊节点的节点为 k1,这个特殊的节点为 k2。那么,咱们如今只须要用 k1 当作新的根节点,左节点不变,同时将 k1 的右节点当作 k2 的左节点,并将 k2 做为 k1 的新的右节点便可。
若是是状况 4,那么咱们这个特殊的节点为 k1,链接插入数据的节点和特殊节点的节点为 k2。那么,咱们如今可使用 k2 做为新的根节点,右节点不变,同时将 k2 的左节点当作 k1 的右节点,最后将 k1 做为 k2 的新的左节点便可。

双旋转

clipboard.png

clipboard.png

clipboard.png

/**
     * 对于情形 2 的一次双旋转
     * 
     *                k3
     *               /   \
     *            k1      D
     *           /   \
     *         A       k2
     *
     * 插入节点 B 或 C,B 和 C 在插入任意一个的时候就已经破坏了 Avl 树的平衡条件
     *
     *                k3
     *               /   \
     *            k1       D
     *           /   \
     *         A       k2
     *                / \
     *              B    C
     * 
     * k3.left = rotateWithRightChild(k3.left),以 k3.left 即 k1 为根节点进行一次右旋转
     * 
     *                k3
     *               /   \
     *            k2      D
     *           /   \
     *         k1       C
     *        / \         
     *      A      B    
     * 
     * rotateWithLeftChild(k3)
     * 
     *                  k2
     *               /      \
     *            k1        k3
     *           /   \       /  \
     *         A      B    C      D
     * 
     * @param k3
     * @return
     */

咱们注意到,在上面的图中,k1 < k2 < k3,这迫使 k1 是 k2 的左儿子,k3 是 k2 的右儿子。因而,最后咱们获得的结果就很明显了。

须要旋转的三个节点是:上行找到的第一个破坏 AVL 树的节点,新的节点插入后的父节点,链接第一个破坏 AVL 树的节点和新的节点的父节点

假设这三个节点分别是 k1,k2,k3,则 k1 和 k3 是根据插入的数据是状况 2 或状况 3 下变化的,而 k2 永远都是那个被插入数据的节点。
在状况 2 下,k1 是链接特殊节点和被插入数据节点的节点,k3 是特殊节点;
在状况 3 下,k1 是特殊节点,k3 是链接特殊节点和被插入数据节点的节点;
在状况 2 和 3 下,咱们都是使用 k2 做为新的根节点,k1 做为左儿子,k3 做为右儿子;
随后,咱们将 k2 原来的左子节点做为 k1 的右子节点,k2 的原来的右子节点做为 k3 的左子节点。

不管是单旋转仍是双旋转,咱们都须要将获得的新的 AVL 子树添加到原来的树中。

AVL 树的插入方法的实现

为了将项是 X 的一个新节点插入到一棵 AVL 树 T 种去,咱们递归的将 X 插入到 T 相应的子树(称为 TLR)中。若是 TLR 的高度不变,那么插入完成。不然,若是在 T 中出现高度不平衡,则根据 X 以及 T 和 TLR 中的项左适当的单旋转或双旋转,更新这些高度(并解决好与树的其他部分的连接)从而完成插入。

package com.mosby.common.structure;

/**
 * AVL 树
 */
public class AvlTree <T extends Comparable<? super T>> {
    private static  class AvlNode <T extends Comparable<? super T>>{
        AvlNode(T element){
            this(element, null, null);
        }
        AvlNode(T element, AvlNode<T> left, AvlNode<T> right){
            this.element = element;
            this.left = left;
            this.right = right;
            this.height = 0;
        }
        T element;
        AvlNode<T> left;
        AvlNode<T> right;
        int height;
        @Override
        public String toString(){
            return element.toString();
        }
    }
    public AvlTree(){
        root = null;
    }
    public void insert(T x){
        root = insert(x, root);
    }
    
    public void remove(T x){
        System.out.println("Sorry, remove unimplemented");
    }
    
    public T findMin(){
        return elementAt(findMin(root));
    }
    public T findMax(){
        return elementAt(findMax(root));
    }
    public T find(T x){
        return elementAt(find(x, root));
    }
    public void makeEmpty(){
        root = null;
    }
    
    public boolean isEmpty(){
        return root == null;
    }
    private T elementAt(AvlNode<T> t){
        return t == null ? null : t.element;
    }
    private AvlNode<T> findMin(AvlNode<T> t){
        if(t == null){
            return t;
        }
        while(t.left != null){
            t = t.left;
        }
        return t;
    }
    private AvlNode<T> findMax(AvlNode<T> t){
        if(t == null){
            return t;
        }
        while(t.right != null){
            t = t.right;
        }
        return t;
    }
    private AvlNode<T> find(T x, AvlNode<T> t){
        while(t != null){
            if(x.compareTo(t.element) < 0){
                t = t.left;
            }else if(x.compareTo(t.element) > 0){
                t = t.right;
            }else{
                return t;
            }
        }
        return null;
    }
    /**
     * 插入方法
     * @param x
     * @param t
     * @return
     */
    private AvlNode<T> insert(T x, AvlNode<T> t){
        if(t == null){
            return new AvlNode<T>(x);
        }else if(x.compareTo(t.element) < 0){
            t.left = insert(x, t.left);
            if(height(t.left) - height(t.right) == 2){
                if(x.compareTo(t.left.element) < 0){
                    t = rotateWithLeftChild(t);
                }else{
                    t = doubleWithLeftChild(t);
                }
            }
        }else if(x.compareTo(t.element) > 0){
            t.right = insert(x, t.right);
            if(height(t.right) - height(t.left) == 2){
                if(x.compareTo(t.right.element) > 0){
                    t = rotateWithRightChild(t);
                }else{
                    t = doubleWithRightChild(t);
                }
            }
        }else{
            //Duplicate; do nothing
        }
        t.height = max(height(t.left), height(t.right)) + 1;
        return t;
    }
    
    /**
     * 插入相关操做
     */
    private static <T extends Comparable<? super T>> int height(AvlNode<T> t){
        return t == null ? -1 : t.height;
    }
    private static int max(int lhs, int rhs){
        return lhs > rhs ? lhs : rhs;
    }
    /**
     * 对于情形 1 的一次旋转
     * 
     *                 5
     *               /   \
     *             4      6
     *           /   \
     *         3        ?
     * 
     *                  4
     *               /      \
     *             3         5
     *           /         /   \
     *         2        ?        6
     * 
     * ? 表明可能存在也可能不存在,不会影响结果
     * @param k2
     * @return
     */
    private static <T extends Comparable<? super T>> AvlNode<T> rotateWithLeftChild(AvlNode<T> k2){
        //k2 是节点 5,k1 是节点 4
        AvlNode<T> k1 = k2.left;
        k2.left = k1.right;
        k1.right = k2;
        /**
         * 咱们能够看到,节点中有三个节点的高度改变了:3,4,5.
         * 而咱们在最后插入时的函数栈以下所示:
         * 2, null
         * ------
         * 2,    3
         * ------
         * 2,    4
         * ------
         * 2,    5
         */
        k2.height = max(height(k2.left), height(k2.right)) + 1;
        k1.height = max(height(k1.left), height(k1.right)) + 1;
        return k1;
    }
    /**
     * 对于情形 4 的一次旋转
     * 
     *                 5
     *               /   \
     *             4        6
     *                    /  \
     *                  ?      7
     * 
     * 
     *                  6
     *               /   \
     *             5        7
     *           /   \       \
     *         4        ?     8
     * 
     * @param k1
     * @return
     */
    private static<T extends Comparable<? super T>> AvlNode<T> rotateWithRightChild(AvlNode<T> k1){
        //k1 是节点 5,k2 是节点 6
        AvlNode<T> k2 = k1.right;
        k1.right = k2.left;
        k2.left = k1;
        /**
         * 节点中有 3 个节点的高度改变了:5,6,7
         * 在最后插入时的函数栈以下所示:
         * 8, null
         * ------
         * 8,   7
         * ------
         * 8,   6
         * ------
         * 8,   5
         */
        k1.height = max(height(k1.left), height(k1.right)) + 1;
        k2.height = max(height(k2.left), height(k2.right)) + 1;
        return k2;
    }
    
    /**
     * 对于情形 2 的一次双旋转
     * 
     *                k3
     *               /  \
     *            k1     D
     *           /   \
     *         A       k2
     *
     * 插入节点 B 或 C,B 和 C 在插入任意一个的时候就已经破坏了 Avl 树的平衡条件
     *
     *                k3
     *               /  \
     *            k1     D
     *           /   \
     *         A       k2
     *                / \
     *              B       C
     * 
     * k3.left = rotateWithRightChild(k3.left),以 k3.left 即 k1 为根节点进行一次右旋转
     * 
     *                k3
     *               /  \
     *            k2     D
     *           /   \
     *         k1       C
     *        / \         
     *      A      B    
     * 
     * rotateWithLeftChild(k3)
     * 
     *                  k2
     *               /      \
     *            k1         k3
     *           /   \      /  \
     *         A      B   C     D
     * 
     * @param k3
     * @return
     */
    private static <T extends Comparable<? super T>> AvlNode<T> doubleWithLeftChild(AvlNode<T> k3){
        /**
         * k1 是节点 4,k2 是节点 5,k3 是节点 7
         * 咱们看到,在第一次旋转中,有几个节点的高度改变了:k二、k3
         * 在插入的时候,函数栈应该以下所示:
         * B|C  null
         * --------
         * B|C   k2
         * --------
         * B|C   k1
         * --------
         * B|C   k3
         */
        k3.left = rotateWithRightChild(k3.left);
        return rotateWithLeftChild(k3);
    }
    private static <T extends Comparable<? super T>> AvlNode<T> doubleWithRightChild(AvlNode<T> k1){
        k1.right = rotateWithLeftChild(k1.right);
        return rotateWithRightChild(k1);
    }
    private AvlNode<T> root;
    
    public static void main(String[] args) {
        AvlTree<Integer> avl = new AvlTree<Integer>();
        avl.insert(5);
        avl.insert(4);
        avl.insert(3);
        avl.insert(2);
        avl.insert(1);
        avl.insert(0);
    }
}

注意几点:

  1. 所谓的 rotateLeft 和 rotateRight 是指旋转左树仍是右树,而不是指向左旋转仍是向右旋转。例如 rotateLeft(t) 就是将 t.left 和 t 调换位置;

  2. 咱们在递归中已经修改了 height,为何在旋转的时候还要修改树的高度呢?
    以左旋转为例子:

    \* 2, null
        \* ------
        \* 2,    3
        \* ------
        \* 2,    4
        \* ------
        \* 2,    5

    在这个函数栈中,4 相对于 5 先出栈,而且在递归的求 4 的高度的时候须要用到 5 的高度,而 5 在进行旋转以后它的高度就已经改变了,因此咱们必须在旋转后当即改变 5 的高度。

而 3 虽然它的高度改变了,可是 3 在栈中的位置决定了它在 null 出栈后即运行,且它仅仅会用到新插入的节点的高度,而新插入的节点的高度显然是为 0 的,因此它不须要作特别的处理,等出栈的时候计算它的高度就能够了。

相关文章
相关标签/搜索