平衡二叉树(
Adelson-Velskii and Landis' tree)是一种高度(height)平衡的BST,拥有O(logn)的平均最坏插入、删除、搜索的BST。性能优异,最关键的是插入和删除操作,而它侧重于搜索占主要操作的应用情况。
AVL的插入和删除离不开树的左旋和右旋操作。下面看下旋转。
先上图:
右旋
操作:
发生右旋操作是因为左子树的高度要比右子树的高度要大2,即节点A的平衡因子为2。出现这种情况是当A的平衡因子为1的时候,在A的左子树中插入了一个节点,使得左子树的高度增1,从而导致了不平衡的产生。右旋的目的是把从A节点开始的这个子树重新平衡起来。
右旋转之前我希望B的左子树的深度要比右子树的深度大或者等于。这样我沿着AB右旋得到以B为根节点的二叉树不会出现不平衡的现象。为甚么需要这么判断呢?分析如下:
设B的左子树为a,B的右子树为b,A的右子树为c,他们的高度依次为|a|,|b|,|c|
因为A的平衡因子变成了2,因此下面的等式成立: max{ |a|+1, |b|+1} - |c| = 2
第一种情况:
我们假设B的左子树的高度大于右子树的高度,即|a|-|b| = 1,当然不会等于2,因为B是平衡的,所谓平衡就是每个节点的平衡因子那么是-1,0那么是1.
那么沿AB,执行右旋之后,我们来看看A和B两个节点的平衡情况,因为旋转不会更改a,b,c三个子树的内部情况,所以他们都是内部平衡的。
首先看A节点:
|a|=|b|+1; 由
max{ |a|+1, |b|+1} - |c| = 2可知|b| = |c|,所以A的平衡因子为|b| - |c| = 0,显然A平衡了。
再来看看B节点:
B的平衡因子 |a| - max{ |b| + 1, |c| + 1},由上式分析可知B的平衡因子也为0.
在这种情况下,右旋达到了平衡这颗子树所有节点的目的。
第二种情况:
我们假设B的左子树和右子树高度相同,即|a| = |b|
我们看看右旋后A和B的平衡情况:
A节点:|b| - |c| = 1;
B节点: |a| - max{ |b| + 1, |c| + 1} = |a| - (|b| + 1) = -1
显然这种情况也能把这颗子树的所有节点都重新平衡。
第三种情况:
这种情况是种例外,如果B的左子树的高度比右子树的高度小1,即B的左子树根节点的平衡因子为-1.
即|a| - |b| = -1,我们看看A和B的右旋后的平衡情况:
A:|b| - |c| = 1
B:|a| - max{ |b| + 1, |c| + 1} = -2
得到的情况是我们右旋后竟然没有得到一颗平衡树,显然这种情况是不能直接用右旋处理的。我们可以想办法把B先处理下,使得B的平衡因子为1或者0. 方法是对B进行左旋。
因为B的平衡因子为-1,所以|a|等于max{ |b|, |d| }而|b|和|d|相差最多为1.因为C是平衡节点。我们看看对BC进行左旋之后,然后对AB进行右旋得到的新树的平衡情况。
我们知道在旋转之前A的左子树的高度为|a| + 2,显然|c| = |a|,而|b|和|d|两个数中有一个值是|a|,另一个值是|a|或者是|a|-1.下面计算ABC三个节点的平衡因子:
A:|b| - |c|那么是0,那么是-1.显然是平衡的
B:|a| - |b|那么是0,那么是1,显然也是平衡的
C:max{ |a| + 1, |b| + 1} - max{ |d| + 1, |c| + 1} , 它的值那么是0,那么也是1.
自此先对BC进行左旋,然后对AB进行右旋的做法,使得这颗子树重新恢复了平衡。
当A节点的平衡因子如果是-2,那么需要进行左旋操作,分的情况刚好跟上面是对称的。
对于B的平衡因子为0或者-1,则直接采用沿AB左旋
对于B的平很因子为1(其实是大于0),则先沿着BC右旋(C为B的左节点),然后沿着AB左旋。
因为无论左旋还是右旋抑或两者兼有,得到的重新平衡的BST,这颗树的高度有可以发生了变化,而因为它的变化使得以它为子树的更大的树有可能失衡,所以需要沿着父节点指针往上回溯,一一重平衡。
重平衡一颗以node为根节点的子树的代码如下:
void balanceAtNode(Node* n)
{
int bal = n->getBalance();
if(bal > 1)
{
if(n->getLeftChild()->getBalance() < 0)
rotateLeft(n->getLeftChild());
rotateRight(n);
}
else if(bal < -1)
{
if(n->getRightChild()->getBalance() > 0)
rotateRight(n->getRightChild());
rotateLeft(n);
}
}
有了上面的分析,现在AVL树的插入操作显得相当的容易了:
bool insert(int val)
{
Node* added_node;
if(root == 0)
{
root = new Node(val);
return true;
}
else
{
Node* temp = root;
while(true)
{
if(temp->getData() > val)
{
if((temp->getLeftChild()) == 0)
{
added_node = temp->setLeftChild(new Node(val));
break;
}
else
{
temp = temp->getLeftChild();
}
}
else if(temp->getData() < val)
{
if((temp->getRightChild()) == 0)
{
added_node = temp->setRightChild(new Node(val));
break;
}
else
{
temp = temp->getRightChild();
}
}
else
{
return false;
}
}
// The following code is for updating heights and balancing.
temp = added_node;
while(temp != 0)
{
temp->updateHeight();
balanceAtNode(temp);
temp = temp->getParent();
}
}
}
AVL树的另一个重要操作就是删除,删除操作分为两大类
第一类是被删除的节点是叶子节点,或者只有一个孩子节点,这个时候把这个节点所在的slot直接删掉。当然如果有孩子节点,那么将这个孩子节点链接到被删节点的父节点上面。
另一类是被删节点既有左子树又有右子树,这个时候是用它的前驱(predecessor)或者是后继(successor)节点去替换被删节点。具体是用前驱还是后继,取决于那种情况更有利于平衡。
删除了一个节点其实有可能改变了以这个节点的祖先节点为根的子树的平衡情况,我们的重平衡也应该循序往上一一平衡。在wikibooks中的代码没有循序往上,是错的,可以写个程序测测。
具体代码参考:http://en.wikibooks.org/wiki/Algorithm_Implementation/Trees/AVL_tree,不过要注意它的remove有错误。