之前我们提到了二叉查找树,我们说过它是一种既简单又有趣的数据结构,它足够强大,也足够简洁,但这有个先决条件,插入的数值足够随机,否则二叉查找树很容易失衡(或者如果数值足够随机,但在不断的删除操作后,也会发生失衡情况),比如当我们使用一个有序数组生成一棵二叉查找树时,实际上相当于生成了链表,查找的时间复杂度为O(n)
,这显然和我们的想要的查找效率存在较大的差距(O(logn)
)。因此,实际二叉查找树的实现都比较复杂,下面我们介绍一种比较常用的数据结构,它可以保证树的深度为 (O(log n)
,这种数据结构称为 平衡树(AVL)
平衡树是每个节点的左子树和右子树高度最多相差1的二叉查找树。
显然,平衡树实际上就是在二叉查找树上添加了限制条件,以此来避免“糟糕”情况的发生。同样的我们只关注“插入节点”,“删除节点”的操作。在某个插入和删除操作中,都有可能导致树的平衡条件被打破,即某个操作可能导致节点i的左子树和右子树的高度相差2,因此,此时我们有必要对二叉树进行必要的调整,使其回到平衡状态,我们使用的操作称为旋转。
考虑下面四种“失衡”情况:
(注意上面的情况都是简化版本,实际情况中各节点都可能存在其他子树,但这些子树对实际操作没有影响,为了便于分析,我们略去)
上述情况分别为:
1. 在k2的左儿子插入左节点,导致k2的右子树比左子树高度低2,失衡。“左左”
2. 在k1的右儿子插入右节点,导致k1的左子树比右子树的高度低2,失衡。“右右”
3. 在k3的左儿子插入右节点,导致k3的右子树比左子树高度低2,失衡。“左右”
3. 在k1的右儿子插入左节点,导致k3的右子树比左子树高度低2,失衡。“右左”
容易发现,前两种情况是对称的,后两者情况也是对称的。
接下来我们需要做的就是通过某些“旋转”操作来使平衡二叉树重新回到平衡状态。首先我们需要明确一点,在某个插入或者删除操作后,AVL的平衡条件被破坏,但在这个操作之前,AVL是满足平衡条件的(不然它就不是AVL树了),因此我们只要关注破坏平衡的那个节点,并使以这个节点为根的子树重新达到平衡状态,则整颗AVL树将会达到平衡状态
对于上述四种情况,破坏平衡的点均在最上面那个节点。现在我们需要解决三点:
1. 如何判断AVL是否失去了平衡。
2. 如何通过旋转操作使AVL重新达到平衡状态。
3. 如何重新调整AVL树节点的高度。
首先,我们先看下AVL数据结构节点的常见定义方法:
struct AvlNode
{
ElementType Element;
AvlTree Left;
AvlTree Right;
int Height;
};
除了二叉树的常规属性外,我们还维护着节点的高度,某个节点的高度指该节点距离其最远的子叶子节点的距离,叶子节点高度为0。
有了上述定义,我们可以解决第一个问题。如下图,
当我们插入9时,节点将通过递归的方式插入到节点7的左子树,如下:
在插入操作中,我们通过递归插入,然后在插入的递归返回中,对每个节点我们都比较其左右子树的高度,判断下式是否成立Height(T->Left) - Height(T->Right) == 2
当返回到达节点5时,发现上式成立,此时平衡被破坏。得出5为破坏平衡的节点。
通过这种递归返回不断比较节点高度的方法就可以判断当前节点是否失去了平衡
对于第二个问题我们需要分情况讨论。
当破坏平衡的情况为左左时,即如图
旋转策略为:k2的父节点指向k1,k1的右子树指向k2。这种方式称为左旋。结果如下
实现代码如下:
/**
* @param k2 失去平衡节点
*/
static Position SingleRotateWithLeft(Position k2)
{
Position k1;
k1 = k2->Left;
k2->Left = k1->Right;
k1->Right = k2;
return k1;
}
注意到我们的输入参数为k2,返回参数为k1。它们分别为平衡操作前后 失去平衡的子树根节点。(这里我们使用了一种比较简单的标记方法:通过给几个关键节点进行标记,则在画图后能够迅速得出节点的变化情况)。
对于情况二,即右右情况,和这个是对称的,平衡前后分别为
代码如下:
static Position SingleRotateWithRight(Position k1)
{
Position k2;
k2 = k1->Right;
k1->Right = k2->Left;
k2->Left = k1;
return k2;
}
接下来我们看“左右”情况,此时我们需要使用双旋转,先看下图实现
“左右”情况需执行左双旋转,即以k1为根结点执行一次右旋,再以k3为根结点执行一次左旋。以单旋转为基础,代码如下:
static Position DoubleRotateWithLeft(Position k3)
{
// k1和k2右旋,k1变成k2的左子树
k3->Left = SingleRotateWithRight(k3->Left);
// k2和k3左旋,k3变成k2的右子树
return SingleRotateWithLeft(k3);
// 结果就是k2跑到了该子树根的位置
}
另一种情况类似。
画出基础图后,四种旋转方式其实是很容易写的。ok,现在我们解决了第二个问题,那第三个问题,如何重新调整AVL树节点的高度如何实现呢?
我们先看一下任意节点的高度求法:k->Height = Max(Height(k->Right), Height(k->Left)) + 1;
因为树是递归定义的,所以上述方法是显而易见的。同样的,调整高度有两种情况:1. 当前节点破坏了树的平衡。2. 当前节点没有破坏树的平衡。
对于第一种情况,我们以左旋为例:
重新平衡后,k2的高度应该为:k2->Height = Max(Height(k2->Right), Height(k2->Left)) + 1;
k3因为没有变化,无需修改,k1的高度为:k1->Height = Max(Height(k1->Left), k2->Height) + 1;
注意 Height(k1->Left) = Height(K3)
此时左旋代码修改如下:
static Position SingleRotateWithLeft(Position k2)
{
Position k1;
k1 = k2->Left;
k2->Left = k1->Right;
k1->Right = k2;
k2->Height = Max(Height(k2->Right), Height(k2->Left)) + 1;
k1->Height = Max(Height(k1->Left), k2->Height) + 1;
return k1;
}
对于第二种情况,因
为可以满足AVL平衡条件,且可保证其子树高度都是正确的,因此可以直接使用k->Height = Max(Height(k->Right), Height(k->Left)) + 1;
计算。
上述三个问题解决后,在进行插入或删除等可能破坏平衡状态的操作时就可以判断是否需要进行平衡操作并执行对应旋转来使AVL树重新达到平衡状态了。
参考:数据结构与算法分析