关闭

AVL树

503人阅读 评论(0) 收藏 举报
分类:

AVL(Adelson-Velskii and Landis)树是带有平衡条件(balance condition)的二叉查找树。这个平衡条件必须容易保持,而且必须保证树的深度是O(logN)。AVL树规定其每个结点的左子树和右子树的高度最多差1。如下图,左边的树是AVL树,右边的则不是。

AVL树的每个结点(在其结点结构中)保留高度信息。可以证明,一般情况下,一棵AVL树的高度最多为1.44log(N+2) – 1.328,并且实际上的高度只比logN稍微多一点。

下图显示了一棵具有最少结点(143)高度为9的AVL树。这棵树的左子树是高度为7且结点数最少的AVL树,右子树是高度为8且结点数最少的AVL树。可以看出在高度为h的AVL树中,最少结点数S(h)=S(h-1) + S(h-2) + 1。对于h=0,S(h) = 1;h=1,S(h)=2。函数S(h)与斐波那契数密切相关,由此可以推出上面的关于AVL树的高度的界。

除了插入操作(假设删除是懒惰删除),所有的AVL树操作都可以以时间O(logN)执行。当进行插入操作时,需要更新通向根节点路径上的那些结点的平衡信息,因为插入操作可能会破坏AVL树的平衡特性。如果AVL树的平衡被破坏,就要进行修正,这里称其为旋转(rotation)。

假设结点a的左子树和右子树失去平衡(高度差2),则可能的情况有下面4种:

1. 对a的左儿子的左子树进行一次插入。

2. 对a的左儿子的右子树进行一次插入。

3. 对a的右儿子的左子树进行一次插入。

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

情形1和4关于结点a对称,情形2和3关于结点a对称,所以理论上只有两种情况,不过从编程的角度来看还是4种情形。

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

单旋转

下图显示了对于情形1如何进行单旋转。左边是旋转前,右边是旋转后。在左边的树中,子树X中新插入了一个结点使得结点k2不再满足AVL平衡性质,因为它的左子树比右子树深2层(图中间的虚线代表树的各层)。

上图中为了使树恢复平衡,把子树X上移了一层,把子树Z下移了一层。这其实已经超额完成了AVL性质的要求。抽象的形容一下单旋转的过程:把树看成是柔软灵活的,抓住子结点k1,用力摇动它,在重力的作用下,k1就变成了新的根。X和Z仍然分别是k1的左儿子和k2的右儿子。子树Y包含原树中介于k1和k2之间的那些结点,现在把它放在新树中k2的左儿子的位置上。这样,所有对顺序的要求都得到满足,旋转后得到的新树是一棵AVL树。不仅如此,其实上图中经过单旋转所得到的新树的高度,和插入新节点(导致原树不平衡)之前原树的高度相同,所以通向根节点的路径上的那些结点不需要进一步调整(它们仍然是平衡的)。下图是一次单旋转的例子:

下图演示应于“情形4”的单旋转修正:

下面演示一个更长一些的例子,从初始的空AVL树开始插入3、2和1,然后依序插入4到7。在插入1时,AVL性质在根处被破坏。于是在根结点与其左儿子之间执行一次单旋转以修正这个问题。

图中以虚线连接的那两个结点是旋转的主体。接着插入4和5,在插入5时破坏了结点3处的AVL性质,通过单旋转将其修正。在编程的时候必须记住:结点2的右儿子必须重新设置以链接到4而不是3,这一点很容易忘记,从而导致树被破坏。

接着插入6,根节点处失去平衡,因为它的左子树高度为0而右子树高度为2。在根节点2和结点4之间执行一次单旋转。

接着插入7,再次执行单旋转。

 

双旋转

对于平衡二叉树失衡的情况2、3,使用单旋转是无效的。

此时需要使用双旋转(两次旋转:“左-右”或“右-左”) 

 

上图中,树B或树C中有一棵比D深两层。为了重新平衡,选择k2作为新的根,k1作k2的左儿子,k3作k2的右儿子。与单旋转一样,把树恢复到插入新节点之前的水平。下图是“右-左”双旋转。

 

