引言
前面一篇博客中我们讲到二叉查找树的评价查找效率可达到O(logN),但是它在最坏情况下的查找效率可能退化为O(N),如下所示:
显然此时二叉查找树已经退化为链表,从而查找效率低下.
为了解决这个问题,有许多的前辈做了无数的优化工作,核心思想都是保持二叉树的平衡或者基本平衡,主要的成果为平衡二叉树(AVL),红黑树,Treap树,伸展树(Splay Tree).
今天先介绍基础的平衡二叉树,理解了它,有助于理解其他的优化方式.
ps:维基等将平衡二叉树定义为一大类,包括AVL树,红黑树,Treap树等。而大陆的书本中平衡二叉树比较狭义,仅仅指AVL树。我个人倾向于维基的定义.
1.AVL的定义
AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G.M. Adelson-Velsky和E.M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。
AVL节点的定义如下:
|
|
可见,相比之前的BinaryNode,多了一个height字段.其中高度的定义和深度的定义恰好相反:空节点是-1,叶子节点是0,非叶子节点的height往根节点递增,比如下图:
从而可得到求节点高度的方法:
|
|
2.平衡操作
为了保持二叉查找树的平衡,在每次插入或删除节点时,如果失衡,就需要进行平衡操作.常见的失衡情况有以下几种:
2.1 左左情形,需要右旋
所谓左左情形,即插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致”根的左子树的高度”比”根的右子树的高度”大2,从而使二叉树失衡.
这种情况需要右旋,如下图所示:
显然,右旋其实是使leftChild成为rootNode的父节点,并且rootNode成为leftChild的右孩子,附带的还有leftChild的右孩子成为rootNode的左孩子.代码如下:
|
|
需要注意的是,虽然newTop.right的位置发生了变化,但它的height并未发生变化,故不需要重新获取它的高度.其他旋转类似.
2.2 右右情形,需要左旋
所谓右右情形,即插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致”根的右子树的高度”比”根的左子树的高度”大2,从而使二叉树失衡.
这种情况需要左旋,如下图所示:
显然,左旋操作的实质是:rightChild成为rootNode的父节点,而rootNode成为rightChild的左孩子,附带操作是rightChild的左孩子成为rootNode的右孩子.代码如下:
|
|
2.3 左右情形,需要先左旋,然后右旋
所谓左右情形,即插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致”根的左子树的高度”比”根的右子树的高度”大2,从而使二叉树失衡.
这种情况需要先左旋,然后右旋,如下图所示:
显然,左右情形只需要使用前面的左旋和右旋组合:
|
|
2.4 右左情形,需要先右旋,然后左旋
所谓右左情形,即插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致”根的右子树的高度”比”根的左子树的高度”大2,从而使二叉树失衡.
这种情况需要先右旋,后左旋,如下图所示:
类似的,右左情形只需要使用右旋和左旋组合:
|
|
3.插入
插入其实跟二叉查找树一样,只不过需要在每次插入结束后检查是否失衡,如失衡则需要进行相应旋转.
|
|
4.删除
删除与普通的二叉查找树一样,只不过在删除节点后需要进行平衡操作.
|
|