继续单旋转时的示例,以倒序插入10-16,接着插入8、9。插入16时并不破坏树的平衡。插入15时引起结点7的不平衡,这属于情况3,需要做一次“右-左”双旋转。

接着插入14,同样也需要做一次“右-左”双旋转。结合上面的“右-左双旋转修正情形3”示意图,子树A的根结点为5,子树B为空树,子树C的根结点为14,子树D的根结点为16。

 插入13,在根结点处失衡。由于13不在4和7之间,所以是上面介绍的失衡情况4(对失衡结点4的右儿子的右子树进行一次插入),这里需要做一次单旋转(左旋)。

 插入12也需要做一次单旋转(右旋)。

 接着插入11、10、8(未作旋转图示),得到下面一棵近乎理想的平衡树。

 最后插入9,需要在8、9、10三个结点之间做“左-右”双旋转。

在编程时,需要计算子树的高度差。有些程序员会在结点的存储结构中,增加一个BF成员(Balance Factor:平衡因子)。这样可以避免平衡因子的重复计算,提高程序的性能,不过却丧失了程序的某些简明性。

下面是一部分例程:

复制代码
struct AvlNode
{
    Comparable element;
    AvlNode   *left;
    AvlNode   *right;
    int       height;

    AvlNode( const Comparable & theElement, AvlNode *lt,
                                            AvlNode *rt, int h = 0 )
      : element( theElement ), left( lt ), right( rt ), height( h ) { }
};

/**
 * Return the height of node t or -1 if NULL.
 */
int height( AvlNode *t ) const
{
    return t == NULL ? -1 : t->height;
}

/**
 * Internal method to insert into a subtree.
 * x is the item to insert.
 * t is the node that roots the subtree.
 * Set the new root of the subtree.
 */
void insert( const Comparable & x, AvlNode * & t )
{
    if( t == NULL )
        t = new AvlNode( x, NULL, NULL );
    else if( x < t->element )
    {
        insert( x, t->left );
        if( height( t->left ) - height( t->right ) == 2 )
            if( x < t->left->element )
                rotateWithLeftChild( t );
            else
                doubleWithLeftChild( t );
    }
    else if( t->element < x )
    {
        insert( x, t->right );
        if( height( t->right ) - height( t->left ) == 2 )
            if( t->right->element < x )
                rotateWithRightChild( t );
            else
                doubleWithRightChild( t );
    }
    else
        ;  // Duplicate; do nothing
    t->height = max( height( t->left ), height( t->right ) ) + 1;
}
复制代码

下面是单旋转函数rotateWithLeftChild的示意图和源码。rotateWithRightChild是对称的,所以未贴出。

 

复制代码
/**
 * Rotate binary tree node with left child.
 * For AVL trees, this is a single rotation for case 1.
 * Update heights, then set new root.
 */
void rotateWithLeftChild( AvlNode * & k2 )
{
    AvlNode *k1 = k2->left;
    k2->left = k1->right;
    k1->right = k2;
    k2->height = max( height( k2->left ), height( k2->right ) ) + 1;
    k1->height = max( height( k1->left ), k2->height ) + 1;
    k2 = k1;
}
复制代码

下面是“左-右”双旋转函数doubleWithLeftChild示意图和源代码,doubleWithRightChild是对称的。

复制代码
/**
 * Double rotate binary tree node: first left child
 * with its right child; then node k3 with new left child.
 * For AVL trees, this is a double rotation for case 2.
 * Update heights, then set new root.
 */
void doubleWithLeftChild( AvlNode * & k3 )
{
    rotateWithRightChild( k3->left );
    rotateWithLeftChild( k3 );
}
复制代码
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:6676次
    • 积分:264
    • 等级:
    • 排名:千里之外
    • 原创:18篇
    • 转载:10篇
    • 译文:0篇
    • 评论:0条
    文章分类
    文章存档